# 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
```
