# Dimse
[](https://hex.pm/packages/dimse)
[](https://hexdocs.pm/dimse)
[](https://github.com/Balneario-de-Cofrentes/dimse/actions)
[](https://opensource.org/licenses/MIT)
```
██████╗ ██╗███╗ ███╗███████╗███████╗
██╔══██╗██║████╗ ████║██╔════╝██╔════╝
██║ ██║██║██╔████╔██║███████╗█████╗
██║ ██║██║██║╚██╔╝██║╚════██║██╔══╝
██████╔╝██║██║ ╚═╝ ██║███████║███████╗
╚═════╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝
DICOM networking for Elixir · PS3.7 + PS3.8
```
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 ([PS3.7 Ch.9](https://dicom.nema.org/medical/dicom/current/output/html/part07.html#chapter_9)),
and DIMSE-N ([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. 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.Handler` callbacks 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:
```elixir
def deps do
[
{:dimse, "~> 0.7"}
]
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
{: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)
```elixir
{: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)
```elixir
{: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)
```elixir
{: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)
```elixir
{: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)
```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]
)
{: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)
```elixir
# 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
```bash
mix test
mix test --cover
mix format --check-formatted
```
## Comparison
| | 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) | [wolfpacs](https://github.com/wolfpacs/wolfpacs) | [dicom-rs](https://github.com/Enet4/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`](https://hex.pm/packages/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](CONTRIBUTING.md)
and [Code of Conduct](CODE_OF_CONDUCT.md) before opening a PR.
## License
MIT -- see [LICENSE](LICENSE) for details.