Skip to content

Plugin Reference

This reference is aimed at users who configure the routing rules of the DICOM Router. It describes the bundled plugins and their configuration — each with its use case, the configurable fields, and a complete YAML example. The general config syntax (filters, destinations, reload) is documented in configuration.md.

How plugins are activated

A plugin is not switched on globally; it is wired up per connection through two blocks in the configuration:

  • The actions: block defines instances. Each named action is a list of plugin entries; each entry names the plugin class (plugin:) and its parameters (config:).
  • A filter: rule references one or more actions by name. The actions only run when a connection matches the filter rule.

The same plugin can be defined multiple times with different config (multi-instance): once as tapAlles with a broad whitelist, once as tapSr for reports only. Which device gets which variant is decided by the filter rule. The order of actions in a filter rule is the execution order.

Details on filter fields (callingAET, calledAET, IP/port criteria, the regex: prefix), destinations, and hot reload via dicom-router-ctl reload are documented in configuration.md.

An oversized field never costs the connection. The router materializes element values only on access, and refuses to do so above router.materialization-threshold or beyond router.max-elements (see configuration.md). The rewrite plugins treat an element refused that way like a missing one: the affected change is skipped, the message is forwarded unchanged, and the association carries on. QrFieldRewriteStage is a step more careful and skips the operation addressing that field rather than treating the field as missing — otherwise a set would overwrite an element whose shape (a sequence, say) was never established. The other operations still apply. With the default values (1 MB) the case is practically unreachable — it becomes relevant when the thresholds are set considerably lower.

Careful, two package names: Most plugins live under ch.immeditech.imconnect.dicomrouter.plugin.…, whereas the example plugins PduLoggingStagePlugin and ExamplePluginStage live under ch.immeditech.imconnect.plugins.… — the safest approach is to copy the plugin: lines from the examples in this reference.


PduLoggingStage

What it does

Writes protocol captures of a connection — either as a summary to the application log or as binary PDU dumps to a file. Used for troubleshooting when a peer does not behave as expected and you need to trace the DICOM handshake. Payload/patient data never end up in the application log.

Plugin class

ch.immeditech.imconnect.plugins.PduLoggingStagePlugin

Important: capturing works entirely without this plugin

The capture function is built into every connection — setting logLevel in the filter rule is all it takes; an action is not required for it. This plugin merely adds additional, named logger instances (e.g. with a custom stageName for attribution in the log). The plugin instances are also governed by the filter rule's logLevel: with logLevel: 0, neither the built-in nor the additional loggers capture anything.

Controlled via the filter logLevel

What is actually captured is controlled by the logLevel field of the filter rule, not by the plugin config:

logLevel Effect
0 No logging
1 Inbound, as a summary to the application log
2 Outbound, as a summary to the application log
3 Inbound and outbound, as a summary to the application log
5 Inbound, as a binary PDU dump in router.log-folder
6 Outbound, as a binary PDU dump in router.log-folder
7 Inbound and outbound, as a binary PDU dump

Config fields

Field Required/Default Description
stageName Default PduLoggingStage Freely chosen name of the instance; appears in the log for attribution.
firstLogger Default true true = instance for the inbound side, false = for the outbound side.
enabled Default true Disables this instance without removing it from the config.

For complete inbound and outbound captures, you typically define two entries in the same action — one with firstLogger: true, one with false.

Example

actions:
  protocolLogging:
    - plugin: ch.immeditech.imconnect.plugins.PduLoggingStagePlugin
      config:
        stageName: "InboundProtocolLogger"
        firstLogger: true
        enabled: true
    - plugin: ch.immeditech.imconnect.plugins.PduLoggingStagePlugin
      config:
        stageName: "OutboundProtocolLogger"
        firstLogger: false
        enabled: true

filter:
  - callingAET: MRI1
    calledAET: PACS
    logLevel: 3                 # summary to the application log
    actions:
      - "protocolLogging"
    destination:
      - "pacs1"

Notes

  • Binary dumps (logLevel 5/6/7) end up in the router.log-folder directory. They may contain patient data and must be protected accordingly.
  • This plugin does not replace the DICOM tap: it serves protocol debugging, not the storage of DICOM files usable for clinical purposes.

AETitleRewriteStage

What it does

Rewrites the calling and/or called AE title with which the router establishes the connection to the destination PACS. Typical case: a device sends with an AE title that the PACS does not know or does not accept — the router replaces it outbound with an agreed value. On the way back, the router restores the original AE titles for the device, so the device never notices the rewrite.

Plugin class

ch.immeditech.imconnect.dicomrouter.plugin.AETitleRewriteStage

Config fields

Field Required/Default Description
callingAetOverride optional New outbound calling AET. Empty/omitted = the original value is preserved.
calledAetOverride optional New outbound called AET. Empty/omitted = the original value is preserved.
logRewrites Default true Logs every rewrite.
enabled Default true Disables the plugin without removing it from the config.

AE titles may be at most 16 characters long.

Example

actions:
  aetRewrite_ultrasound:
    - plugin: ch.immeditech.imconnect.dicomrouter.plugin.AETitleRewriteStage
      config:
        callingAetOverride: "US_GATEWAY"
        calledAetOverride: "PACS_PROD"
        logRewrites: true
        enabled: true

filter:
  - callingAET: US1
    calledAET: PACS
    actions:
      - "aetRewrite_ultrasound"
    destination:
      - "pacs1"

Notes

  • Setting only one of the two override fields is allowed — the other one then remains unchanged.
  • If C-MOVE or a worklist is additionally routed through the router, the plugins CMoveDestinationRewriteStage or WorklistAetRewriteStage are usually required, because otherwise the rewritten AE titles are not carried along in the message contents (see below).

WorklistAetRewriteStage

What it does

Rewrites the station AE title ScheduledStationAETitle in worklist queries (C-FIND MWL). Use case: the router rewrites the modality's AE title via AETitleRewriteStage, so the RIS only knows the modality under the RIS-side AET. Without this plugin, the modality queries with its own AET as the search key and gets an empty worklist; it also discards delivered entries because their station AET does not match its own. The plugin rewrites the AET in the query forward (modality → RIS) and back in the responses (RIS → modality), so the worklist responses carry the device's original AETs.

Plugin class

ch.immeditech.imconnect.dicomrouter.plugin.WorklistAetRewriteStage

Config fields

Field Required/Default Description
aetMapping Required Mapping modality AET → RIS AET for the query direction.
responseMapping optional Explicit reverse direction RIS AET → modality AET. Takes precedence over the automatic inversion of aetMapping.
logRewrites Default true Logs every rewrite.
enabled Default true Disables the plugin. With enabled: true, aetMapping must contain at least one entry.

Example

The complete setup combines both plugins in one action: the AETitleRewriteStage rewrites the connection's calling AET (the RIS sees US_GATEWAY as the sender), and the WorklistAetRewriteStage carries the same value through the message content (search key ScheduledStationAETitle outbound, responses back). The WorklistAetRewriteStage alone is sufficient if the RIS accepts the modality's original calling AET and only the station AET in the worklist entries differs.

actions:
  worklistRewrite:
    - plugin: ch.immeditech.imconnect.dicomrouter.plugin.AETitleRewriteStage
      config:
        callingAetOverride: "US_GATEWAY"   # connection level: outbound sender AET
    - plugin: ch.immeditech.imconnect.dicomrouter.plugin.WorklistAetRewriteStage
      config:
        aetMapping:              # message level: modality AET -> RIS AET
          US1: "US_GATEWAY"
        # responseMapping:       # optional; only needed if the reverse direction is ambiguous
        #   US_GATEWAY: "US1"
        logRewrites: true
        enabled: true

filter:
  - callingAET: US1
    calledAET: RIS
    actions:
      - "worklistRewrite"
    destination:
      - "ris1"

Notes

  • The reverse direction is normally derived automatically from aetMapping.
  • If multiple modality AETs map to the same RIS AET (pool), the inversion is ambiguous. In that case, specify responseMapping explicitly; otherwise the affected response values cannot be unambiguously written back and remain unchanged (warning when the configuration is loaded).
  • Non-worklist traffic and unmappable values are passed through unchanged; the plugin never discards messages.

QrFieldRewriteStage

What it does

General field rewriter for query traffic (C-FIND, C-MOVE, C-GET). It lets you set individual fields in queries and responses, prefill them with a default value, or remove them — for example to restrict a query to specific modalities, add a fixed retrieve AE to responses, or take a field out. Without further configuration the plugin applies to the Query/Retrieve models (Patient Root and Study Root); via sopClasses it can be extended to other query classes, e.g. worklist queries (MWL).

Plugin class

ch.immeditech.imconnect.dicomrouter.plugin.QrFieldRewriteStage

Config fields

Field Required/Default Description
applyTo Default [C-FIND, C-MOVE, C-GET] Which operations the plugin applies to.
sopClasses Default: the six Q/R models (Patient/Study Root) Whitelist of SOP class UIDs the query class is checked against. A custom list replaces the default — to keep the Q/R models and add e.g. MWL (1.2.840.10008.5.1.4.31), list both.
request optional Rules for inbound query identifiers. Contains set, default, remove.
response optional Rules for outbound (pending) response identifiers. Contains set, default, remove.
fail-open Default false true = a processing error only skips this rule instead of aborting the connection.
logRewrites Default true Logs a summary per data set (without values).
enabled Default true With enabled: true, at least one rule must be present under request: or response:.

Within request: and response: there are three operations:

  • set — overwrite a field or create it.
  • default — set a field only if it is not yet present.
  • remove — remove a field.

Field/path notation: Tags are specified as "GGGG,EEEE" (or 8 hex digits).

Some DICOM fields are sequences: instead of a simple value (text, number, UID) they contain a list of items, and each item is its own small data set with fields of its own. Example: the "Referenced Study Sequence" (0008,1110) contains one item per referenced study, and inside it are the actual fields (such as the study UID). You descend into such structures with /-separated paths: "<sequence-tag>/<item-index or *>/<tag>", arbitrarily deep; * stands for all existing items. Paths only descend into existing sequences and items — set/default can create simple fields, but no sequence structure and no items; a path into a non-existent sequence simply does nothing.

VR specification: Every DICOM field has a data type (VR, e.g. AE for AE titles, LO for short text, UI for UIDs). For all standard DICOM fields (the complete standard data dictionary, PS3.6) the router knows the type itself — then the short form suffices ("0008,0061": "CT\\MR" or "0008,0054": "ROUTER").

Only for private and exotic fields (those not in the standard dictionary, or without a unique type there) is the extended form with an explicit VR mandatory ({ value: "XYZ", vr: LO }) — if it is missing, the router rejects the configuration at startup or on reload with a clear error message; nothing can go wrong at runtime as a result. An extended form whose VR contradicts the standard dictionary is rejected as well. Fields with a binary data type (e.g. US, OW) can never be written via set/default — rewrite values are text; such entries are rejected at load time. Multiple values are separated with a backslash.

Example

actions:
  qrFieldRewrite:
    - plugin: ch.immeditech.imconnect.dicomrouter.plugin.QrFieldRewriteStage
      config:
        applyTo: ["C-FIND"]
        request:
          set:
            "0008,0061": "CT\\MR"          # ModalitiesInStudy (short form)
          remove:
            - "0010,1000"                  # OtherPatientIDs
        response:
          set:
            "0008,0054": "ROUTER"          # RetrieveAETitle (short form)
          default:
            "0008,0080": "KLINIK_A"        # InstitutionName (short form)
            "0009,1001":                   # private field - extended form with VR
              value: "ZUWEISER-A"
              vr: LO
          remove:
            - "0008,1120/*/0010,0020"      # in all items of the sequence
        fail-open: false
        logRewrites: true
        enabled: true

filter:
  - callingAET: WS1
    calledAET: PACS
    actions:
      - "qrFieldRewrite"
    destination:
      - "pacs1"

Notes

  • The configuration is strictly validated at load time: invalid paths or VR codes, missing VRs on unknown fields, empty values, and overlapping rules (such as set and default on the same field) are rejected at startup or reload — not only at runtime.
  • Traffic outside the sopClasses whitelist, unreadable messages, and missing fields are expected conditions and lead to unchanged pass-through, never to an abort.
  • ISO 2022 character sets: If a device declares an unsupported multi-valued character set (ISO 2022 code extensions, e.g. ISO 2022 IR 6\ISO 2022 IR 87), the router cannot safely decode text fields — the stage is skipped for that object (WARN in the log, the original is forwarded byte-exact). This applies to all standard text fields, including implicit-VR transfers.
  • If set sets the same value that is already present anyway, no change occurs.
  • If both QrFieldRewriteStage (with MWL in sopClasses) and WorklistAetRewriteStage act on worklist queries on the same connection, they run in the order the actions are listed in the filter rule.

CMoveDestinationRewriteStage

What it does

Rewrites the destination field MoveDestination in inbound C-MOVE requests. Use case: if the AE title is rewritten via AETitleRewriteStage, the C-MOVE request still carries the original destination AET, which the PACS no longer knows after the rewrite. This plugin replaces the destination AET with a value the PACS can reach (typically the pool or router AET).

Plugin class

ch.immeditech.imconnect.dicomrouter.plugin.CMoveDestinationRewriteStage

Config fields

Field Required/Default Description
destinationMapping optional Exact mapping original destination AET → new destination AET. Takes precedence over destinationOverride.
destinationOverride optional Fixed replacement AET for every destination AET without a mapping entry.
logRewrites Default true Logs every rewrite.
enabled Default true With enabled: true, at least one of destinationMapping/destinationOverride must be set.

Example

actions:
  cmoveRewrite_us:
    - plugin: ch.immeditech.imconnect.dicomrouter.plugin.CMoveDestinationRewriteStage
      config:
        destinationOverride: "US_POOL"
        # destinationMapping:
        #   US1: "RTR_US1"
        #   US2: "RTR_US2"
        logRewrites: true
        enabled: true

filter:
  - callingAET: US1
    calledAET: PACS
    actions:
      - "cmoveRewrite_us"
    destination:
      - "pacs1"

Notes

  • If neither a mapping entry nor an override exists for a destination AET, the request remains unchanged (with a warning in the log).
  • C-MOVE responses carry no destination field and are not touched; other message types are passed through unchanged.

CMoveTrackingStage

What it does

Prepares the return path of a C-MOVE for the Q/R bridge. The plugin does three things in one step: it rewrites MoveDestination exactly like CMoveDestinationRewriteStage (which it replaces in this scenario), it additionally swaps the C-MOVE request's MessageID for a router-owned, cluster-unique number, and it records which destination the returning images belong to under that number. A standard-conformant PACS copies the number into every image message it sends back, which lets the router match the second, independent connection to the original request. If the PACS leaves the field empty or fills in a value of its own, the router falls back to matching the image to the most recent fitting C-MOVE by its study UID. C-MOVE responses and a possible C-CANCEL from the device are rewritten back/forward automatically; the device only ever sees its own message number.

Status in this version: the path is complete: the plugin creates the tracking entry, the router accepts the PACS's return connection itself, resolves the mapping per image and forwards the image to the device (destination type qr-bridge, see configuration.md). The device gets its own message number and its own AE title back, and the status with which the device acknowledges an image is passed through to the PACS unchanged — warnings stay warnings. Because of the presentation context configuration, enabling it should still be coordinated with support.

Plugin class

ch.immeditech.imconnect.dicomrouter.plugin.CMoveTrackingStage

Config fields

Field Required/Default Meaning
destinationMapping optional Exact mapping original destination AET → new destination AET. Takes precedence over destinationOverride.
destinationOverride optional Fixed replacement AET for every destination AET without a mapping entry (typically the router AET).
returnDestination required Name of an entry in destinations: of type forward — that is where the returning images belong. A typo, and likewise a reference to a qr-bridge or reject destination, deliberately fails the config load; otherwise the mistake would only surface hours later on the return path.
store default memory memory for a single router, database in a cluster (shared database; requires an active database). Must match the store: of the qr-bridge destination — every store in use must be read by a bridge.
ttl default 24h Lifetime of a tracking entry (ms/s/m/h/d, at most 30 days).
logRewrites default true Logs every rewrite.
enabled default true With enabled: true, returnDestination and at least one of destinationMapping/destinationOverride must be set.

Example

actions:
  cmoveTracking_us1:
    - plugin: ch.immeditech.imconnect.dicomrouter.plugin.CMoveTrackingStage
      config:
        destinationOverride: "ROUTER"
        returnDestination: "us1"
        store: memory
        ttl: 24h

filter:
  - callingAET: US1
    calledAET: PACS
    actions:
      - "aetRewrite_us"
      - "cmoveTracking_us1"
    destination:
      - "pacs1"

Notes

  • If neither a mapping entry nor an override exists for a destination AET, the request is left completely unchanged (with a warning in the log) — nothing is tracked in that case either.
  • If no number can be allocated (database unreachable), the request is likewise forwarded unchanged and an error is logged. The connection always stays alive.
  • Switching store: memory to database (or back) requires a restart. So does removing or renaming such an entry when no entry with the same store takes its place in the same reload — the store instances are bound at startup, and a renamed entry is indistinguishable from a removed one. Every other field can be changed with reload.
  • The store value must match that of the qr-bridge destination which accepts the return path — otherwise one side writes into a different store than the other reads from. The configuration check rejects such a combination. Conversely, a qr-bridge destination without this plugin is allowed: in a cluster one node can accept the return connections while another instance writes the entries.
  • Tracking entries deliberately outlive the connection — the PACS often only comes back hours later. They are cleaned up by the lifetime setting and the operator-side database retention.
  • How long the router waits for a slow device, and after how many undeliverable bytes it aborts a return connection, is not configured here but on the qr-bridge destination (timeouts: and max-drain-bytes, see configuration.md).

DicomTapStage

What it does

Taps data of the captured connection as byte-exact DICOM files (Part-10) into a configurable storage location, or additionally sends it to a third-party system — without affecting the actual forwarding. Use case: tapping data for a third-party system, an archive, or an analysis without changing the productive flow to the PACS. By default the stored images (C-STORE) are tapped; via commands: other messages (such as query contents) can be captured as well. The wire transfer syntax is taken over 1:1; no transcoding takes place.

Plugin class

ch.immeditech.imconnect.dicomrouter.plugin.DicomTapStage

Config fields

Field Required/Default Description
pathTemplate Required unless forward set Absolute path template with placeholders for the storage location. At least one of pathTemplate/forward must be set.
sopClasses optional Whitelist of SOP class UIDs (exact UIDs, no wildcards). If absent, every selected message is tapped.
dataSetFilters optional Filter over data-set fields: list of {tag, values} entries. Only objects whose data set satisfies all entries are tapped (semantics below).
commands Default [C-STORE-RQ] Which DIMSE messages are tapped (e.g. C-FIND-RQ, C-FIND-RSP, C-MOVE-RQ).
finalize Default on-success on-success = store and send only after the forwarded connection's successful storage confirmation; always = immediately.
quotaMb optional Upper limit (in MB) for the storage location's disk usage. When exceeded, the tap pauses until usage drops.
forward optional Block for sending the captures as C-STORE to a destination (see below).
enabled Default true Disables the tap.

Placeholders in the pathTemplate: named aliases <callingAet>, <calledAet> (the device's original AETs before any rewrite), <sopClassUid>, <sopInstanceUid>, <studyInstanceUid>, <seriesInstanceUid>, <modality>, <date>/<time> (time of receipt), <command> (DIMSE name such as C-FIND-RSP), <messageId> (identifier of the exchange; request and response group together through it), <seq> (4-digit counter per exchange), <associationId> (connection identifier), as well as the generic <tag:GGGG,EEEE> for arbitrary header tags. If a value is missing, UNKNOWN is inserted.

Selecting "all Structured Reports": sopClasses compares exact UIDs — there is no category selection ("all SR"). For reference, the common SR SOP classes from the DICOM standard (for classic report SRs the first three are usually sufficient; which classes a device actually produces is stated in its DICOM Conformance Statement):

UID Description
1.2.840.10008.5.1.4.1.1.88.11 Basic Text SR
1.2.840.10008.5.1.4.1.1.88.22 Enhanced SR
1.2.840.10008.5.1.4.1.1.88.33 Comprehensive SR
1.2.840.10008.5.1.4.1.1.88.34 Comprehensive 3D SR
1.2.840.10008.5.1.4.1.1.88.35 Extensible SR
1.2.840.10008.5.1.4.1.1.88.40 Procedure Log
1.2.840.10008.5.1.4.1.1.88.50 Mammography CAD SR
1.2.840.10008.5.1.4.1.1.88.59 Key Object Selection Document
1.2.840.10008.5.1.4.1.1.88.65 Chest CAD SR
1.2.840.10008.5.1.4.1.1.88.67 X-Ray Radiation Dose SR
1.2.840.10008.5.1.4.1.1.88.68 Radiopharmaceutical Radiation Dose SR
1.2.840.10008.5.1.4.1.1.88.69 Colon CAD SR
1.2.840.10008.5.1.4.1.1.88.73 Patient Radiation Dose SR

dataSetFilters: — selection by data-set fields: In addition to the SOP class whitelist, filtering by data-set content is possible — the main use case is the modality (0008,0060):

        dataSetFilters:
          - tag: "0008,0060"        # Modality
            values: [CT, MR]
          - tag: "0008,0016"        # SOP Class UID via regex (MR Image Storage + Enhanced)
            values: ["regex:1\\.2\\.840\\.10008\\.5\\.1\\.4\\.1\\.1\\.4(\\..*)?"]

Semantics:

  • Each entry consists of tag (a top-level tag in hex notation "GGGG,EEEE" or as 8 hex digits "GGGGEEEE", private tags allowed; no descent into sequences) and values (at least one value).
  • Multiple entries must all match (AND); multiple values of one entry are alternatives (OR). The same tag may appear in several entries (those count as AND too — satisfiable via multi-valued attributes).
  • The comparison uses the value's text representation, exactly (after removing surrounding whitespace). A value with the regex: prefix is a regular expression and must match the entire value — the same prefix convention as for the AE titles of the filter rules; unlike there, however, * is not a wildcard here but an ordinary character (only regex: is special).
  • Tags with a binary VR (e.g. Rows US, PixelData OW) can never match as text and are rejected at config load; numeric string VRs such as IS/DS (e.g. SeriesNumber) work normally.
  • For multi-valued attributes (backslash-separated, e.g. modality CT\MR) one matching item is sufficient.
  • If the attribute is missing from the data set or empty (including: consisting only of padding spaces), the object is not tapped (noted in the application log at DEBUG level). A read error during the filter check likewise only skips that capture (the first occurrence is logged as a warning) — as always, the forwarding is never affected.
  • Without dataSetFilters the behavior stays unchanged (every selected message is tapped). The filter also applies to commands: captures — there it is checked against the fields of the captured data set (e.g. the keys of a query identifier). Careful with C-FIND requests (C-FIND-RQ): return keys such as the modality are usually empty in the request (universal matching) — a filter on them therefore never captures anything for RQ captures; there it only makes sense for matching keys that are actually filled. Responses (C-FIND-RSP) carry the values filled in.

Limitations: For private tags or tags unknown to the router the value is compared as raw Latin-1 text — umlauts/special characters may not match there; without a character-set declaration (0008,0005), ASCII is assumed. Extremely large attribute values are not read (counts as a read error → never captured), and individual values over 1024 characters never match.

forward: block:

Field Required/Default Description
destination Required Name of a destination from destinations:; must be of type forward.
spoolPath Required Absolute directory for the queue; a dedicated directory per action.
onSuccess Default delete delete = delete sent files; done = move to <spool>/done/.
spoolMaxMb optional Upper limit of the queue; above it, spooling pauses (warning). Only counts the active queue — done/ and failed/ do not count.
callingAet Default AET of the router listener Calling AET when sending.
calledAet Default AET of the destination Called AET when sending.

Example

actions:
  tapSr:                                  # store reports (SR) only
    - plugin: ch.immeditech.imconnect.dicomrouter.plugin.DicomTapStage
      config:
        pathTemplate: "/var/imconnect/tap/sr/<callingAet>/<studyInstanceUid>/<sopInstanceUid>.dcm"
        sopClasses:
          - "1.2.840.10008.5.1.4.1.1.88.11"   # Basic Text SR
          - "1.2.840.10008.5.1.4.1.1.88.22"   # Enhanced SR
          - "1.2.840.10008.5.1.4.1.1.88.33"   # Comprehensive SR
        finalize: on-success
        quotaMb: 10240

  tapForward:                             # send captures to a third-party system
    - plugin: ch.immeditech.imconnect.dicomrouter.plugin.DicomTapStage
      config:
        forward:
          destination: pacs2
          spoolPath: /var/imconnect/tap-spool/sr
          onSuccess: delete
          spoolMaxMb: 4096

  tapQueries:                             # capture C-FIND exchange in both directions
    - plugin: ch.immeditech.imconnect.dicomrouter.plugin.DicomTapStage
      config:
        pathTemplate: "/var/imconnect/tap/queries/<callingAet>/<date>/<messageId>/<command>-<seq>.dcm"
        commands: ["C-FIND-RQ", "C-FIND-RSP"]

filter:
  - callingAET: US1
    calledAET: PACS
    actions:
      - "tapSr"
    destination:
      - "pacs1"

Notes

  • Atomic files: A stored file only appears at the target path once it is complete. Downstream scripts or inotify watchers never see a partial file.
  • finalize: With on-success (default), a capture only takes effect once the successful storage confirmation (C-STORE response, Success or Warning) arrives from the destination of the forwarded connection — that is, from the filter rule's primary destination, not from the forward: destination. The gating applies equally to both outputs: neither is the file stored nor anything placed in the sending spool while the confirmation is pending; a failure or connection abort discards the capture. With always, the capture is stored immediately, regardless of the outcome.
  • Combining storage and sending: pathTemplate and forward may appear in the same instance (at least one of the two must be configured) — then every capture is stored and sent. Separate action entries are only needed if storage and sending are to have different selections (sopClasses, commands).
  • The tap only observes: The connection always needs a normal forwarding destination in the filter rule — "tap only, without forwarding" does not exist (a reject destination would reject the connection before any data flows).
  • Target directories and write permissions: The router creates subdirectories below the template's root directory itself; the root directory must be writable for the service user. Careful with the bundled systemd unit: It only allows writes under /var/imconnect and /var/log/imconnect (ProtectSystem=strict). If the storage location is to live elsewhere (e.g. /data/...), the path must be added to the unit under ReadWritePaths= — otherwise the tap pauses with a warning in the application log while forwarding continues normally. Captures land readable for the service user only (files 0600, directories the router creates itself 0700) — the files contain patient data. If a consumer picks the files up under a different account, it needs explicitly granted access, for example via the group or an ACL (setfacl) on the storage directory, or it runs under the router's account; pre-existing, operator-provisioned directories keep their permissions.
  • Never disruptive: Every tap error (full disk, unwritable directory) is logged, pauses the tap for the connection, and lets the forwarding continue unchanged. The productive route is never affected.
  • Failed sends (failed/): Sends that the destination permanently rejects (e.g. unsupported transfer syntax or repeatedly failed storage attempts) are moved by the sender to <spool>/failed/ with a logged warning — they do not block the queue and are not retried automatically. The same applies when the destination answers a storage attempt illegally: the image is fully transferred by that point, only the acknowledgement is unusable. Another attempt would just create a second copy at the destination, so the entry goes straight to failed/. If the destination instead aborts the connection mid-transfer, that counts as one failed delivery attempt like a regular failure response: an abort can be temporary (a device that is restarting), so it is retried and the entry is only filed away after five attempts.
  • Quota/cleanup: Regular cleanup of the storage location as well as the done/ and failed/ directories is the responsibility of the consumer or operator — done/ and failed/ do not count against spoolMaxMb and otherwise grow without bound. quotaMb and spoolMaxMb apply per action entry (each entry has its own root or spool directory); they are a safeguard against filling up the disk, not a retention mechanism.
  • PHI note: If PHI-bearing tags (e.g. patient name) are included in the pathTemplate, patient data ends up in file names. Whether that is permissible and how the storage directory is secured is a deliberate operator decision.
  • Query capture (commands:): When capturing queries, mandatory placeholders apply to the pathTemplate so that captures do not overwrite each other: when capturing responses, <seq> and <messageId> are required (one query yields many responses, and <seq> restarts per exchange); when capturing multiple commands, <command> is required. If captures from different connections must never overwrite each other, <associationId> belongs in the template as well.
  • Sending via loopback: If the sending is to pass through further rewrite plugins, point the forward destination at a router listener; a matching filter rule attaches the desired actions there. This loopback rule must not itself reference a tap forward action, otherwise an endless loop arises (the config load warns about this constellation).

CharsetTranscodePlugin

What it does

Re-encodes the text values of selected DICOM messages from one character set into another and rewrites the character-set declaration (0008,0005) accordingly. Use case: a device cannot render UTF-8 (ISO_IR 192) — e.g. a modality displays worklist responses with umlauts incorrectly because its display side only handles Latin-1 (ISO_IR 100). The plugin transcodes the worklist responses on their way to the device; the source system (RIS/PACS) stays untouched.

All text fields with an extended character repertoire are re-encoded (person names, descriptions, comments — the VRs PN/LO/SH/ST/LT/UT/UC), including inside sequences. Data sets without a character-set declaration, or with a declaration not listed in from, pass through byte-identically. If the target character set is ISO_IR 6 (default repertoire, ASCII), (0008,0005) is removed — as the DICOM standard requires.

Plugin class

ch.immeditech.imconnect.dicomrouter.plugin.charset.CharsetTranscodePlugin

Config fields

Field Required/Default Meaning
from Required Source character sets (DICOM terms, e.g. ISO_IR 192). Only data sets whose (0008,0005) declares one of these terms are transcoded.
to Required Target character set (DICOM term). ISO_IR 6 removes the declaration. Must not also appear in from.
commands Default ["C-FIND-RSP"] Which DIMSE messages are transcoded (e.g. C-FIND-RSP, C-FIND-RQ, C-STORE-RQ). The direction follows from RQ/RSP.
sopClasses optional (empty = all) Restriction to specific SOP classes (the command's Affected SOP Class UID), e.g. 1.2.840.10008.5.1.4.31 for the Modality Worklist.
replacement Default ? Replacement for characters the target character set cannot represent (exactly one character; no DICOM delimiter \, ^, = and no control character). Replacements are logged once per connection as a warning (tag numbers only, never values).
transliterate Default false Transliterates unrepresentable characters before replacement kicks in: first a built-in table (ä→ae, ö→oe, ü→ue, Ä→Ae, Ö→Oe, Ü→Ue, ß→ss, ẞ→SS), then generic accent stripping (é→e, à→a, ç→c, ñ→n, …). Example: Mueller^Joerg instead of M?ller^J?rg toward an ASCII-only device. Without the flag the plugin behaves exactly as before.
transliterations optional (empty) Custom entries extending or overriding the built-in table (e.g. "ø": "oe"). Key: exactly one character; value: text that is representable in the target character set and contains no DICOM delimiters (\, ^, =) or control characters. Only allowed together with transliterate: true.
logTranscodes Default true Logs every transcoded message (without contents).
enabled Default true Disables the plugin.

Supported character-set terms: ISO_IR 6 (ASCII), the single-byte family ISO_IR 100/101/109/110/144/127/126/138/148/166/203, plus ISO_IR 192 (UTF-8), GB18030 and GBK. The config load rejects unknown terms.

Example

Transcoding worklist responses for a Latin-1 device:

actions:
  worklistLatin1:
    - plugin: ch.immeditech.imconnect.dicomrouter.plugin.charset.CharsetTranscodePlugin
      config:
        enabled: true
        commands: ["C-FIND-RSP"]                  # default
        sopClasses: ["1.2.840.10008.5.1.4.31"]    # Modality Worklist
        from: ["ISO_IR 192"]
        to: "ISO_IR 100"
        replacement: "?"

filter:
  - callingAET: US1
    actions:
      - "worklistLatin1"
    destination:
      - "ris1"

Which device gets the transcoding is, as always, controlled by the filter rule — no AET logic is needed in the plugin itself.

For an ASCII-only device, transliterate: true yields readable names instead of replacement characters — Mueller^Joerg instead of M?ller^J?rg:

actions:
  worklistAscii:
    - plugin: ch.immeditech.imconnect.dicomrouter.plugin.charset.CharsetTranscodePlugin
      config:
        commands: ["C-FIND-RSP"]
        sopClasses: ["1.2.840.10008.5.1.4.31"]
        from: ["ISO_IR 192", "ISO_IR 100"]
        to: "ISO_IR 6"
        transliterate: true
        # transliterations:        # optional: extend/override the built-in table
        #   "ø": "oe"

Notes

  • Warning — lossy targets: A target like ISO_IR 100 cannot represent every source character — unrepresentable characters are irrevocably replaced by the replacement character. Therefore use lossy transcodings only toward display devices (worklist, query responses). Toward an archive/PACS, only the lossless target ISO_IR 192 (UTF-8) is defensible — archived objects with replaced characters are permanently damaged.
  • Transliteration only touches unrepresentable characters: A target like ISO_IR 100 can represent ü — there ü is kept, even with transliterate: true. ue only appears when the target does not know the character (e.g. ISO_IR 6/ASCII). For uppercase letters the substitution adapts to its surroundings: MÜLLER becomes MUELLER, Müller becomes Mueller — this also applies to ß in an all-caps environment (STRAßE becomes STRASSE). Custom transliterations: entries, by contrast, are always used exactly as configured — without any case adaptation.
  • Truncation of overlong values (only with transliterate: true): Transliteration can lengthen values (one character becomes two). If a value thereby grows past the maximum length the DICOM standard allows for its field type (person name and long description 64 bytes, short description 16 bytes, short text 1024 bytes, long text 10240 bytes; unlimited text fields are never cut), it is truncated at the end — for multi-valued fields each value individually, the separators survive. Truncations are logged once per connection as a warning (tag numbers only, never values). Without transliterate: true nothing is ever truncated.
  • Matching against RIS/PACS: After transliteration the device shows Mueller while RIS and PACS still carry Müller. If the device sends the patient name back (e.g. in MPPS or in the images), it differs from the order system — in practice, matching runs on Patient ID and Accession Number, not on the name. Compared to M?ller, transliteration is deterministic and readable.
  • ISO 2022 is not supported: Multi-valued (0008,0005) declarations (code extensions, e.g. \ISO 2022 IR 100 or Japanese/Korean multi-byte sets) are not transcoded — such data sets pass through unchanged, with a warning in the log (once per connection).
  • Implicit-VR limitation: With Implicit-VR transfer, the field type is not in the data stream. The plugin recognizes standard tags through a built-in table; private and unknown tags stay untouched under Implicit VR (under Explicit VR they are transcoded normally). Unresolvable sequences of unknown tags stay untouched as well.
  • All or nothing: A data set is either transcoded completely or passed through completely unchanged. On any unexpected problem (e.g. a pathologically large text value) the data set passes through unchanged — the plugin never drops messages and never aborts connections.
  • Sequence items with their own (0008,0005) declaration are transcoded with that set, provided it is listed in from; if an item declares a character set outside from, the whole item stays untouched (warning in the log).
  • No transfer-syntax conversion takes place — only the character encoding of the text values changes.

CharsetDeclarePlugin

What it does

Sets or overrides only the character-set declaration (0008,0005) of selected DICOM messages — the text values themselves are never re-encoded (that is the CharsetTranscodePlugin's job). Two use cases:

  • mode: if-absent (default): A query (e.g. a worklist C-FIND-RQ) without a character-set declaration gets one — many RIS/PACS then deliver their responses in the requested character set right away, i.e. they transcode at the source. That often makes transcoding the responses in the router unnecessary.
  • mode: always: Fixes lying declarations — a device declares e.g. ISO_IR 100 but actually sends UTF-8 bytes. The plugin rewrites the declaration to the actual character set without touching the bytes.

If the configured character set is ISO_IR 6 (default repertoire, ASCII), (0008,0005) is removed instead of set — as the DICOM standard requires (the default repertoire is expressed by absence of the declaration).

Plugin class

ch.immeditech.imconnect.dicomrouter.plugin.charset.CharsetDeclarePlugin

Config fields

Field Required/Default Meaning
charset Required The character set to declare (DICOM term, e.g. ISO_IR 100). ISO_IR 6 removes the declaration (only with mode: always).
mode Default if-absent if-absent: set only when (0008,0005) is missing or blank (with an ASCII check, see notes). always: always override an existing declaration.
commands Default ["C-FIND-RQ"] Which DIMSE messages get the declaration (e.g. C-FIND-RQ, C-FIND-RSP, C-STORE-RQ). The direction follows from RQ/RSP.
sopClasses optional (empty = all) Restriction to specific SOP classes (the command's Affected SOP Class UID), e.g. 1.2.840.10008.5.1.4.31 for the Modality Worklist.
logDeclares Default true Logs every set/removed declaration (without contents).
enabled Default true Disables the plugin.

Supported character-set terms: the same as for the CharsetTranscodePlugin (ISO_IR 6, the single-byte family ISO_IR 100203, ISO_IR 192, GB18030, GBK). The config load rejects unknown terms.

Example

Worklist queries of a Latin-1 device get a character-set declaration so the RIS delivers its responses in Latin-1 itself; as a fallback for RIS systems that ignore the query character set, the CharsetTranscodePlugin additionally transcodes the responses:

actions:
  worklistCharset:
    - plugin: ch.immeditech.imconnect.dicomrouter.plugin.charset.CharsetDeclarePlugin
      config:
        commands: ["C-FIND-RQ"]                   # default
        sopClasses: ["1.2.840.10008.5.1.4.31"]    # Modality Worklist
        charset: "ISO_IR 100"
        mode: if-absent
    - plugin: ch.immeditech.imconnect.dicomrouter.plugin.charset.CharsetTranscodePlugin
      config:
        commands: ["C-FIND-RSP"]                  # fallback: transcode the responses
        sopClasses: ["1.2.840.10008.5.1.4.31"]
        from: ["ISO_IR 192"]
        to: "ISO_IR 100"

filter:
  - callingAET: US1
    actions:
      - "worklistCharset"
    destination:
      - "ris1"

Which device gets the declaration is, as always, controlled by the filter rule.

Notes

  • No bytes are ever re-encoded. The plugin only changes the (0008,0005) declaration; all text values pass through byte-identically. If the values themselves need re-encoding, that is the CharsetTranscodePlugin's job.
  • ASCII check with if-absent: Before setting a declaration, the plugin checks all recognized text values (the VRs PN/LO/SH/ST/LT/UT/UC, including inside sequences). If any value contains a byte outside ASCII, nothing is set and the message passes through unchanged (warning in the log, once per connection) — bytes of unknown encoding are never re-interpreted. With mode: always this check is deliberately skipped: re-declaring existing non-ASCII bytes is exactly the point there.
  • Sequence items with their own (0008,0005) declaration stay untouched in both modes (warning in the log) — only the top-level declaration is written; the item's own declaration keeps governing its values.
  • Implicit-VR limitation: With Implicit-VR transfer, the field type is not in the data stream; the ASCII check recognizes text fields through a built-in table of the standard tags. Private and unknown tags are not checked — the check is conservatively limited to the recognized text values.
  • ISO_IR 6 removes: The default character set is expressed on the wire by absence of (0008,0005); charset: "ISO_IR 6" therefore removes an existing declaration instead of setting one. This only works with mode: always — the combination with if-absent would be guaranteed to have no effect (with the declaration absent there is nothing to remove) and is rejected at config load.
  • First check whether the modality can do it itself: Many devices can be configured to request the desired query character set themselves — that is the cleanest solution. The plugin is meant for devices where that is not possible.
  • On any unexpected problem the message passes through unchanged — the plugin never drops messages and never aborts connections.