Solution strategy¶
arc42 §4.
The framework’s shape is the consequence of ten architectural decisions
made during brainstorming. Each decision is captured here as an ADR
that :refines: the requirement or feature it answers.
Context. Four protocol connectors (MQTT, OPC UA, gRPC, ADS) and three codecs (JSON, Protobuf, MessagePack) were on the table. Each protocol introduces its own design quirks; specifying all four in one round risks the spec drifting into protocol-specific minutiae. Decision. This spec covers the framework core plus MQTT as the reference connector. OPC UA / gRPC / ADS get follow-on specs reusing the same five contracts. Consequences. ✅ Spec stays focused on the framework’s contracts. ✅ MQTT exercises every contract (codec, routing, health, reconnect) end-to-end. ❌ Other connector specs are blocked on this one landing. |
Context. PLC runtime heart on iceoryx2 (FEAT_0010) “PLC runtime heart” is the existing top-level umbrella, with Fieldbus integration interface (FEAT_0023) “Fieldbus integration interface” as a sub-feature. The connector framework is broader than fieldbus (MQTT and gRPC are application-protocol level). Decision. Add Connector framework (FEAT_0030) “Connector framework” as a peer
top-level feature, not under PLC runtime heart on iceoryx2 (FEAT_0010). Fieldbus integration interface (FEAT_0023)
later Consequences. ✅ Honest semantics — the framework is general purpose, not PLC-bound. ❌ The spec now has two top-level umbrellas, which the overview page should explicitly explain. |
Context. Gateway-as-separate-process gives fault isolation (Fault isolation between pro... (QG_0001)); gateway-as-tokio-task is operationally simpler (one binary, one signal handler). Different consumers want different trade-offs. Decision. Define the framework so the same envelope/iceoryx2 contract works in either deployment. The host wires the gateway as a tokio task or a separate binary using identical code; only process-startup differs. Consequences. ✅ Fault-isolation-conscious deployments and single-binary deployments share one framework. ❌ Both paths must be tested; shutdown coordination is specified twice (in-process, out-of-process), but the SHM mechanics are unchanged. |
Context. A universal 64 KB envelope (the C# Apex.Ida pattern) wastes shared memory for small messages and refuses large ones. iceoryx2’s typed services support per-service payload sizes. Decision. Consequences. ✅ Memory sized to the workload. ✅ Type system prevents publishers and subscribers from disagreeing on size. ❌ Different channels are different types; const-generic monomorphisation could grow code size if many channel sizes are used (see Const-generic monomorphisat... (RISK_0003)). |
Context. Two clean alternatives existed: type-erased
Decision. Generic-on-connector. Concrete connector types are
Consequences. ✅ Zero dynamic dispatch on the hot path. ✅ Codec
errors carry a static |
Context. Two alternatives: Decision. Explicit builder. Matches taktora-executor’s existing
Consequences. ✅ One file you can grep for the wiring; no link-time global state alongside the compile-time generics. ❌ Adding a connector requires rebuilding the host (already true given Codec is a generic paramete... (ADR_0005)). |
Context. Three options: tokio-only gateway (separate world from plugin), taktora-executor on both sides with tokio bridged in, or raw-iceoryx2 gateway emitting unified observability. Decision. Both halves are Consequences. ✅ |
Context. Three positions: opaque channel name + side-channel YAML config; channel name + typed routing struct; channel name + key-value attribute bag. Decision. Typed routing struct ( Consequences. ✅ Routing is part of the public, type-checked API. ✅ Catches misspelled / missing fields at compile time. ❌ Plugin code is connector-aware (no protocol-portable channels — see NO protocol-portable Channe... (REQ_0294)). |
Context. Different protocol stacks own reconnect differently —
Decision. Provide both a Consequences. ✅ Stacks that fit a uniform policy aren’t reinventing backoff; stacks that handle reconnect internally aren’t forced into a foreign mechanism. ❌ Two ways to get reconnect means new connector authors must pick the right one for their protocol. |
Context. “Reference connector” must exercise enough of the framework’s contracts to validate them, without ballooning into MQTT-protocol-minutiae territory. Decision. Pub+sub, QoS 0+1, retained messages, wildcard subscriptions, username/password auth, optional TLS, MQTT 3.1.1. Defer: QoS 2, MQTT 5, LWT, persistent sessions, client-cert TLS. Consequences. ✅ Each deferred feature exercises framework contracts — adding them later doesn’t reshape the framework. ❌ MQTT 5 user-properties / shared-subscriptions adoption is blocked on a follow-on spec. |
Context. EtherCAT MainDevice options in Rust are Decision. Use Consequences. ✅ No C build dependencies; one |
Context. An EtherCAT network is physically one segment per network interface; the MainDevice owns that segment’s TX/RX cycle. Multi-NIC support would require multiple MainDevices arbitrating shared cycle timing and working-counter state. Decision. Each Consequences. ✅ Cycle timing, working-counter ownership, and Distributed Clocks bring-up have a single source of truth. ✅ Mirrors NO multi-broker / multi-ten... (REQ_0295) (one broker per MQTT gateway). ❌ Operators wanting one process to own two EtherCAT segments must instantiate two gateways (acceptable — rare configuration). |
Context. EtherCAT SubDevice PDO mappings can be sourced two ways: (1) parsing an ESI / EEPROM XML descriptor per SubDevice at startup, or (2) declaring the mapping in application code at build time. ESI parsing is what TwinCAT and similar engineering tools do; it handles arbitrary vendor modules. Static declaration trades generality for compile-time type safety on the routing struct. Decision. The application declares each SubDevice’s PDO
mapping as a static description in Consequences. ✅ |
Context. DC sub-microsecond synchronisation matters for motion
control and time-stamped sampling; many EtherCAT deployments
(digital I/O, ramped analog, slow process control) don’t need it.
DC bring-up adds a multi-pass register dance (BWR Decision. The gateway performs DC bring-up only when
Consequences. ✅ Buses without DC-capable SubDevices work out of the box. ✅ Bring-up latency is lower when DC is unused. ❌ Motion-control applications must remember to enable DC. ❌ Two bring-up paths to test (with and without DC). |
Context. ethercrab supports Linux raw sockets, NPCAP / WinPcap
on Windows, and Decision. The first cut uses ethercrab’s Consequences. ✅ One bring-up path to test in the first cut. ✅ Deployment recipe is “install the binary, grant CAP_NET_RAW”. ❌ Windows-based engineering desks cannot run the gateway natively (they can run plugins; the gateway must live on Linux). ❌ Embedded MCU EtherCAT mainboards await a follow-on spec. |
Context. taktora-connector-ethercat (BB_0030) decomposes into plugin (EthercatConnector (sub-bloc... (BB_0031)),
gateway (EthercatGateway (sub-block ... (BB_0032)), PDO mapping (PDO mapping (sub-block of B... (BB_0033)), and the
tokio bridge (Tokio bridge for ethercrab ... (BB_0034)). An implementing crate can either
place everything in one Decision. Consequences. ✅ Each module maps to one BB, so the
|
Context. Tokio sidecar contained ins... (REQ_0321) requires the ethercrab TX/RX task to
run on a tokio runtime contained inside the connector crate, with
no tokio leakage into taktora-executor’s Decision. Each Consequences. ✅ Lifecycle is one-to-one with the gateway — no global state, multiple gateways on one host are independent. ✅ Mirrors Single MainDevice per gateway (ADR_0021) (one MainDevice per gateway). ❌ Spawning two gateways doubles the tokio worker-thread count; operators wanting a shared pool must consolidate gateways or wait for a follow-on spec. |
Architecture Decision: ``EthercatConnectorOptions`` is a typed builder; PDO map declared as ``&'static [SubDeviceMap]`` ADR_0027
|
Context. Static PDO mapping per SubD... (REQ_0314) requires the PDO mapping be declared
by the application at build time via Decision. Consequences. ✅ No heap allocation for the PDO map after
gateway construction (consistent with taktora-executor’s REQ_0060
posture for the steady-state hot path). ✅ Builder API parallel to
the framework’s other connector options. ❌ Applications that need
runtime-discovered PDO maps (e.g. EEPROM-parsed) must roll their
own |
Context. EtherCAT reference connector (FEAT_0041) ships 16 TEST artefacts
(TEST_0200..TEST_0215) verifying REQ_0310..REQ_0325. Six of those
tests (TEST_0203, TEST_0205, TEST_0208, TEST_0209, TEST_0210,
TEST_0215) exercise real bus state transitions, PDO mapping
application, working-counter accounting, DC bring-up, or raw
socket access — operations that need either an Decision. The connector’s testable logic is factored into
pure-Rust modules — taktora-connector-ethercat ... (IMPL_0050)’s Consequences. ✅ Every PR build is green on every developer
machine and CI runner — no flaky “missing NIC” failures.
✅ The factored pure-logic modules ( |
Architecture Decision: Zenoh queries live on a concrete handle type, not the Connector trait ADR_0040
|
Context. The framework explicitly rejected protocol-portable
channels (NO protocol-portable Channe... (REQ_0294)) and framework-level request/response
matching (NO request/response matchin... (REQ_0290)). Three options for surfacing Zenoh
queries existed: (a) concrete methods on Decision. Option (a). Consequences. ✅ Honors NO request/response matchin... (REQ_0290) / NO protocol-portable Channe... (REQ_0294). ✅
MQTT and EtherCAT connectors are not forced to invent
no-op query plumbing. ❌ Plugin code wanting queries depends on
the concrete |
Context. Zenoh’s own session machinery handles scout and
reconnect (peer mode) and reconnect-to-router (client mode). The
framework provides ReconnectPolicy trait (REQ_0232) Decision. The Zenoh connector follows the
stack-internal-reconnect path. Consequences. ✅ No duplicate retry policy contending with
Zenoh’s own. ✅ Health emission stays uniform across all
connectors (HealthEvent emitted on ever... (REQ_0234)). ❌ If a future user wants
|
Context. Connector ships its own rou... (REQ_0224) already declares that each
connector ships a single routing struct ( Decision. Option (a). Consequences. ✅ Preserves Connector ships its own rou... (REQ_0224)’s single-routing- struct rule. ✅ Mirrors MqttRouting carries topic, ... (REQ_0251) (MQTT carries QoS in routing). ❌ Per-channel query target / consolidation overrides require a builder method instead of a routing field — accepted tradeoff for type-system simplicity. |
Context. Device configuration that must precede PDO assignment
(motor current, operation-mode selection) has to be written before the
Decision. Such configuration is declared as a static
Consequences. ✅ Bring-up stays fully declarative and reproducible
from the checkout (consistent with |
Context. Multi-reply Zenoh queries need an end-of-stream
signal in addition to data chunks. Two options: (a) allocate
one bit of Decision. Option (b). Every envelope on the two reply-side
iceoryx2 services ( Consequences. ✅ Framework anti-goal (no inspection of
envelope payload, no protocol-portable semantics in the
reserved word) preserved. ✅ Future connectors can re-use the
pattern without coordinating with the framework. ❌ Plugin-side
|