Dimse
Pure Elixir DICOM DIMSE networking library for the BEAM.
Implements the DICOM Upper Layer Protocol (PS3.8), DIMSE-C message services (PS3.7 Ch.9), and DIMSE-N notification/management services (PS3.7 Ch.10) for building SCP (server) and SCU (client) applications.
Built on Elixir's binary pattern matching for fast, correct PDU parsing, with one GenServer per association for fault isolation and natural backpressure.
Features
- Upper Layer Protocol -- full PDU encode/decode for all 7 PDU types (PS3.8 Section 9.3)
- Association state machine -- GenServer-per-association with ARTIM timer (PS3.8 Section 9.2)
- DIMSE-C services -- C-ECHO, C-STORE, C-FIND, C-MOVE, C-GET
- DIMSE-N services -- N-EVENT-REPORT, N-GET, N-SET, N-ACTION, N-CREATE, N-DELETE
- SCP behaviour --
Dimse.Handlercallbacks for implementing DICOM servers (all 11 services) - SCU client API -- connect, echo, store, find, move, get, cancel, n_get, n_set, n_action, n_create, n_delete, n_event_report, release, abort
- Presentation context negotiation -- abstract syntax + transfer syntax matching
- Max PDU length negotiation -- with automatic message fragmentation
- Telemetry --
:telemetry-based events for association lifecycle, PDU, and command metrics - Built-in C-ECHO -- SCP and SCU implementations for verification (DICOM "ping")
- TLS / DICOM Secure Transport -- PS3.15 Annex B via OTP
:ssl+ Ranch SSL, mutual TLS support - 3 runtime deps --
dicom+ranch+telemetry(:ssl/:public_keyare OTP stdlib)
Installation
Add dimse to your mix.exs dependencies:
def deps do
[
{:dimse, "~> 0.6.0"}
]
endQuick Start
C-ECHO SCP (Server)
defmodule MyApp.DicomHandler do
@behaviour Dimse.Handler
@impl true
def supported_abstract_syntaxes do
[
"1.2.840.10008.1.1",
"1.2.840.10008.5.1.4.1.1.2",
"1.2.840.10008.5.1.4.1.2.2.1",
"1.2.840.10008.5.1.4.1.2.2.2",
"1.2.840.10008.5.1.4.1.2.2.3"
]
end
@impl true
def handle_echo(_command, _state), do: {:ok, 0x0000}
@impl true
def handle_store(_command, data_set, _state) do
# Persist the DICOM instance...
{:ok, 0x0000}
end
@impl true
def handle_find(_command, _query, _state), do: {:ok, []}
@impl true
def handle_move(_command, _query, _state), do: {:ok, []}
@impl true
def handle_get(_command, _query, _state), do: {:ok, []}
# Optional: resolve C-MOVE destination AE titles
def resolve_ae("DEST_SCP"), do: {:ok, {"192.168.1.20", 11112}}
def resolve_ae(_), do: {:error, :unknown_ae}
end
# Start the listener
{:ok, _ref} = Dimse.start_listener(
port: 11112,
handler: MyApp.DicomHandler,
ae_title: "MY_SCP",
max_associations: 200
)C-ECHO SCU (Client)
# Open an association
{:ok, assoc} = Dimse.connect("192.168.1.10", 11112,
calling_ae: "MY_SCU",
called_ae: "REMOTE_SCP"
)
# Verify connectivity (DICOM "ping")
:ok = Dimse.echo(assoc)
# Release the association
:ok = Dimse.release(assoc)C-STORE SCU (Client)
# Open an association proposing CT Image Storage
{:ok, assoc} = Dimse.connect("192.168.1.10", 11112,
calling_ae: "MY_SCU",
called_ae: "REMOTE_SCP",
abstract_syntaxes: ["1.2.840.10008.5.1.4.1.1.2"]
)
# Store a DICOM instance
:ok = Dimse.store(assoc, sop_class_uid, sop_instance_uid, data_set)
# Release the association
:ok = Dimse.release(assoc)C-FIND SCU (Client)
# Open an association proposing Study Root Query/Retrieve
{:ok, assoc} = Dimse.connect("192.168.1.10", 11112,
calling_ae: "MY_SCU",
called_ae: "REMOTE_SCP",
abstract_syntaxes: ["1.2.840.10008.5.1.4.1.2.2.1"]
)
# Query for matching studies (query_data is an encoded DICOM identifier)
{:ok, results} = Dimse.find(assoc, :study, query_data)
# Or use the SOP Class UID directly
{:ok, results} = Dimse.find(assoc, "1.2.840.10008.5.1.4.1.2.2.1", query_data)
# Release the association
:ok = Dimse.release(assoc)C-MOVE SCU (Client)
# Open an association proposing Study Root Query/Retrieve MOVE
{:ok, assoc} = Dimse.connect("192.168.1.10", 11112,
calling_ae: "MY_SCU",
called_ae: "REMOTE_SCP",
abstract_syntaxes: ["1.2.840.10008.5.1.4.1.2.2.2"]
)
# Retrieve matching studies — SCP pushes to DEST_SCP via C-STORE sub-ops
{:ok, result} = Dimse.move(assoc, :study, query_data, dest_ae: "DEST_SCP")
# result.completed, result.failed, result.warning
:ok = Dimse.release(assoc)C-GET SCU (Client)
# Open an association proposing Study Root GET + storage SOP classes to receive
{:ok, assoc} = Dimse.connect("192.168.1.10", 11112,
calling_ae: "MY_SCU",
called_ae: "REMOTE_SCP",
abstract_syntaxes: [
"1.2.840.10008.5.1.4.1.2.2.3", # Study Root GET
"1.2.840.10008.5.1.4.1.1.2" # CT Image Storage (to receive)
]
)
# Retrieve matching instances on the same association
{:ok, data_sets} = Dimse.get(assoc, :study, query_data)
:ok = Dimse.release(assoc)DIMSE-N SCU (Client)
# Storage Commitment example (PS3.4 Annex J)
commitment_uid = "1.2.840.10008.1.20.1"
{:ok, assoc} = Dimse.connect("192.168.1.10", 11112,
calling_ae: "MY_SCU",
called_ae: "REMOTE_SCP",
abstract_syntaxes: [commitment_uid]
)
# Request storage commitment (N-ACTION)
{:ok, 0x0000, _reply} = Dimse.n_action(assoc, commitment_uid, instance_uid, 1, action_data)
# Retrieve attributes (N-GET)
{:ok, 0x0000, attrs} = Dimse.n_get(assoc, sop_class_uid, sop_instance_uid)
# Modify attributes (N-SET)
{:ok, 0x0000, updated} = Dimse.n_set(assoc, sop_class_uid, sop_instance_uid, modifications)
# Create a managed instance (N-CREATE)
{:ok, 0x0000, created_sop_instance_uid, created} =
Dimse.n_create(assoc, sop_class_uid, attributes)
# Delete a managed instance (N-DELETE)
{:ok, 0x0000, nil} = Dimse.n_delete(assoc, sop_class_uid, sop_instance_uid)
# Send an event notification (N-EVENT-REPORT)
{:ok, 0x0000, _data} = Dimse.n_event_report(assoc, sop_class_uid, sop_instance_uid, 1, event_data)
:ok = Dimse.release(assoc)TLS / DICOM Secure Transport (PS3.15 Annex B)
# Start a TLS-enabled SCP listener
{:ok, ref} = Dimse.start_listener(
port: 2762,
handler: MyApp.DicomHandler,
tls: [
certfile: "/path/to/server.pem",
keyfile: "/path/to/server_key.pem"
]
)
# Connect an SCU over TLS
{:ok, assoc} = Dimse.connect("192.168.1.10", 2762,
calling_ae: "MY_SCU",
called_ae: "REMOTE_SCP",
abstract_syntaxes: ["1.2.840.10008.1.1"],
tls: [
cacertfile: "/path/to/ca.pem",
verify: :verify_peer
]
)
:ok = Dimse.echo(assoc)
:ok = Dimse.release(assoc)Mutual TLS (client certificate verification):
# SCP requires client certificate
{:ok, ref} = Dimse.start_listener(
port: 2762,
handler: MyApp.DicomHandler,
tls: [
certfile: "/path/to/server.pem",
keyfile: "/path/to/server_key.pem",
cacertfile: "/path/to/ca.pem",
verify: :verify_peer,
fail_if_no_peer_cert: true
]
)
# SCU provides client certificate
{:ok, assoc} = Dimse.connect("192.168.1.10", 2762,
calling_ae: "MY_SCU",
called_ae: "REMOTE_SCP",
tls: [
cacertfile: "/path/to/ca.pem",
certfile: "/path/to/client.pem",
keyfile: "/path/to/client_key.pem",
verify: :verify_peer
]
)
All standard OTP :ssl options are passed through — no opinionated wrappers.
Architecture
lib/dimse/
dimse.ex -- Public API facade
pdu.ex -- 7 PDU type structs + sub-items
pdu/
decoder.ex -- Binary → struct (PS3.8 §9.3)
encoder.ex -- Struct → iodata
association.ex -- GenServer: Upper Layer state machine
association/
state.ex -- Association state struct
negotiation.ex -- Presentation context matching
config.ex -- Timeouts, AE titles, max PDU
command.ex -- Command set encode/decode (group 0000)
command/
fields.ex -- Command field constants (PS3.7 §E)
status.ex -- DIMSE status codes (PS3.7 §C)
message.ex -- DIMSE message assembly from P-DATA fragments
listener.ex -- Ranch listener lifecycle
connection_handler.ex -- Ranch protocol → Association
handler.ex -- SCP behaviour
scp/echo.ex -- Built-in C-ECHO SCP
scu.ex -- SCU client API
scu/echo.ex -- C-ECHO SCU
scu/store.ex -- C-STORE SCU
scu/find.ex -- C-FIND SCU
scu/move.ex -- C-MOVE SCU
scu/get.ex -- C-GET SCU
scu/n_get.ex -- N-GET SCU
scu/n_set.ex -- N-SET SCU
scu/n_action.ex -- N-ACTION SCU
scu/n_create.ex -- N-CREATE SCU
scu/n_delete.ex -- N-DELETE SCU
scu/n_event_report.ex -- N-EVENT-REPORT SCU
telemetry.ex -- Event definitionsDICOM Standard Coverage
Services -- 11/11
All DIMSE-C (PS3.7 Ch.9) and DIMSE-N (PS3.7 Ch.10) services, both SCP and SCU:
| Service | SCP | SCU | Description |
|---|---|---|---|
| C-ECHO | Yes | Yes | Verification (connectivity test) |
| C-STORE | Yes | Yes | Store DICOM instances |
| C-FIND | Yes | Yes | Query patient/study/series/instance |
| C-MOVE | Yes | Yes | Retrieve via C-STORE sub-ops to destination AE |
| C-GET | Yes | Yes | Retrieve via C-STORE sub-ops on same association |
| N-EVENT-REPORT | Yes | Yes | Event notification |
| N-GET | Yes | Yes | Retrieve managed SOP Instance attributes |
| N-SET | Yes | Yes | Modify managed SOP Instance attributes |
| N-ACTION | Yes | Yes | Request action on managed SOP Instance |
| N-CREATE | Yes | Yes | Create managed SOP Instance |
| N-DELETE | Yes | Yes | Delete managed SOP Instance |
Upper Layer Protocol (PS3.8)
| Component | Status |
|---|---|
| All 7 PDU types | Complete |
| Association state machine (5-phase + ARTIM) | Complete |
| Presentation context negotiation | Complete |
| Max PDU length negotiation + fragmentation | Complete |
| Implementation Class UID / Version Name | Complete |
| Command set encoding (Implicit VR LE, PS3.7 6.3.1) | Complete |
| Status code handling (success/warning/failure/cancel/pending) | Complete |
| Sub-operation tracking (C-MOVE/C-GET remaining/completed/failed/warning) | Complete |
| C-CANCEL support | Complete |
Not Yet Implemented
| Feature | Priority | Notes |
|---|---|---|
| SOP Class Extended Negotiation (0x56) | Medium | Role selection for SOP classes |
| SOP Class Common Extended Negotiation (0x57) | Medium | Service class-wide negotiation |
| User Identity Negotiation (0x58/0x59) | Medium | Username/password/Kerberos |
| Asynchronous Operations Window (0x53) | Low | Multi-message pipelining |
Testing
206 tests + 10 property-based tests, 0 failures.
mix test # Run all tests
mix test --cover # Run with coverage report
mix format --check-formattedProperty-based tests using StreamData verify PDU encode/decode roundtrips. Integration tests verify all 11 DIMSE services end-to-end over TCP and TLS.
Competitive Analysis
dimse is one of only 5 libraries in any language that implements all 11 DIMSE
services with both SCP and SCU roles. The others are DCMTK (C++), dcm4che (Java),
pynetdicom (Python), and fo-dicom (C#/.NET).
Cross-Language Comparison
| Feature | dimse | DCMTK | dcm4che | pynetdicom | fo-dicom | dicom-rs |
|---|---|---|---|---|---|---|
| Language | Elixir | C++ | Java | Python | C#/.NET | Rust |
| License | MIT | BSD-3 | MPL-1.1 | MIT | MS-PL | MIT/Apache |
| DIMSE-C services | 5/5 | 5/5 | 5/5 | 5/5 | 5/5 | 2/5 |
| DIMSE-N services | 6/6 | 6/6 | 6/6 | 6/6 | 6/6 | 0/6 |
| SCP + SCU | Both | Both | Both | Both | Both | SCU only |
| TLS | Yes | Yes | Yes | Yes | Yes | No |
| Extended negotiation | No | Yes | Yes | Yes | Yes | No |
| Async ops window | No | Yes | Partial | Negotiation only | Yes | No |
| Telemetry | :telemetry | Logging | Logging | Events | Events | -- |
| Concurrency model | GenServer/OTP | Threads | Threads | Threads | async/await | -- |
| Runtime deps | 3 | Many | JDK | pydicom | .NET | Minimal |
BEAM Ecosystem
| Feature | dimse | wolfpacs | dicom.ex |
|---|---|---|---|
| Language | Elixir | Erlang | Elixir |
| License | MIT | AGPL-3.0 | Apache-2.0 |
| DIMSE-C services | 5/5 (SCP+SCU) | 2/5 (C-ECHO, C-STORE) | 2.5/5 (SCP only) |
| DIMSE-N services | 6/6 (SCP+SCU) | 0/6 | 0/6 |
| All 7 PDU types | Yes | 6/7 | 6/7 |
| ARTIM timer | Yes | No | No |
| SCU client API | Full | Partial | None |
| Tests | 216 (206 + 10 prop) | ~81 | ~25 |
| Status | Active | Sporadic | Inactive |
Ecosystem Positioning
dimse is the networking counterpart to dicom,
which handles DICOM P10 file parsing and writing. Together they provide a complete
pure-Elixir DICOM toolkit:
| Library | Scope | DICOM Parts |
|---|---|---|
dicom | P10 files, data sets, DICOM JSON | PS3.5, PS3.6, PS3.10, PS3.18 |
dimse | DIMSE networking, SCP/SCU | PS3.7, PS3.8 |
AI-Assisted Development
This project welcomes AI-assisted contributions. See AGENTS.md for instructions that AI coding assistants can use to work with this codebase, and CONTRIBUTING.md for our AI contribution policy.
Contributing
Contributions are welcome. Please read our Contributing Guide and Code of Conduct before opening a PR.
License
MIT -- see LICENSE for details.