Dimse
██████╗ ██╗███╗ ███╗███████╗███████╗
██╔══██╗██║████╗ ████║██╔════╝██╔════╝
██║ ██║██║██╔████╔██║███████╗█████╗
██║ ██║██║██║╚██╔╝██║╚════██║██╔══╝
██████╔╝██║██║ ╚═╝ ██║███████║███████╗
╚═════╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝
DICOM networking for Elixir · PS3.7 + PS3.8Pure Elixir DICOM DIMSE networking library for the BEAM.
Implements the DICOM Upper Layer Protocol (PS3.8), DIMSE-C (PS3.7 Ch.9), and DIMSE-N (PS3.7 Ch.10) for building SCP (server) and SCU (client) applications. 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 §9.3)
- Association state machine -- GenServer-per-association with ARTIM timer (PS3.8 §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 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
- TLS / DICOM Secure Transport -- PS3.15 Annex B, mutual TLS via OTP
:ssl+ Ranch SSL - Extended Negotiation (PS3.7 Annex D) -- Role Selection, SOP Class Extended/Common Extended, User Identity authentication
- Telemetry -- association lifecycle, PDU, and command metrics via
:telemetry - 3 runtime deps --
dicom+ranch+telemetry
Installation
Add dimse to your mix.exs dependencies:
def deps do
[
{:dimse, "~> 0.7"}
]
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)
{:ok, assoc} = Dimse.connect("192.168.1.10", 11112,
calling_ae: "MY_SCU",
called_ae: "REMOTE_SCP"
)
:ok = Dimse.echo(assoc)
:ok = Dimse.release(assoc)C-STORE SCU (Client)
{: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"]
)
:ok = Dimse.store(assoc, sop_class_uid, sop_instance_uid, data_set)
:ok = Dimse.release(assoc)C-FIND SCU (Client)
{: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"]
)
{:ok, results} = Dimse.find(assoc, :study, query_data)
:ok = Dimse.release(assoc)C-MOVE SCU (Client)
{: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"]
)
# SCP pushes instances 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)
{: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)
]
)
{: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]
)
{:ok, 0x0000, _reply} = Dimse.n_action(assoc, commitment_uid, instance_uid, 1, action_data)
{:ok, 0x0000, attrs} = Dimse.n_get(assoc, sop_class_uid, sop_instance_uid)
{:ok, 0x0000, updated} = Dimse.n_set(assoc, sop_class_uid, sop_instance_uid, modifications)
{:ok, 0x0000, created_uid, created} = Dimse.n_create(assoc, sop_class_uid, attributes)
{:ok, 0x0000, nil} = Dimse.n_delete(assoc, sop_class_uid, sop_instance_uid)
{: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)
# TLS SCP listener
{:ok, ref} = Dimse.start_listener(
port: 2762,
handler: MyApp.DicomHandler,
tls: [
certfile: "/path/to/server.pem",
keyfile: "/path/to/server_key.pem"
]
)
# TLS SCU connection
{: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
]
)
Mutual TLS: add cacertfile:, verify: :verify_peer, and fail_if_no_peer_cert: true on
the SCP side, and certfile: + keyfile: on the SCU side. All standard OTP :ssl options
are passed through.
DICOM Standard Coverage
All 11 DIMSE services, both SCP and SCU:
| Service | SCP | SCU | Description |
|---|---|---|---|
| C-ECHO | Yes | Yes | Verification |
| C-STORE | Yes | Yes | Store 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 |
The only unimplemented PS3.7 Annex D item is the Asynchronous Operations Window (0x53), which requires concurrent in-flight request support — a future milestone.
Testing
mix test
mix test --cover
mix format --check-formattedComparison
| dimse | DCMTK | dcm4che | pynetdicom | fo-dicom | wolfpacs | dicom-rs | |
|---|---|---|---|---|---|---|---|
| Language | Elixir | C++ | Java | Python | C#/.NET | Erlang | Rust |
| DIMSE-C | 5/5 | 5/5 | 5/5 | 5/5 | 5/5 | 2/5 | 2/5 |
| DIMSE-N | 6/6 | 6/6 | 6/6 | 6/6 | 6/6 | 0/6 | 0/6 |
| SCP + SCU | Both | Both | Both | Both | Both | SCP only | SCU only |
| TLS | Yes | Yes | Yes | Yes | Yes | No | No |
| Extended negotiation | Yes | Yes | Yes | Yes | Yes | No | No |
| License | MIT | BSD-3 | MPL-1.1 | MIT | MS-PL | AGPL-3.0 | MIT/Apache |
dimse pairs with dicom for a complete pure-Elixir DICOM toolkit:
dicom handles P10 files and data sets (PS3.5, PS3.6, PS3.10, PS3.18);
dimse handles DIMSE networking (PS3.7, PS3.8).
Contributing
Contributions are welcome. Please read our Contributing Guide and Code of Conduct before opening a PR.
License
MIT -- see LICENSE for details.