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 pluginsPduLoggingStagePluginandExamplePluginStagelive underch.immeditech.imconnect.plugins.…— the safest approach is to copy theplugin: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¶
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-folderdirectory. 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¶
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
CMoveDestinationRewriteStageorWorklistAetRewriteStageare 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¶
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
responseMappingexplicitly; 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¶
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
setanddefaulton the same field) are rejected at startup or reload — not only at runtime. - Traffic outside the
sopClasseswhitelist, 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
setsets the same value that is already present anyway, no change occurs. - If both
QrFieldRewriteStage(with MWL insopClasses) andWorklistAetRewriteStageact 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¶
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¶
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: memorytodatabase(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 samereload— the store instances are bound at startup, and a renamed entry is indistinguishable from a removed one. Every other field can be changed withreload. - The
storevalue must match that of theqr-bridgedestination 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, aqr-bridgedestination 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-bridgedestination (timeouts:andmax-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¶
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) andvalues(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 (onlyregex:is special). - Tags with a binary VR (e.g. Rows
US, PixelDataOW) can never match as text and are rejected at config load; numeric string VRs such asIS/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
dataSetFiltersthe behavior stays unchanged (every selected message is tapped). The filter also applies tocommands: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: Withon-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 theforward: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. Withalways, the capture is stored immediately, regardless of the outcome.- Combining storage and sending:
pathTemplateandforwardmay 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
rejectdestination 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/imconnectand/var/log/imconnect(ProtectSystem=strict). If the storage location is to live elsewhere (e.g./data/...), the path must be added to the unit underReadWritePaths=— otherwise the tap pauses with a warning in the application log while forwarding continues normally. Captures land readable for the service user only (files0600, directories the router creates itself0700) — 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 tofailed/. 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/andfailed/directories is the responsibility of the consumer or operator —done/andfailed/do not count againstspoolMaxMband otherwise grow without bound.quotaMbandspoolMaxMbapply 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 thepathTemplateso 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¶
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 100cannot 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 targetISO_IR 192(UTF-8) is defensible — archived objects with replaced characters are permanently damaged. - Transliteration only touches unrepresentable characters: A target like
ISO_IR 100can representü— thereüis kept, even withtransliterate: true.ueonly 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ÜLLERbecomesMUELLER,MüllerbecomesMueller— this also applies toßin an all-caps environment (STRAßEbecomesSTRASSE). Customtransliterations: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). Withouttransliterate: truenothing is ever truncated. - Matching against RIS/PACS: After transliteration the device shows
Muellerwhile RIS and PACS still carryMü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 toM?ller, transliteration is deterministic and readable. - ISO 2022 is not supported: Multi-valued
(0008,0005)declarations (code extensions, e.g.\ISO 2022 IR 100or 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 infrom; if an item declares a character set outsidefrom, 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 100but 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¶
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 100–203, 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. Withmode: alwaysthis 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 6removes: 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 withmode: always— the combination withif-absentwould 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.