A low-code DICOM communication validator and PACS admin toolkit.
Published as-is. All rights reserved. The source is public so you can read it and download builds. This is not an invitation to use it as a library, fork it, or send patches. Issues and pull requests are not accepted. See CONTRIBUTING.md.
Configure this workstation as a DICOM Application Entity, register remote nodes (PACS, Orthanc, RIS/MWL, modalities), impersonate extra calling AE Titles, and run the checks a connectivity ticket actually needs: network PING, C-ECHO, simulated C-STORE, PDF to Encapsulated PDF Storage, Study Root C-FIND (including Study / Series / Image), Modality Worklist C-FIND, and HL7 v2 send over MLLP.
Dicommunication is one application, with a top tab row for each product and a shared Configuration tab: Dicommunication (the network/DIMSE/HL7 workstation tools), Dicomtag Analytics (Study Root C-FIND, including Vue ELSCINT1 keys, plus listing and retrieving DICOM Structured Reports), Dicom Anonymizer (query, retrieve, and anonymize studies/series/images — see below), Dicom Router (scheduled C-FIND rules with optional retrieve/forward — see below), and Dicom Cleaner (query, retrieve, redact a rectangle of burned-in pixel data, and send the result back over C-STORE — see below). Each tab's content area has its own left sidebar with that product's own pages. Local AE, virtual identities, remote nodes, and logs are configured once, under the Configuration tab, and shared by every product. The Windows MSI and macOS DMG each install a single shortcut / app.
The web UI is FastAPI + HTMX. DICOM uses pynetdicom/pydicom. New test tools are Python plugins: drop a file in app/tools/ and it appears in the Dicommunication sidebar. The sidebar About button shows the running version; Help is the in-app administrator guide.
Open http://127.0.0.1:8080.
Synthetic lab studies (not clinical data). Light theme, v0.3.0.
Dicommunication — workstation dashboard, remotes, Test tools.
Dicomtag Analytics — Study Root C-FIND, then retrieve Structured Report text (Findings / Impression, Copy / CSV / JSON).
- Screenshots
- What this is (and is not)
- DICOM services this tool distinguishes
- Run it
- Windows MSI
- macOS DMG
- Where data is stored
- Configuration
- Logs
- Virtual local AE titles
- Test tools
- Worklist
- HL7 send
- Talking to Orthanc on a LAN
- JSON API
- Add a tool plugin
- Tests
- Security
- Troubleshooting
This is a trusted-network admin workstation. Use it on the PACS VLAN, a lab, or a jumphost that can reach DICOM ports. It is not a PACS, not a viewer, and not a replacement for vendor modality simulators.
It does:
- Present a calling AE Title (the workstation, or a virtual identity such as
CT1) - Associate with a remote AE and show which SOP Classes were accepted or rejected
- Send Verification (C-ECHO), a tiny Secondary Capture (C-STORE), Encapsulated PDF Storage (PDF to DICOM), Study Root Query/Retrieve C-FIND at Study, Series, or Image level, and Modality Worklist C-FIND
- Optionally serve a local web worklist as an MWL SCP on the listen port
- Optionally accept C-STORE of Structured Reports on that same port, and C-MOVE SR series from Dicomtag Analytics so the Content Sequence can be parsed (no language model)
- Send an HL7 v2 message over TCP (MLLP by default) to a host:port
It does not:
- Implement C-GET, or C-MOVE of imaging series (only SR series used for report text)
- Store a real archive of clinical images
- Speak DICOM TLS, or authenticate the web UI
- Parse, validate, or map HL7 fields — paste a message and send it
A successful C-ECHO only proves Verification. Orthanc (or any PACS) can accept C-ECHO and still reject Storage, Query/Retrieve, or Modality Worklist. That difference is the point of the Testbench.
| UI name | DIMSE | SOP Class | What it actually tests |
|---|---|---|---|
| C-ECHO | C-ECHO | Verification 1.2.840.10008.1.1 |
Association plus a DICOM ping. Connectivity only. |
| C-STORE | C-STORE | Secondary Capture Image Storage | Can the peer receive an instance? |
| PDF to DICOM | C-STORE | Encapsulated PDF Storage 1.2.840.10008.5.1.4.1.1.104.1 |
Wrap a PDF as a DICOM document and store it. A peer that takes Secondary Capture may still reject Encapsulated PDF. |
| C-FIND | C-FIND | Study Root Query/Retrieve FIND | Search stored studies in a PACS archive at STUDY level. Zero matches can still be a successful Q/R C-FIND. |
| Dicomtag Analytics | C-FIND, optional C-MOVE | Study Root Query/Retrieve FIND (and MOVE for SR) | Reached at /tools/c-find-advanced in the sidebar. Same SOP Class at Study, Series, or Image plus optional Vue ELSCINT1 keys. Hierarchical: Series needs Study Instance UID; Image needs Study and Series Instance UID. List SR reports is Series C-FIND with modality SR for every study in the table. Retrieve report text C-MOVEs each listed SR on its own association and parses the Content Sequence. Results copy / CSV / JSON. |
| MWL C-FIND / Worklist | C-FIND | Modality Worklist 1.2.840.10008.5.1.4.31 |
Search scheduled procedures, not the archive. |
| Dicom Router | C-FIND, optional C-MOVE + C-STORE | Study Root Query/Retrieve FIND (MOVE/STORE for retrieve-and-forward) | Reached at /router in the sidebar. Same FIND as Dicomtag Analytics, but scheduled/automatic instead of manual, with an optional automatic retrieve-and-forward to one or more destination nodes. |
| Dicom Cleaner | C-FIND, C-MOVE, C-STORE | Study Root Query/Retrieve FIND/MOVE, then Storage back out | Reached at /tools/dicom-cleaner in the sidebar. Study-level C-FIND, C-MOVE retrieve, blacks out one operator-configured rectangle of pixel data on every retrieved instance (PyDicom + numpy), then C-STOREs the cleaned instances back to a PACS. |
MWL C-FIND and the Worklist page are the same SOP Class. Study Root C-FIND is not. Orthanc without the worklist plugin typically accepts Verification, Storage, and Q/R, then rejects MWL. The Testbench and Worklist results show accepted vs rejected presentation contexts so that is visible.
Two AE Titles matter on a worklist query:
- Calling AE Title — who you are on the association. The remote must allow this AE (Orthanc
DicomModalities, vendor “known AEs”, etc.). - Scheduled Station AE Title — a query key in the MWL identifier. Modalities normally ask for procedures scheduled to their own station (
CT1,MR1, …).
The image already contains Python 3.12, FastAPI, pynetdicom, pydicom, and ping. You do not need a local virtualenv.
git clone https://github.com/arnoutpro/dicommunication.git
cd dicommunication
docker compose up --buildOpen http://127.0.0.1:8080. Stop with Ctrl+C, or run detached:
docker compose up --build -dCompose publishes:
| Port | What | Published on | Override |
|---|---|---|---|
8080 |
Web UI and JSON API | 127.0.0.1 — this machine only |
DICOMM_HTTP_BIND |
11112 |
Optional local MWL SCP (only listens if you enable it in Configuration) | every interface, so a modality can C-FIND this workstation | DICOMM_DICOM_BIND |
The UI has no login, so it is kept on loopback. To reach it from another machine, put an authenticating reverse proxy in front and publish it deliberately:
DICOMM_HTTP_BIND=0.0.0.0 docker compose up -dDICOMM_DICOM_BIND pins the MWL SCP to one address (for example DICOMM_DICOM_BIND=10.0.0.5) when the host has several NICs and only one faces the modality VLAN.
Startup names the address the UI was published on, so docker compose logs answers "why can I not reach this from my laptop":
Dicommunication 0.3.0 started (data dir /app/data, log level INFO)
Web UI published on 127.0.0.1:8080 — reachable from the Docker host only. The UI has
no login, so this is the default. To reach it from another machine, restart with
DICOMM_HTTP_BIND=0.0.0.0 and put an authenticating reverse proxy in front of it.
It also sets host.docker.internal so a PACS on the Docker host (typical on a Mac) is reachable from inside the container.
Pushes to main build ghcr.io/arnoutpro/dicommunication:latest. After that image exists:
docker compose pull
docker compose up -dor without Compose:
mkdir -p ~/.dicommunication
docker run --rm \
-p 127.0.0.1:8080:8080 -p 11112:11112 \
-v "$HOME/.dicommunication:/app/data" \
-e DICOMM_DATA_DIR=/app/data \
ghcr.io/arnoutpro/dicommunication:latest-p 127.0.0.1:8080:8080 keeps the login-less UI on this machine. Drop the 127.0.0.1: only behind an authenticating reverse proxy.
git pull origin main
docker compose up --build -dRebuilding the image does not wipe AE titles if config lives in ~/.dicommunication (see below).
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements-dev.txt
make runThat serves the UI on port 8080 with reload. Use this when developing plugins; Docker is the supported way to run it on a PACS admin laptop.
The browser does not need Python. Opening http://127.0.0.1:8080 is a normal web page.
Python is required to serve that page and to speak DICOM (FastAPI, pynetdicom, ping, disk config). You should not install Python yourself on a locked-down PACS PC. The MSI freezes a private runtime into Program Files\Dicommunication. Docker does the same thing inside the image. A local venv is only for developers.
Those components cannot be installed from this app’s webpage. The UI is served by the Python process, so the page only exists after the backend is already running. A “download Python from this screen” button would be a chicken-and-egg, and a web bootstrapper that fetches python.org at install time is a poor fit for hospital VLANs (often offline, SmartScreen/AV, no admin). The MSI is the offline installer IT can push with Intune or GPO. A public download page can host that MSI later; it still will not pip-install the server from inside the running UI.
CI builds dicommunication-<version>-win64.msi on windows-latest (workflow Windows MSI). A v* tag attaches that MSI to the GitHub Release and also publishes dicommunication.msi on GitHub Packages (NuGet). GitHub has no MSI registry; the nupkg carries tools/dicommunication-<version>-win64.msi. GitHub Packages always needs a token, even for a public package — anonymous hospital downloads should keep using the GitHub Release.
dotnet nuget add source --name github-arnoutpro --username YOUR_GITHUB_USERNAME --password YOUR_PAT --store-password-in-clear-text https://nuget.pkg.github.com/arnoutpro/index.json
dotnet nuget install dicommunication.msi --version 0.3.0 --source github-arnoutpro
The already-cut v0.2.0 MSI can be wrapped without rebuilding: Actions → Windows MSI → Run workflow and set nuget_from_release to v0.2.0.
The setup wizard shows a single Dicommunication feature (on by default), with an off-by-default Desktop shortcut sub-feature alongside the Start Menu one it always gets. Upgrading from an older, multi-shortcut install removes the previous Dicomtag Analytics / Dicom Anonymizer / Dicom Router / Dicom Cleaner shortcuts automatically.
Install the MSI, start Dicommunication from the Start menu (or the Desktop, if that shortcut was selected). The UI opens in its own window (Edge WebView2 — not a browser tab). Close that window to stop the server if this shortcut started it. Config lives in %LOCALAPPDATA%\dicommunication and survives upgrades. Windows 10/11 already have WebView2; if it is missing the app falls back to the default browser. Uninstalling asks, once, whether to also delete that folder — it can hold patient data from past runs (see SECURITY.md); answering No (the default) leaves it in place, same as before.
Unsigned builds trigger SmartScreen until a code-signing certificate is used. If a modality must C-FIND this workstation, allow inbound TCP for dicommunication.exe (listen port 11112). Details and a local build script: packaging/windows/README.md.
Same idea as the MSI: the browser does not need Python. The DMG freezes a private runtime into a single self-contained Dicommunication.app bundle. You should not install Python yourself on a locked-down PACS Mac. Docker does the same thing inside the image.
Those components cannot be installed from this app’s webpage. The UI is served by the Python process, so the page only exists after the backend is already running. Ship the DMG through IT (or a USB stick), not through a button on localhost.
CI builds dicommunication-<version>-macos-arm64.dmg on macos-latest (workflow macOS DMG). A v* tag attaches that DMG to the GitHub Release next to the Windows MSI.
The already-cut v0.2.0 release can get a DMG without a new version tag: Actions → macOS DMG → Run workflow and set release_tag to v0.2.0.
Open the DMG, drag Dicommunication.app to Applications (or straight to the Desktop for a shortcut). Right-click and choose Open the first time (unsigned builds trip Gatekeeper). The Dock icon is the arnout.pro brand mark. The UI opens in its own window (not a Safari tab). Close the window or quit from the Dock to stop the server. Config lives in ~/.dicommunication and survives upgrades.
Apple Silicon only for now. Intel Macs keep using Docker Compose. If a modality must C-FIND this workstation, allow incoming TCP for Dicommunication (listen port 11112). Details: packaging/macos/README.md.
Linux keeps using Docker Compose.
| File | Contents |
|---|---|
config.json |
Local AE, virtual identities, remote nodes, logging settings |
results.json |
Recent tool runs (capped at 200) |
worklist.json |
Local web worklist entries |
hl7_messages.json |
Saved HL7 v2 drafts for the sender |
route_rules.json |
Dicom Router rule definitions (source PACS, filters, schedule, destinations, status) |
route_runs.json |
Dicom Router run history (capped at 500), including matched studies' patient/accession data |
dicommunication.log |
Rotating application log (level and size set on Logs) |
Default directory:
$DICOMM_DATA_DIRif set (Docker Compose sets this to/app/data; the Windows launcher sets%LOCALAPPDATA%\dicommunicationwhen unset)- else
%LOCALAPPDATA%\dicommunicationon Windows - else
~/.dicommunicationon the host (/app/datain the container, bind-mounted to~/.dicommunication) - else legacy
./dataif that folder already hasconfig.jsonand~/.dicommunication/config.jsondoes not exist (non-Windows only)
Writes are atomic (temp file + replace). Replacing the Docker image does not reset this folder.
Open the Configuration tab. Child links stay visible in its own left sidebar: Configured nodes, Local DICOM AE, Virtual AEs, Remote nodes, and Logs — shared across every product tab, so it's configured once. The overview (/config) lists everything at a glance. Add or edit on the child pages:
/config/local— Local DICOM AE/config/identities— virtual local AE titles/config/remotes— remote DICOM nodes
On Local AE, Advanced settings hides PDU, timeouts, and MWL SCP options.
This is the real Application Entity of this software: listen address, listen port, timeouts, and the default calling AE Title when you are not impersonating a modality.
| Field | Default | Meaning |
|---|---|---|
| AE Title | DICOMM |
Calling AE Title on associations unless a virtual identity is selected. 1–16 printable ASCII characters, no backslash. Remote nodes must accept this AE. |
| IP address | 0.0.0.0 |
Bind / listen address. 0.0.0.0 listens on all interfaces. DICOM connections to remotes use the remote’s host, not this field. |
| Hostname | empty | Documentation only. Listen still uses the IP address. |
| Port | 11112 |
Listen port for the optional local MWL SCP. Common DICOM ports elsewhere: 104, Orthanc 4242. |
| Timeout | 10 s |
ACSE, DIMSE, and network timeout (1–120). |
| Max PDU | 16382 |
Association max PDU (4096–131072). |
| Implementation version | DICOMM_1 |
DICOM implementation version name (≤16 characters). |
| Station AE Title | empty | Default Scheduled Station AE Title for worklist queries when no virtual identity is selected. |
| Serve the web worklist over DICOM | off | Start an MWL SCP on the listen port, backed by the local web worklist. |
The listen port is one workstation AE. Virtual titles do not add extra SCP ports.
A remote is any peer you want to test: PACS, Orthanc, RIS/MWL, modality, VNA, or a vendor test SCP.
| Field | Meaning |
|---|---|
| Display name | Label in the UI |
| AE Title | Called AE Title (what you send as the called AE on associate) |
| Hostname / IP | Connection uses IP when set, otherwise hostname. Fill one or both. |
| Port | DICOM port (104, 11112, 4242, …) |
| Node type | PACS, DMWL, modality, VNA, other |
| Provides Modality Worklist | Marks the node as an MWL SCP. Choosing type DMWL also sets this. |
| Notes | VLAN, TLS front-end, vendor, contact — not sent on the wire |
From Docker, a PACS on the same Mac or Linux host is usually host.docker.internal. On Linux Compose this mapping is already added.
Open Logs under the Configuration tab (/logs).
The page has two parts:
- Amount and file size — log level (
DEBUG,INFO,WARNING,ERROR), maximum size of the current file (1–50 MB), and how many rotated files to keep. Changes apply immediately and are stored inconfig.json. - Log view — the tail of
dicommunication.log, refreshed every two seconds. Download the current file, or clear it (rotated files stay).
INFO records startup, configuration changes, tool runs, and MWL SCP start/stop. DEBUG also records HTTP requests (except health, static files, and the live tail poll). The console window on Windows shows the same stream.
A modality does not query worklist as a generic workstation. It associates as CT1 (calling AE) and asks for scheduled procedures for station CT1.
Virtual local AE titles are saved impersonation identities for outbound associations. Add as many as you need (CT1, MR1, US1, …). They do not listen.
| Field | Meaning |
|---|---|
| Display name | e.g. CT scanner 1 |
| Calling AE Title | Who you are on C-ECHO, C-STORE, C-FIND, and MWL |
| Station AE Title | Worklist query filter. Empty means “same as calling AE”. |
| Default modality | Optional MWL/Q/R filter (CT, MR, …) filled when you pick this identity |
| Notes | Room, vendor, Orthanc modality key |
On Worklist, Testbench, and each tool page, use Present as:
(workstation)— the Local DICOM AE Title- a virtual identity — calling AE + station/modality defaults from that identity
The result header shows as CT1 (or whatever calling AE was used).
Every virtual calling AE must be allowed on the remote, the same way the workstation AE is. For Orthanc, add each one to DicomModalities.
The Dicommunication tab's own left sidebar lists these directly (no separate "Test tools" fold — that's the whole point of this tab): Dashboard, Testbench, C-ECHO board, Worklist, then Connectivity (PING), DIMSE (C-ECHO, C-STORE, PDF to DICOM, C-FIND, MWL C-FIND, Tag Editor), and HL7 (HL7 send), each category foldable. Dicomtag Analytics, Dicom Anonymizer, and Dicom Cleaner have their own tabs instead of living in this list. The category that contains the current page stays open. New plugin tools appear under their category, unless promoted to their own tab.
One form to send C-ECHO, C-STORE, Study Root C-FIND, or MWL C-FIND to a selected remote, optionally as a virtual AE.
- C-STORE sends a 16×16 Secondary Capture: patient
ARNPRO^TESTBENCH/ARNPRO-TEST, modalityOT. Look that instance up on the PACS if Storage was accepted. - C-FIND is Study Root, STUDY level. Leave filters empty for a broad query. Optional: patient name/ID, accession, study date, modality (
ModalitiesInStudy). - MWL C-FIND uses the worklist SOP Class and the identity’s station AE unless you override it.
- The result lists SOP Classes negotiated (accepted vs rejected) plus returned/stored records and the association log.
Runs Verification against every configured remote in one click (up to 8 in parallel). Uses the workstation calling AE, not a virtual identity. Also POST /api/echo-board/run.
DNS resolve, ICMP echo, then TCP connect to the DICOM port (the same kind of check as PowerShell Test-NetConnection -Port). ICMP is often blocked on clinical networks; the TCP check is the useful layer-4 result.
Same engines as the Testbench, one page each: /tools/c-echo, /tools/c-store, /tools/c-find, /tools/mwl-find. Each has Present as.
Study Root Query/Retrieve FIND at Study, Series, or Image, with the searchable keys for that level. Checked keys are sent as return columns; a typed value is also a matching key.
Hierarchical FIND (no relational queries): Series keys unlock after Study Instance UID is present; Image keys unlock after Study Instance UID and Series Instance UID. MR-only keys such as Repetition Time stay locked unless Modality is MR or empty. Keys are grouped by Study / Series / Image as a list (not a grid). Collapsed Vue PACS (ELSCINT1) lists expose Tamar private study and series tags as optional return/match keys. Vue’s DCS does not list them. Tamar Assign To Doctor is a confirmed matching key on Vue 12.2.8 (used with Modalities in Study); other Vue tags may still come back empty. Grid Token sequences are not sent. Results are a column-aligned table. Copy table is tab-separated for Excel; Download CSV and Download JSON export the same rows. Click a result row to copy parent UIDs into the next level.
Radiology reports stored as DICOM SR are a series in the same study (same Study Instance UID, different Series Instance UID, modality SR). After a Study query (for example today’s CTs), List SR reports runs Series C-FIND with Modality=SR for every study in the table (cap 200). Retrieve report text C-MOVEs each listed SR on its own association so Vue cannot stop after the first exam. Enable Accept C-STORE (Structured Reports) on Local DICOM AE, and register that AE Title (not a virtual Present as title) in Vue as a C-MOVE destination at this host:listen-port. The Content Sequence is flattened into concept / type / value rows — no language model. Download JSON (and CSV) include Findings and Impression as their own fields, plus sr_text for the full report. Imaging series are not stored. C-GET is not used.
Study-level C-FIND on Patient ID / Accession Number / Study Date (required) / Modality, same fields and rules as Dicomtag Analytics. Pick studies from the result table and a level — Study anonymizes exactly what you checked; Series or Image C-MOVEs every series or image inside those studies (no per-series/per-image picker yet). Four modes: Nuke (keep only what a viewer needs to open the image, drop everything else, fresh UIDs), Fuzz (keep every tag, scramble its value, pixel data untouched), Remove patient information (erase or scramble only a curated ~50-tag patient-identifying list), and Custom (per-tag keep / erase / replace, same list). Study/Series/SOP Instance UIDs are remapped consistently across one run so a multi-instance study stays one coherent study afterward. Output is loose .dcm files or a ZIP in a folder you choose (7z not implemented yet); filenames are always built from the anonymized UIDs, never the originals.
The simple C-FIND tool and Testbench stay STUDY-level with a short filter list.
A background service you manage alongside everything else, not a one-off query form — the Dicom Router tab's own left sidebar lists every configured rule with a small status dot — green (active), amber (paused), gray (stopped), pulsing accent while a run is actually executing — so you can see the state of everything at a glance without opening the page. This list shows on every page under this tab, the same way the Configuration tab's own sidebar always does.
A route rule is a scheduled Study Root C-FIND (interval in minutes, or specific times of day with optional days-of-week) against a configured PACS, filtered by modality / study date scope / query level. The scheduler runs due rules automatically and records what's new since the rule last ran.
A rule with no destination nodes just tracks new studies (find-only). Add one or more destination nodes and the scheduler additionally C-MOVEs each new match to this workstation's local Storage SCP (same Accept C-STORE setting Dicomtag Analytics' SR retrieve uses) and C-STOREs it on to every destination, using the retrieved object's own SOP Class — no need to pre-register storage SOP classes per rule. A study is only marked "seen" once it's fully handled, so a failed retrieve or forward is retried automatically on the next run rather than silently dropped.
Each rule can be Started, Paused, or Stopped independently of its own schedule (click the rule in the sidebar, or open it from the route rules page, to reach these):
- Start runs the rule immediately and computes a fresh next scheduled time — whether it was paused or stopped before.
- Pause and Stop both stop the schedule from firing again, and both interrupt a run that's already in progress — but cooperatively, between studies rather than mid-association, so nothing already retrieved/forwarded is ever redone or lost. They differ only in what's left behind: Pause keeps the rule's previously computed next-run time around as a record, Stop clears it.
- Run now executes a rule immediately without touching its status or schedule at all — useful for testing a paused or stopped rule.
Manual runs (Run now, and the run Start triggers) execute in a background thread rather than blocking the browser request, which is what makes Pause/Stop clickable while a batch is still going and lets the sidebar show a live "Running" dot. Each rule's own page shows a run history with every match and its status (found / retrieved / forwarded / failed, with the error if any) and whether the run itself completed or was interrupted.
Study-level C-FIND on Patient ID / Accession Number / Study Date (required) / Modality, same fields and rules as Dicomtag Analytics and Dicom Anonymizer. Pick studies from the result table and a level — Study, Series, or Image C-MOVEs the matching instances to this workstation's local Storage SCP.
Each retrieved instance's pixel data (PyDicom + numpy) has one operator-configured rectangle — X, Y, Width, Height in pixels, top-left origin, width/height 0 meaning "to the edge" — blacked out to zero, for redacting burned-in patient info or device overlays that live in the image itself rather than in DICOM tags. The same rectangle is applied to every retrieved instance, so setting it once covers every image in the checked study/series. It works from the pixel geometry the dataset's own tags describe (NumberOfFrames, SamplesPerPixel), not the array shape alone, so a multi-frame grayscale cine and a single-frame color image are never confused with each other. Compressed Transfer Syntaxes (JPEG Baseline/Lossless, RLE, …) are decompressed first; that needs pylibjpeg or gdcm installed, same as any other pydicom pixel_array read of those syntaxes. Burned-In Annotation (0028,0301) is set to NO on every cleaned instance. Dicom Cleaner only touches pixel data and that one tag — it does not scrub PatientName/PatientID/other tags the way Dicom Anonymizer's modes do; combine the two tools if a study needs both.
Typing coordinates blind is optional — Preview & pick region retrieves one image from the first checked study and renders it (a plain min-max stretch, not a diagnostic display) so you can drag a rectangle on the actual image instead; the drag is translated back to full-resolution pixel coordinates and fills in the X/Y/Width/Height fields above.
The cleaned instances are then C-STOREd back out, by default to the same PACS the study was retrieved from — override this with the Send to destination picker if it should go somewhere else instead. Send back as chooses between a fresh SOP Instance UID (default; the cleaned image lands as a new object next to the original, which stays untouched on the PACS) or the original SOP Instance UID (the PACS is expected to overwrite the existing instance — only do this against a PACS that actually replaces on a duplicate SOP Instance UID C-STORE, rather than rejecting or duplicating it).
Import PDF files, a ZIP of PDFs, a browser folder, or a directory path on this workstation. … opens a native folder dialog on this machine; Scan lists PDF files only. Each PDF is wrapped as Encapsulated PDF Storage (modality DOC). Checking Generate Patient Name / ID fills those fields. Unique patient per PDF derives identities from file names so a directory of reports does not stack as one person. Store on PACS C-STOREs those instances to the selected remote. Uncheck it to encapsulate only. The peer must accept Encapsulated PDF Storage — C-ECHO or the Secondary Capture test image is a different SOP Class.
Caps: 25 MB per PDF, 40 files, 40 MB ZIP. Only .pdf files are imported. ZIP entries with .., __MACOSX, or a non-PDF extension are skipped. The directory path is read by this process; the UI has no login.
/worklist is the table view for Modality Worklist C-FIND (1.2.840.10008.5.1.4.31). It is not Study Root C-FIND.
Query worklist
- Source: local web worklist, or a remote node
- Present as: workstation or a virtual AE
- Filters: patient name (
DOE*style), patient ID, accession, modality, station AE, scheduled date
Empty filters on a remote mean “return what the SCP is willing to send”. Presenting as CT1 fills station (and modality, if the identity has one) unless you override the fields.
Local web worklist
Add scheduled procedures here. If Serve the web worklist over DICOM is on, a modality can C-FIND this workstation on the listen port (11112 by default). The local MWL SCP also answers C-ECHO. It uses the workstation AE Title, not virtual identities.
/tools/hl7-send sends an HL7 v2 message to a TCP endpoint. It is a sender, not an analyzer: paste (or save) the pipe-delimited text and ship it.
- Host / port — the HL7 listener, not the DICOM port. On Philips Vue, IS Link Configuration → Listeners shows Port Number (often
10010) and Host IP. That Listeners page is settings, not the inbound queue. Send to that Host IP:Port. Control Port (often2112) is not MLLP. Encoding Cp1252 matches Latin-1 for ASCII HL7. - Framing — MLLP (
0x0B…0x1C 0x0D) is the default. Raw TCP is there for engines that do not wrap. - Message — HL7 v2 starting with
MSH. The editor shows one segment per row (toggle Raw for the full paste). Long pipe-delimited lines wrap. Newlines become CR on the wire. - ACK — if the peer replies, the result shows the raw ACK, MSA-1 (
AA/AE/AR), and ACK MSH-3 (who answered). An ACK is not a promise that PACS applied an order update. - Advanced troubleshooting — collapsed by default. Optional stamps applied to the paste before send (they do not invent missing ORC/OBR segments):
- New MSH-10 — on by default in the UI. The same Message Control ID is often ACKed and ignored.
- Change existing order — on by default. Stamps ORC-1 (see the control next to it) and ORC-9. Philips Vue / IS Link uses SC to update an order (
NWnew,CAcancel). XO is generic HL7 change; Vue often ignores it while Mirth still ACKs. - ORC-1 on change — UI default is SC (Vue). JSON API still stamps
XOunless you pass"orc_control": "SC". - OBR-31 as CE text — on by default in the UI. Reason for Study is
id^text. A spaced identifier becomesfirstword^full text.^text(empty id) is filled from the text. Vue maps OBR-31 to DICOM(0040,2010), which may not be the study description you see in the Vue UI (that is often OBR-4.2 / C-STORE). - Set OBR-25 to SC (in progress) — on by default, test only. That is result status, not Vue’s ORC-1 SC. Also sets ORC-5 to
IP.
- The Send result repeats MSH-10, MSH-5/MSH-6, ORC-1, OBR-25, and OBR-31 as they went on the wire, plus a Hint when the ACK is likely not a PACS update.
- Saved drafts live in
hl7_messages.jsonnext to config.
The ACK you see is from whoever answers on Host/Port. IS Link Configuration → Listeners is the bind settings (Port Number, Host IP, Encoding, Control Port). It is not the message queue.
If you cannot open Mirth, use IS Link. Stop editing OBR-31 until a message shows under Queues & Notifications.
- On Listeners, note Port Number (often 10010) and click Edit Listener for Host IP. Send Dicommunication to that Host IP:Port. Do not send to Control Port (often 2112).
- After Add/Edit Listener, IS Link reminds you to start the Listener process. Config on 10010 does nothing if the process is stopped.
- Look at Queues & Notifications, not the Listeners form:
- Message appears — routing worked. Then ORC-1 SC, accession, OBR-31 can matter.
- Still empty, connection refused / timeout — wrong Host IP, Listener down, or this workstation cannot reach that VLAN.
- Still empty, but you got an ACK — ACK MSH-3 is who answered. If it is Mirth/IBE, Host is not this IS Link. MSH-5 / MSH-6 must match what IS Link accepts; Mirth often rewrites those. Encoding Cp1252 is fine for ASCII ORM.
- You do not need Mirth for that test. If you later get Mirth access: Message Browser received → transformed → sent to this IS Link Host IP:Port.
- Vue IS Link order control is NW (new), SC (update), CA (cancel). XO is not in that table.
- Match the accession Vue already has: ORC-3 / OBR-3 (filler / order number) and often OBR-18.
- Vue maps OBR-31 to DICOM Reason for Requested Procedure
(0040,2010). The text you stare at in Vue is often Study Description from the images (C-STORE) or OBR-4.2, not OBR-31. - Updating an existing study may also need Vue’s ZDS Study Instance UID. HL7-PACS Field Mapping in this same tree decides which ORM fields actually overwrite.
curl -s -X POST http://127.0.0.1:8080/api/tools/hl7-send/run \
-H 'Content-Type: application/json' \
-d '{"options":{"host":"10.0.0.20","port":2575,"message":"MSH|^~\\&|DICOMM|ARNPRO|RECVAPP|RECVFAC|20260101000000||ADT^A01|MSG00001|P|2.5\rPID|||ARNPRO-TEST||TEST^DICOMMUNICATE"}}'Typical Orthanc DICOM port is 4242. From Docker on a Mac, host is host.docker.internal.
- Add a remote: AE Title
ORTHANC(or whatever Orthanc uses), hosthost.docker.internalor the LAN IP, port4242. - In Orthanc
DicomModalities, allow this tool’s calling AEs — the workstation (DICOMM) and every virtual title you will impersonate (CT1, …). - Testbench: C-ECHO should pass if the association is accepted.
- C-STORE / Study Root C-FIND usually pass on a stock Orthanc Storage/Q/R setup.
- MWL C-FIND passes only if the Orthanc worklist plugin (or equivalent) is enabled and that SOP Class is offered. Otherwise the Testbench shows MWL rejected and a “not an MWL SCP” message. That is expected, not a bug in this tool.
If the association is rejected entirely, the calling AE is not in Orthanc’s allow-list, or host/port/called AE are wrong.
Same operations as the UI. GET /health returns {"status":"ok","version":"..."}. Interactive docs: http://127.0.0.1:8080/docs.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/config |
Full config |
| PUT | /api/config/local |
Replace local AE |
| GET/PUT | /api/logging |
Log level and rotation |
| GET | /api/logs |
Tail of the application log |
| GET/POST/PUT/DELETE | /api/remotes, /api/remotes/{id} |
Remote nodes |
| GET/POST/PUT/DELETE | /api/identities, /api/identities/{id} |
Virtual local AEs |
| GET | /api/tools |
Registered tools |
| POST | /api/tools/{tool_id}/run |
Run a tool |
| GET | /api/echo-board |
Last C-ECHO board snapshot |
| POST | /api/echo-board/run |
C-ECHO every remote |
| GET/POST/DELETE | /api/worklist, /api/worklist/{id} |
Local worklist items |
| POST | /api/worklist/query |
MWL query (local or remote) |
| GET/POST/DELETE | /api/hl7/messages, /api/hl7/messages/{id} |
Saved HL7 v2 drafts |
Tool ids: ping, c-echo, c-store, pdf-store, c-find, c-find-advanced, mwl-find, hl7-send. hl7-send does not need remote_id; pass options.host, options.port, and options.message. pdf-store takes options.patient_name, options.patient_id, plus options.pdfs (filename + content_b64), options.zip_b64, or options.directory, and options.send (default true). c-find-advanced takes options.level (STUDY, SERIES, IMAGE), options.values (DICOM keywords to match), and optional options.return_keys. At STUDY, StudyDate is required.
Run a tool as a virtual AE:
curl -s -X POST http://127.0.0.1:8080/api/tools/c-echo/run \
-H 'Content-Type: application/json' \
-d '{"remote_id":"REPLACE","identity_id":"REPLACE"}'Study Root C-FIND with filters:
curl -s -X POST http://127.0.0.1:8080/api/tools/c-find/run \
-H 'Content-Type: application/json' \
-d '{"remote_id":"REPLACE","options":{"patient_id":"1001","modality":"CT"}}'Dicomtag Analytics at Series (hierarchical — Study Instance UID required):
curl -s -X POST http://127.0.0.1:8080/api/tools/c-find-advanced/run \
-H 'Content-Type: application/json' \
-d '{"remote_id":"REPLACE","options":{"level":"SERIES","values":{"StudyInstanceUID":"1.2.840…","BodyPartExamined":"CHEST"}}}'Encapsulate a PDF from a directory without sending:
curl -s -X POST http://127.0.0.1:8080/api/tools/pdf-store/run \
-H 'Content-Type: application/json' \
-d '{"options":{"patient_name":"DOE^JANE","patient_id":"1001","directory":"/data/reports","send":false}}'Worklist query as CT1:
curl -s -X POST http://127.0.0.1:8080/api/worklist/query \
-H 'Content-Type: application/json' \
-d '{"source":"REMOTE_ID","identity_id":"IDENTITY_ID"}'source is "local" or a remote id. If identity_id is set, calling AE and default station/modality come from that identity.
Create app/tools/your_tool.py:
from app.models import LocalAE, RemoteNode, ToolResult, ToolStep
from app.tools.base import BaseTool
from app.tools.registry import register
class CMoveTool(BaseTool):
id = "c-move"
name = "C-MOVE"
description = "Request a Query/Retrieve move from the remote AE."
category = "dimse"
def run(self, local: LocalAE, remote: RemoteNode | None, options=None) -> ToolResult:
return ToolResult(
tool_id=self.id,
tool_name=self.name,
ok=False,
summary="Not implemented yet",
)
register(CMoveTool())Rebuild or restart. The tool appears in the sidebar, at /tools/c-move, and at POST /api/tools/c-move/run. local is already the selected identity (workstation or virtual AE). options is the optional JSON object from the API (or Testbench form fields for built-in FIND tools).
pip install -r requirements-dev.txt
make testC-ECHO, C-STORE, PDF to DICOM, Study Root C-FIND (including Dicomtag Analytics at Series/Image), and MWL tests start in-process SCPs. PING uses loopback ICMP and a local TCP listener. HL7 send uses a loopback MLLP listener. Identity tests check that a virtual AE is the calling AE Title and the worklist station filter.
Full threat model, what has no protection by design, and how patient data is stored on disk: SECURITY.md.
This is a trusted-network admin tool. The web UI has no login, so Compose publishes it on 127.0.0.1 only; DICOMM_HTTP_BIND=0.0.0.0 opens it up and should only be used behind an authenticating reverse proxy. Do not publish port 8080 to the internet without one. Do not point it at production archives unless you intend to send the test C-STORE instance (ARNPRO^TESTBENCH) or Encapsulated PDF documents you import. DICOM and HL7 are sent in the clear unless you terminate TLS elsewhere. HL7 send transmits whatever you paste. PDF to DICOM reads uploaded files, ZIP contents, and any workstation path you type.
The data directory is not encrypted, and results.json keeps the last 200 tool results — including the body of any HL7 message you sent and the worklist rows a C-FIND returned. Against real systems that means patient identifiers on disk in cleartext. See SECURITY.md.
| Symptom | Likely cause |
|---|---|
| Association rejected / aborted | Called AE, host, or port wrong; calling AE not in the peer’s allow-list |
| C-ECHO works, C-STORE fails | Peer is not a Storage SCP for Secondary Capture (or that SOP Class is disabled) |
| C-ECHO / C-STORE work, PDF to DICOM fails | Peer does not accept Encapsulated PDF Storage. Enable that SOP Class; Secondary Capture is a different test. |
| C-ECHO works, Study Root C-FIND fails | Peer is not a Q/R SCP. This is not MWL. |
| Dicomtag Analytics Series/Image stays locked or fails | Hierarchical FIND. Fill Study Instance UID for Series; Study and Series Instance UID for Image. Empty Study UID cannot search body part archive-wide. |
| List SR reports returns zero series | Those studies have no series with modality SR, or Vue omitted Modality on Series C-FIND. |
| Retrieve report text returns one report from a long list | Vue accepted one C-MOVE per association. Retrieve now opens a new association per report. The buttons show the row count; the summary is Retrieved N of M. Clicking a result row does not limit retrieve to that exam. |
| Retrieve report text: Move Destination unknown | Vue does not list this Local AE Title as a C-MOVE destination. Enable Accept C-STORE on Local DICOM AE and register that AE at this host:port. |
| C-ECHO / C-STORE / Q/R work, MWL fails | Peer does not offer Modality Worklist FIND. Enable the worklist plugin (Orthanc) or query a RIS. |
| Worklist and MWL C-FIND look the same | They are the same SOP Class. Use Testbench Study Root C-FIND to search stored studies. |
| Empty MWL / C-FIND table but Pass | The SOP Class was accepted and the query succeeded with zero matches. Check station AE, date, and Present as. |
| HL7 send times out / no ACK | Peer is not listening, or that port is DICOM not MLLP. HL7 engines are often 2575 or 6661, not 104/4242. |
| HL7 ACK AA to 10010 but Vue IS Link “empty” | Listeners is settings, not the queue. Look at Queues & Notifications. Send to Listener Host IP:10010, not Control Port 2112. Confirm the Listener process is started. ACK MSH-3 is who answered. |
| HL7 ACK AA, IS Link queued, Vue UI unchanged | Vue updates with ORC-1 SC, not XO. Accession must match ORC-3/OBR-3 (and often OBR-18). OBR-31 is (0040,2010); the Vue UI may show Study Description from C-STORE instead. |
| PING ICMP fails, TCP succeeds | Normal on locked-down clinical networks. Trust TCP to the DICOM port. |
docker compose --build fails at apt-get with exit 100 |
Debian mirrors were unreachable from the Docker builder. Current images copy a static ping and do not run apt. Pull/rebuild from this change. |
| Cannot reach Orthanc from Docker | Use host.docker.internal (same Mac/host) or the LAN IP; publish/check 4242. |
| Config vanished after image rebuild | Config should be in ~/.dicommunication (Windows MSI: %LOCALAPPDATA%\dicommunication). Legacy ./data is only used until the home folder exists. |
| Windows SmartScreen blocks the MSI | Unsigned first builds are expected. Use an Authenticode certificate for hospital rollout, or IT can allow the publisher. |
| MSI UI opens then nothing listens | Close the Dicommunication window only when you are done. The old black console is no longer the server. |
| macOS Gatekeeper blocks the app | Unsigned first builds are expected. Right-click Open, or run xattr -dr com.apple.quarantine /Applications/Dicommunication.app. |
| Mac Dock icon is live, no window | Quit from the Dock, then install a DMG built after the argv-emulation fix. Check ~/.dicommunication/launch.log. Rebuild: Actions → macOS DMG with release_tag=v0.2.0. |
| UI opens in Safari/Chrome instead of an app window | Frozen builds should use a native window. Pass --window, or check WebView2 on Windows. --browser forces the system browser. |
| Modality cannot C-FIND this tool | Enable the MWL SCP, publish/allow 11112 (Windows: inbound TCP for dicommunication.exe; macOS: allow incoming for Dicommunication), and put this workstation AE on the modality’s worklist node list. |
| UI loads on the Docker host but not from another machine | Expected. Compose publishes 8080 on 127.0.0.1 because there is no login. docker compose logs names the address it published on and how to change it. Start with DICOMM_HTTP_BIND=0.0.0.0 and front it with an authenticating reverse proxy. |


