# mkiso dcmtk Replacement — Implementation Details ## Core Architectural Change dcm4che2's `dcmqr` is a **monolithic** tool: one process handles the storage SCP, C-MOVE SCU, query, AND file writing. dcmtk separates these into **two processes** that must run concurrently: ``` ┌─────────────────────────────────────────┐ │ dcm4che2 dcmqr (single process) │ │ ┌─────────────────────────────────────┐│ │ │ Storage SCP (listen CDRECORD:10104) ││ │ │ C-MOVE SCU ││ │ │ File writer (−cstoredest) ││ │ └─────────────────────────────────────┘│ └─────────────────────────────────────────┘ ┌──────────────┐ ┌──────────────┐ │ storescp │ │ movescu │ │ AE:CDRECORD │ │ AE:CDRECORD │ │ port:10104 │ │ -aem CDRECORD│ │ -od DICOMDIR│ │ +P 10104 │ └──────┬───────┘ └──────┬───────┘ │ │ │ receives │ sends C-MOVE-RQ │ C-STORE │ to PACS ▼ ▼ ┌─────────────────────────────┐ │ PACS (ABPACS:11112) │ └─────────────────────────────┘ ``` ## Command Templates ### 1. storescp (background receiver) ```bash # Start storage SCP in background, capture PID /data/dcmtk-bin/storescp 10104 \ -aet CDRECORD \ -od "${dicomdir}/DICOMDIR" \ +xa \ &> /tmp/storescp_${uniq}.log & STORESCP_PID=$! # Give storescp time to bind the port sleep 1 ``` **Key flags:** | Flag | Purpose | |------|---------| | `10104` | Listen port (must match what PACS sends to) | | `-aet CDRECORD` | AE title (must match C-MOVE destination AE) | | `-od DICOMDIR` | Output directory for received files | | `+xa` | Accept all transfer syntaxes (max compatibility) | ### 2. movescu (C-MOVE initiator) — Simplified (no modality loop) ```bash /data/dcmtk-bin/movescu \ -aet CDRECORD \ -aec ABPACS \ -aem CDRECORD \ localhost 11112 \ -S \ -k 0008,0050="${accession_number}" \ -k 0010,0020="" \ --no-port ``` **Key flags:** | Flag | Purpose | |------|---------| | `-aet CDRECORD` | Our AE title (calling) | | `-aec ABPACS` | PACS AE title (called) | | `-aem CDRECORD` | Move destination AE (tells PACS to C-STORE to CDRECORD) | | `localhost 11112` | PACS host:port | | `-S` | Study Root query model | | `-k 0008,0050=X` | Accession Number query key | | `-k 0010,0020=""` | Patient ID = wildcard (required for Study Root) | | `--no-port` | No incoming port on movescu (storescp handles incoming) | ### 3. movescu — Conservative (with modality loop, replicating original behavior) ```bash # Same as above but add modality filter via query key /data/dcmtk-bin/movescu \ -aet CDRECORD \ -aec ABPACS \ -aem CDRECORD \ localhost 11112 \ -S \ -k 0008,0050="${accession_number}" \ -k 0010,0020="" \ -k 0008,0060="${modality_code}" \ --no-port ``` **Note:** `-k 0008,0060=MR` filters queries to studies with Modality=MR. This is NOT identical to dcm4che2's `-cstore MR` (which filters at the association/presentation-context level). For most PACS, the query-level filter is sufficient. If the PACS requires specific presentation context negotiation per modality, the simplified approach (no modality loop) with `+xa` (accept all) is recommended. ### 4. Cleanup ```bash # After movescu completes (or times out): kill $STORESCP_PID 2>/dev/null wait $STORESCP_PID 2>/dev/null ``` ## Per-File Replacements ### mkiso.php — current Java block (lines ~56-59) **Current:** ```php foreach($modalities as $cstore=>$v) { $cmd = "JAVA_HOME=/usr/lib/jvm/jdk1.8.0_144 LANG=en_US.iso-8859-1 /usr/local/dcm4che/dcm4che2/bin/dcmqr -L CDRECORD:10104 ABPACS@localhost:11112 -cmove CDRECORD -qAccessionNumber=${accession_number} -cstore $cstore -cstoredest $dicomdir/DICOMDIR"; exec($cmd, $outputRes); } ``` **Replace with (simplified approach):** ```php // Start storescp as background storage receiver $storescp_pid = exec("/data/dcmtk-bin/storescp 10104 -aet CDRECORD -od ${dicomdir}/DICOMDIR +xa &> /tmp/storescp_" . basename($dicomdir) . ".log & echo $!"); // Allow storescp to bind port sleep(1); // Single C-MOVE for the accession number (Study Root model) $cmd = "/data/dcmtk-bin/movescu -aet CDRECORD -aec ABPACS -aem CDRECORD localhost 11112 -S -k 0008,0050=${accession_number} -k 0010,0020= --no-port"; exec($cmd, $outputRes, $exitCode); // Clean up storescp exec("kill $storescp_pid 2>/dev/null"); ``` ### mkiso2.php — current Java block (lines ~56-59) Same C-MOVE replacement as mkiso.php. **Additionally, replace dcmsend (line ~62):** **Current:** ```php $cmd = "/usr/bin/dcmsend +sd +rd 172.16.0.120 104 $dicomdir/DICOMDIR"; ``` **Replace with:** ```php $cmd = "/data/dcmtk-bin/storescu -aet CDRECORD +sd +r 172.16.0.120 104 ${dicomdir}/DICOMDIR"; ``` `/usr/bin/dcmsend` does not exist on the current system. `storescu +sd +r` (scan directories + recurse) is the dcmtk equivalent. ### mkiso_multiple.php — current Java block (lines ~80-87) **Current (nested loop):** ```php foreach($modalities as $cstore=>$v) { foreach($as as $accession_number) { $cmd = "JAVA_HOME=... dcmqr ... -qAccessionNumber=${accession_number} -cstore $cstore -cstoredest $dicomdir/DICOMDIR"; exec($cmd, $outputRes); } } ``` **Replace with (simplified, one C-MOVE per accession):** ```php // Start storescp once for all accessions $storescp_pid = exec("/data/dcmtk-bin/storescp 10104 -aet CDRECORD -od ${dicomdir}/DICOMDIR +xa &> /tmp/storescp_" . basename($dicomdir) . ".log & echo $!"); sleep(1); foreach($as as $accession_number) { $cmd = "/data/dcmtk-bin/movescu -aet CDRECORD -aec ABPACS -aem CDRECORD localhost 11112 -S -k 0008,0050=${accession_number} -k 0010,0020= --no-port"; exec($cmd, $outputRes, $exitCode); } exec("kill $storescp_pid 2>/dev/null"); ``` ## DICOM Tag Reference | Tag | Name | Used As | |-----|------|---------| | (0008,0050) | Accession Number | Query key — filters by accession | | (0008,0052) | Query/Retrieve Level | Set by `-S` (Study) — queries at study level | | (0008,0060) | Modality | Query key — filters by modality (if using conservative approach) | | (0010,0020) | Patient ID | Required wildcard for Study Root Q/R | ## Error Handling Considerations 1. **storescp already bound**: If `10104` is in use, storescp will fail. Use a random port or check first. 2. **movescu timeout**: Default is unlimited. Consider `-to 300` (5 min timeout) for production. 3. **PACS unreachable**: movescu returns non-zero exit code — PHP should handle this (currently scripts have no error handling). 4. **Partial retrieval**: movescu may return success even if some images fail. The exit code reflects the C-MOVE response status. 5. **Race condition**: The `sleep 1` is a heuristic. For production, consider polling to check storescp is ready. ## Verification Commands ```bash # Test storescp starts correctly /data/dcmtk-bin/storescp 10104 -aet CDRECORD -od /tmp/test_dicom +xa & PID=$! sleep 1 echo "storescp PID: $PID (should be running)" kill $PID # Test movescu against PACS (dry run verification) /data/dcmtk-bin/movescu -aet CDRECORD -aec ABPACS -aem CDRECORD localhost 11112 \ -S -k "0008,0050=MR.180505.026" -k "0010,0020=" --no-port -v # Test storescu (replacement for dcmsend) /data/dcmtk-bin/storescu -aet CDRECORD +sd +r 172.16.0.120 104 /tmp/test_dicom/ -v ```