feat: base go mkiso

This commit is contained in:
2026-06-05 08:11:44 +07:00
commit 983667a76a
63 changed files with 5322 additions and 0 deletions

View File

@@ -0,0 +1,218 @@
# 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
```