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
|
Context. A local human-machine interface needs to observe and
command a taktora-executor application without binding taktora to
any one UI toolkit or runtime. The grill that produced
UI connector (MVVM) (FEAT_0092) resolved five tightly-coupled forks, each with a
rejected alternative: (1) the boundary’s reach — Rust-GUI-only vs
language/runtime-agnostic; (2) who builds the ViewModel — a passive
transport the app pushes into vs an active aggregator that reaches
into executor/stats/other connectors; (3) the substrate — desugar
onto the existing Decision. Adopt the following bundled posture for the UI connector, captured as the requirements cited:
Consequences. ✅ A WPF/.NET, web, C++, or Python UI can bind off
the manifest on day one — the stated goal — without taktora shipping
N language generators. ✅ The deterministic executor is never
encoded on, blocked, or back-pressured by a UI consumer
(Producer writes a seqlock l... (REQ_0860) / Command handler runs off th... (REQ_0870)). ✅ Health, lifecycle,
|
Context. J1939 sits on top of CAN. Three ways to realise it: (A)
depend on Decision. Take (A). Consequences. ✅ The framework’s deterministic layer-1 mock story
survives — |
Context. Every payload normally crosses iceoryx2 as a fixed- Decision. Deliver J1939 in two tiers. Tier 1 — single-frame PGNs
and BAM/RTS-CTS (≤ 1785 B) — rides the existing Consequences. ✅ ETP is genuinely supported without inlining a
worst-case buffer into every sample, and the common hot path keeps its
fixed-size POD envelope. ✅ The bounded-allocation invariant stays
auditable — growth is capped and explicit, not open-ended. ❌ The
connector has two delivery paths and the slice channel is new framework
surface in |
Context. A node on a shared J1939 bus needs a valid source address. The minimum is a configured static SA; J1939-81 defines the full claim/cannot-claim protocol (NAME-priority contention, null address 254, Request-for-AC, Address-Commanded). Decision. Implement the full J1939-81 claim state machine
(J1939 address-claim state m... (BB_0102) / Full J1939-81 address claiming (REQ_0897)), not just a static SA. Claim
state maps onto Consequences. ✅ The connector is well-behaved on a real shared bus
— it arbitrates, cedes to higher-priority NAMEs, and honours commanded
addresses. ✅ Claim state reuses the existing |
Context. An MQTT connection drops and must be re-established. The
Zenoh reference connector (Zenoh queries live on a con... (ADR_0040) cluster) does not repair
connections at all — it surfaces liveness as Decision. The gateway task pumps Open end. Consequences. ✅ Least code on the recovery path, reusing the
library’s battle-tested reconnect. ✅ Mirrors the connector framework’s
“observe liveness as health, don’t repair in the connector” stance
(Zenoh queries live on a con... (ADR_0040)). ❌ Backoff policy is bounded by what |
Context. MQTT subscriptions may use wildcards ( Decision. The gateway keeps a subscription table (filter →
gateway-side writer) and registers each distinct filter with the
broker exactly once (deduplicating overlapping filters so the broker
never multi-delivers). On each inbound PUBLISH the gateway runs MQTT
topic-matching locally against every registered filter and fans the
message out to all matching channels. The table is reference-counted:
an Consequences. ✅ A single source of truth for fan-out that does not
depend on broker-specific behaviour. ✅ Ref-counting keeps broker
subscription state minimal and correct across channel teardown. ❌ The
connector carries its own MQTT topic-matcher and a rewritten filter
validator ( |
Context. MQTT Decision. v1 connects with Consequences. ✅ Deterministic, self-contained recovery with no dependence on broker-side session storage or client-id stability. ✅ The replay loop is straightforward to test against the mock session. ❌ Messages published to subscribed topics during a disconnect window are not recovered — persistent-session semantics are deferred to a follow-on. ❌ Every reconnect pays a re-subscribe round-trip. |
Context. Decision. Consequences. ✅ The MQTT connector documents a sensible default and
the |