# Dimse
[](https://hex.pm/packages/dimse)
[](https://hexdocs.pm/dimse)
[](https://github.com/Balneario-de-Cofrentes/dimse/actions)
[](https://opensource.org/licenses/MIT)
Pure Elixir DICOM DIMSE networking library for the BEAM.
Implements the DICOM Upper Layer Protocol ([PS3.8](https://dicom.nema.org/medical/dicom/current/output/html/part08.html)),
DIMSE-C message services ([PS3.7 Ch.9](https://dicom.nema.org/medical/dicom/current/output/html/part07.html#chapter_9)),
and DIMSE-N notification/management services ([PS3.7 Ch.10](https://dicom.nema.org/medical/dicom/current/output/html/part07.html#chapter_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.Handler` callbacks 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_key` are OTP stdlib)
## Installation
Add `dimse` to your `mix.exs` dependencies:
```elixir
def deps do
[
{:dimse, "~> 0.6.0"}
]
end
```
## Quick Start
### C-ECHO SCP (Server)
```elixir
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)
```elixir
# 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)
```elixir
# 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)
```elixir
# 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)
```elixir
# 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)
```elixir
# 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)
```elixir
# 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)
```elixir
# 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):
```elixir
# 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 definitions
```
## DICOM 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.
```bash
mix test # Run all tests
mix test --cover # Run with coverage report
mix format --check-formatted
```
Property-based tests using [StreamData](https://hex.pm/packages/stream_data)
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](https://github.com/DCMTK/dcmtk) | [dcm4che](https://github.com/dcm4che/dcm4che) | [pynetdicom](https://github.com/pydicom/pynetdicom) | [fo-dicom](https://github.com/fo-dicom/fo-dicom) | [dicom-rs](https://github.com/Enet4/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](https://github.com/wolfpacs/wolfpacs) | [dicom.ex](https://github.com/jjedele/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`](https://hex.pm/packages/dicom),
which handles DICOM P10 file parsing and writing. Together they provide a complete
pure-Elixir DICOM toolkit:
| Library | Scope | DICOM Parts |
|---------|-------|-------------|
| [`dicom`](https://hex.pm/packages/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](AGENTS.md)
for instructions that AI coding assistants can use to work with this codebase,
and [CONTRIBUTING.md](CONTRIBUTING.md) for our AI contribution policy.
## Contributing
Contributions are welcome. Please read our [Contributing Guide](CONTRIBUTING.md)
and [Code of Conduct](CODE_OF_CONDUCT.md) before opening a PR.
## License
MIT -- see [LICENSE](LICENSE) for details.