Connector framework — architecture (arc42)¶
Architecture documentation for the connector framework, structured per the arc42 template (12 sections) and encoded with sphinx-needs using the useblocks “x-as-code” conventions (https://x-as-code.useblocks.com/how-to-guides/arc42/index.html).
Each architectural element :refines: or :implements: a parent
requirement from Connector framework so the trace is
preserved end-to-end.
1. Introduction and goals¶
The connector framework’s reason-to-exist is fault isolation: keep messy network protocol code (MQTT, OPC UA, gRPC, fieldbus) outside the taktora-executor application’s deterministic core, while preserving zero-copy data flow. Quality goals capture the qualities that the architecture is optimised for.
A panic, hang, or crash in a protocol stack (rumqttc, opcua, tonic, ADS) shall not be able to crash, deadlock, or stall the taktora-executor application that uses the framework. This goal is what motivates the gateway-as-separate-process deployment shape and the single-direction control plane. |
Plugin code that targets a specific protocol shall be checked at
compile time for routing correctness, codec compatibility, and
payload-size compliance. Runtime “config-as-strings” indirection
shall be avoided; type errors are caught by |
Outbound messages from the application to the broker shall not be
copied into any intermediate buffer between the codec’s encode call
and the iceoryx2 publish. The iceoryx2 |
Every connector — regardless of which protocol stack owns its reconnect mechanism — shall report the same four health states (Up / Connecting / Degraded / Down) on a single observable channel, so monitoring and alerting code is connector-agnostic. |
2. Constraints¶
Constraints come from the surrounding workspace and the iceoryx2 ecosystem; they are non-negotiable inputs to the architecture.
The plugin and gateway shall be taktora-executor consumers
( |
The framework shall use the workspace’s pinned iceoryx2 version
( |
All new crates shall target edition 2024 with MSRV 1.85, matching
the workspace’s |
Workspace tests run with |
Where async protocol stacks ( |
3. Context and scope¶
The connector framework sits between a taktora-executor application and one or more external systems (brokers, servers, PLCs). Internally, the boundary is split between a plugin (in-app side) and a gateway (out-of-app side); externally, the gateway is the only component that touches network I/O.
flowchart LR
APP["taktora-executor application<br/>(plugin uses Connector trait)"]
SHM[("iceoryx2 shared memory<br/>+ event service")]
GW["taktora-connector gateway<br/>(tokio + protocol stack)"]
EXT[("external system<br/>e.g. MQTT broker")]
APP -- ConnectorEnvelope --> SHM
SHM -- ConnectorEnvelope --> APP
SHM -- ConnectorEnvelope --> GW
GW -- ConnectorEnvelope --> SHM
GW -- protocol native --> EXT
EXT -- protocol native --> GW
In-process deployment collapses the SHM hop to a single-process shared-memory transport but preserves the same envelope contract; see In-process gateway deployment (ARCH_0020) and Separate-process gateway de... (ARCH_0021). |
4. Solution strategy¶
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. 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
|
5. Building block view¶
The framework decomposes into five workspace crates plus reuse of two existing taktora-executor crates. The decomposition is hierarchical: a level-1 view shows crate-level building blocks; level-2 zooms into the two crates that carry the most logic.
Pure trait definitions and shared types. No IPC, no protocol code.
Public surface: |
Concrete envelope ( |
Concrete |
MQTT plugin ( |
Composition layer. Provides |
Crate-level building blocks and their dependency graph. All edges
point from depender to dependee. The graph is acyclic; the host is
the only consumer of every other new crate. The
flowchart TB
subgraph existing_crates[existing crates]
EX[taktora-executor]
TR[taktora-executor-tracing]
end
subgraph new_crates["new crates (this spec)"]
CO[taktora-connector-core<br/>BB_0001]
TX[taktora-connector-transport-iox<br/>BB_0002]
CD[taktora-connector-codec<br/>BB_0003]
MQ[taktora-connector-mqtt<br/>BB_0004]
EC[taktora-connector-ethercat<br/>BB_0030]
ZE[taktora-connector-zenoh<br/>BB_0040]
HO[taktora-connector-host<br/>BB_0005]
end
CO --> TX
CO --> CD
CO --> MQ
CO --> EC
TX --> MQ
TX --> EC
CD --> MQ
CD --> EC
EX --> TX
EX --> MQ
EX --> EC
CO --> HO
TX --> HO
CD --> HO
MQ --> HO
EC --> HO
CO --> ZE
TX --> ZE
CD --> ZE
EX --> ZE
ZE --> HO
TR -.optional adapter.-> HO
|
The on-wire form. #[repr(C)]
#[derive(Debug, Copy, Clone, ZeroCopySend)]
pub struct ConnectorEnvelope<const N: usize> {
pub sequence_number: u64,
pub timestamp_ns: u64,
pub payload_length: u32,
pub _reserved: u32,
pub correlation_id: [u8; 32],
pub payload: [u8; N],
}
At plan stage, the implementation may substitute a small set of size-tier types (4 KB / 64 KB / 1 MB) for the const-generic variant. The external contract — fixed at service-creation time — is identical either way. |
Derives iceoryx2 service names deterministically from a
out service: taktora.connector.<connector>.<channel>.out
in service: taktora.connector.<connector>.<channel>.in
out event: taktora.connector.<connector>.<channel>.out.evt
in event: taktora.connector.<connector>.<channel>.in.evt
|
|
Hosts |
Two bounded channel pairs that translate between taktora-executor’s
thread (WaitSet driver) and the tokio runtime owning rumqttc.
Outbound = |
EtherCAT plugin ( |
Plugin-side |
Gateway-side executable item that owns the ethercrab |
Module that accepts a static PDO-mapping description per SubDevice
from |
Two bounded channel pairs that translate between taktora-executor’s
WaitSet thread and the tokio runtime owning ethercrab’s
|
Zenoh plugin ( |
Plugin-side |
Gateway-side executable item that owns one |
|
Two bounded channel pairs that translate between taktora-executor’s
WaitSet thread and the tokio runtime owning |
CAN plugin ( |
Plugin-side |
Gateway-side executable item that owns one |
Two bounded channel pairs per owned interface that translate
between taktora-executor’s WaitSet thread and the tokio runtime
owning the SocketCAN sockets. Outbound saturation surfaces as
|
Pure-logic helper that maps the per-interface registry of
inbound |
In-process loopback implementation of |
6. Runtime view¶
Four scenarios cover the connector framework’s externally-observable
behaviour. Each :refines: the requirements that govern its
behaviour and the building blocks that implement it.
The send path is fully zero-copy on the sender’s side: the codec
writes directly into shared memory via
sequenceDiagram
autonumber
participant U as user code
participant W as ChannelWriter
participant P as Publisher (taktora-executor)
participant SHM as iceoryx2 SHM
participant S as Subscriber (gateway)
participant GI as OutboundGatewayItem
participant BR as Tokio bridge
participant MQ as rumqttc::AsyncClient
participant B as Broker
U->>W: writer.send(&value)
W->>P: publisher.loan(|slot| codec.encode(value, slot.payload))
P->>SHM: zero-copy publish + notify
SHM-->>S: WaitSet wakes
S->>GI: ExecutableItem::execute()
GI->>BR: bridge_tx.try_send(payload, routing)
BR-->>MQ: tokio task drains bridge
MQ->>B: client.publish(topic, qos, retained, payload)
B-->>MQ: PUBACK (QoS 1)
|
The receive path mirrors the send path. The gateway’s tokio task pushes incoming protocol-stack messages into an inbound bridge channel; the inbound gateway item resolves the channel by topic match (with wildcard demultiplexing) and re-publishes the envelope into the inbound iceoryx2 service.
sequenceDiagram
autonumber
participant B as Broker
participant MQ as rumqttc::EventLoop
participant BR as Tokio bridge
participant GI as InboundGatewayItem
participant P as Publisher (gateway, in svc)
participant SHM as iceoryx2 SHM
participant S as Subscriber (app)
participant R as ChannelReader
participant U as user code
B->>MQ: PUBLISH (topic, payload)
MQ-->>BR: tokio task pushes (topic, payload) into bridge_in
BR->>GI: taktora-executor Channel wakes item
GI->>GI: resolve channel by topic match (wildcard demux)
GI->>P: publisher.loan(|slot| copy payload, set header)
P->>SHM: zero-copy publish + notify
SHM-->>S: WaitSet wakes
S->>R: reader.try_recv() → Received{ value, header }
R->>U: user code consumes value
|
Every connector implements the same state machine. Concrete connectors may add private sub-states, but the externally-visible variants are exactly four.
stateDiagram-v2
[*] --> Connecting: gateway started
Connecting --> Up: protocol stack reports connected
Up --> Degraded: transient error (e.g. PUBACK timeout)
Degraded --> Up: recovery
Up --> Down: stack-level disconnect
Degraded --> Down: error threshold exceeded
Down --> Connecting: ReconnectPolicy backoff elapses
Connecting --> Down: connect attempt fails
Up --> [*]: shutdown
Down --> [*]: shutdown
Every transition emits a |
Shutdown is downstream of taktora-executor: when
sequenceDiagram
autonumber
participant SIG as SIGINT/SIGTERM
participant EX as taktora-executor WaitSet
participant HO as ConnectorHost / Gateway
participant GI as Gateway items
participant TT as Tokio runtime
participant B as Broker
SIG->>EX: signal delivered
EX->>EX: WaitSet returns Interrupt
EX->>HO: Executor::run() returns
HO->>GI: drop items
HO->>TT: shutdown_handle.send(())
TT->>B: client.disconnect() (best-effort)
B-->>TT: DISCONNECT ack (or timeout)
TT->>TT: tokio runtime drained
HO-->>SIG: process exits
|
Bring-up walks the four EtherCAT bus states. PDO mapping is applied during the PRE-OP → SAFE-OP transition — the only window where SDO writes to the sync-manager assignment indices land on a stable mailbox but the cyclic process image is not yet live. Plugin traffic is accepted only after the bus reaches OP.
sequenceDiagram
autonumber
participant HO as ConnectorHost
participant GW as EthercatGateway
participant EC as ethercrab MainDevice
participant SD as SubDevices
participant PL as Plugin (EthercatConnector)
HO->>GW: start gateway
GW->>EC: PduStorage.try_split + MainDevice::new
GW->>EC: spawn tx_rx_task on tokio sidecar
GW->>EC: init_single_group (INIT to PRE-OP)
EC->>SD: discover and address SubDevices
GW->>SD: SDO writes 0x1C12 / 0x1C13 (PDO mapping)
GW->>EC: group.into_safe_op (PRE-OP to SAFE-OP)
GW->>EC: group.into_op (SAFE-OP to OP)
GW-->>HO: ConnectorHealth::Up
PL->>GW: writer.send / reader.try_recv accepted
|
The gateway runs one
stateDiagram-v2
[*] --> Connecting: gateway started
Connecting --> Up: bus reaches OP and WKC matches
Up --> Degraded: WKC below expected on N consecutive cycles
Degraded --> Up: WKC restored
Up --> Down: bus drops below OP
Degraded --> Down: WKC remains below expected past timeout
Down --> Connecting: ReconnectPolicy backoff elapses
Connecting --> Down: bring-up attempt fails
Up --> [*]: shutdown
Down --> [*]: shutdown
|
When
sequenceDiagram
autonumber
participant GW as EthercatGateway
participant EC as ethercrab MainDevice
participant SD as SubDevices
GW->>EC: read SupportFlags (0x0008) per SubDevice
EC->>SD: BRD 0x0008
SD-->>EC: 64-bit DC support flags
GW->>EC: latch port-0 receive times
EC->>SD: BWR 0x0900 (system time)
loop per DC-capable SubDevice
EC->>SD: read 0x0918 (receive time processing unit)
SD-->>EC: t_recv
EC->>SD: write 0x0920 (system time offset = t_sys - t_recv)
end
GW->>EC: propagate reference clock
EC->>SD: FRMW 0x0910 (first SubDevice clock)
|
The CAN send path is the framework’s zero-copy publish path
(Send path (app → broker) (ARCH_0010)) terminated by a SocketCAN write. The codec
writes envelope payload bytes directly into shared memory via
sequenceDiagram
autonumber
participant U as user code
participant W as ChannelWriter
participant P as Publisher (taktora-executor)
participant SHM as iceoryx2 SHM
participant S as Subscriber (gateway)
participant GI as OutboundGatewayItem
participant BR as Tokio bridge (per-iface outbound)
participant SK as socketcan socket (iface)
participant BUS as CAN bus
U->>W: writer.send(&value)
W->>P: publisher.loan(|slot| codec.encode(value, slot.payload))
P->>SHM: zero-copy publish + notify
SHM-->>S: WaitSet wakes
S->>GI: ExecutableItem::execute()
GI->>BR: bridge_tx.try_send(payload, routing)
BR-->>SK: tokio task drains bridge
SK->>SK: build CanFrame{id, ext, data} or CanFdFrame{id, brs, esi, data}
SK->>BUS: write_frame()
|
The CAN receive path applies the per-interface kernel filter
(compiled by Per-iface filter compiler (... (BB_0074) from the union of registered
readers’
sequenceDiagram
autonumber
participant BUS as CAN bus
participant SK as socketcan socket (iface)
participant RX as RX task (tokio)
participant FC as filter / demux (BB_0074)
participant HC as error classifier (BB_0072)
participant BR as Tokio bridge (per-iface inbound)
participant GI as InboundGatewayItem
participant P as Publisher (gateway, in svc)
participant SHM as iceoryx2 SHM
participant S as Subscriber (app)
participant R as ChannelReader
participant U as user code
BUS->>SK: arbitration + ACK, data frame matches kernel filter
SK->>RX: read_frame() → CanFrame | CanFdFrame | error frame
alt error frame
RX->>HC: classify (passive / warning / bus-off)
HC->>HC: update per-iface sub-state and aggregate via worst-of
else data frame
RX->>FC: lookup matching readers by (can_id, mask, ext)
loop for each matching reader
FC->>BR: enqueue (descriptor, data)
end
BR->>GI: taktora-executor Channel wakes item
GI->>P: publisher.loan(|slot| copy payload bytes, set header)
P->>SHM: zero-copy publish + notify
SHM-->>S: WaitSet wakes
S->>R: reader.try_recv() → Received{ value, header }
R->>U: user code consumes value
end
|
Per-interface sub-state machine driven by error-frame
classification; aggregated into the connector’s single visible
stateDiagram-v2
[*] --> Connecting: iface socket bound, filter applied
Connecting --> Up: first successful read or successful send
Up --> Degraded: error-passive / error-warning frame received
Degraded --> Up: no further error frames for recovery window
Up --> Down: bus-off error frame received
Degraded --> Down: bus-off escalation
Down --> Connecting: ReconnectPolicy backoff elapses, socket reopened, filter re-applied
Connecting --> Down: socket reopen / filter apply fails
Up --> [*]: shutdown
Down --> [*]: shutdown
Aggregation rule (ConnectorHealth aggregates ... (REQ_0630)): any iface |
7. Deployment view¶
The framework supports two deployment shapes from the same envelope contract. Operators choose based on fault-isolation requirements; the plugin’s code is unchanged across both.
One OS process; the host launches both the plugin’s executor and a
tokio task hosting
flowchart LR
subgraph one_process[Single OS process]
direction LR
PLUGIN[Plugin executor<br/>taktora-executor]
GATEWAY[Gateway tokio task<br/>rumqttc + bridge]
SHM[(iceoryx2 SHM)]
PLUGIN <--> SHM <--> GATEWAY
end
BROKER[(MQTT broker)]
GATEWAY <--> BROKER
Pros. Simpler ops (one binary, one signal handler, one log stream). No SHM pool sizing for a peer process. Cons. A panic in the tokio task aborts the application — loses Fault isolation between pro... (QG_0001). Recommended for development, examples, and small deployments where protocol-stack stability is trusted. |
Two OS processes; each runs its own taktora-executor. The plugin’s
process embeds only
flowchart LR
subgraph plugin_proc[Plugin process]
PLUGIN[Plugin executor<br/>taktora-executor]
end
subgraph shm[iceoryx2 SHM]
POOL[(shared memory pool)]
end
subgraph gw_proc[Gateway process]
GATEWAY[Gateway executor + tokio<br/>rumqttc + bridge]
end
PLUGIN <--> POOL <--> GATEWAY
BROKER[(MQTT broker)]
GATEWAY <--> BROKER
Pros. Full fault isolation — a panic in the gateway crashes the
gateway only; the plugin observes |
8. Crosscutting concepts¶
These concepts cut across building blocks and runtime scenarios.
Every connector instance is parameterised on its |
No silent failures: every error class is either returned to the
user or emitted as a |
The gateway is a taktora-executor consumer (Plugin and gateway are both... (ADR_0007)), so
|
Four bounded buffers participate; saturation surfaces explicitly at
each. The framework never silently drops outbound user messages;
inbound is protocol-bounded — drops are reported via
flowchart LR
U[user code] -->|send| W[ChannelWriter]
W -->|loan/publish| SHM["iceoryx2 SHM<br/>(bounded queue)"]
SHM -->|wakes| GI[GatewayItem]
GI -->|try_send| BR1["Tokio bridge OUT<br/>(bounded mpsc)"]
BR1 --> TT[Tokio task]
TT -->|publish| B[Broker]
B -->|publish| TT
TT -->|send| BR2["Tokio bridge IN<br/>(bounded crossbeam)"]
BR2 -->|wakes| GI2[InboundGatewayItem]
GI2 -->|loan/publish| SHM2["iceoryx2 SHM<br/>(bounded queue)"]
SHM2 --> R[ChannelReader]
|
9. Architecture decisions¶
The decisions ADR_0001 through ADR_0010 recorded in
section 4 are the canonical
architecture decision log for this framework. This section is a
needtable view for quick browsing.
ID |
Title |
Status |
Refines |
|---|---|---|---|
Spec scope — framework core + MQTT reference |
open |
||
Umbrella feature is a peer of FEAT_0010 |
open |
||
Both deployment shapes supported |
open |
||
Per-channel envelope size, declared in descriptor |
open |
||
Codec is a generic parameter on the connector |
open |
||
Explicit-builder plugin discovery |
open |
||
Plugin and gateway are both taktora-executor consumers |
open |
||
Routing carried as a typed struct |
open |
||
Lifecycle = ReconnectPolicy + ConnectorHealth |
open |
||
MQTT scope — realistic but bounded |
open |
||
Pre-allocate dispatch scratch at Executor::build time |
open |
||
Compile-time caps + hand-rolled fixed-block bitmap |
open |
||
ethercrab as the EtherCAT MainDevice library |
open |
||
Single MainDevice per gateway |
open |
||
Static PDO mapping declared at build time |
open |
||
Distributed Clocks bring-up is opt-in |
open |
||
Linux raw socket only in first cut |
open |
||
``taktora-connector-ethercat`` module decomposition |
open |
||
Tokio runtime owned by ``EthercatGateway``, joined on Drop |
open |
||
``EthercatConnectorOptions`` is a typed builder; PDO map declared as ``&'static [SubDeviceMap]`` |
open |
||
Verification harness — pure-logic unit tests + env-gated bus tests |
open |
||
Zenoh queries live on a concrete handle type, not the Connector trait |
open |
||
Stack-internal reconnect for Zenoh — no ReconnectPolicy |
open |
||
One ZenohRouting struct carries pub/sub QoS; query knobs on options |
open |
||
Reply framing uses a Zenoh-private 1-byte payload prefix |
open |
||
Process boundary as spatial isolation context |
open |
||
Bounded allocator as spatial-determinism anchor |
open |
||
Fixed-bucket histogram for percentile estimation |
open |
||
Harness as xtask, not CI gate |
open |
||
Parser separated from codegen (strict layering) |
open |
||
Two-trait runtime split (EsiDevice + EsiConfigurable) |
open |
||
PDO assignment alternatives as sum types |
open |
||
Future CANopen support via shared OD IR |
accepted |
||
Vendor extensions captured as opaque blobs |
open |
||
Object dictionary as static table, feature-gated |
open |
||
Use prettyplease, not rustfmt, for emit formatting |
open |
||
cargo subcommand for inspection, not proc-macro |
open |
||
Lift OD IR to fieldbus-od-core now |
open |
||
fieldbus-od-core stays data-only |
open |
||
Re-export from ethercat-esi, do not break it |
open |
||
INI backend choice — serde-derive façade |
open |
||
PDO entry dedup is structural, name-blind |
open |
||
Dummy entries skip into bit offsets, not padding fields |
open |
||
heapless::Vec<u8, 8> for PdoOut payload |
open |
||
Async only on configure, sync on frame path |
open |
||
JSON SDO-dump format with versioned schema |
open |
10. Quality requirements¶
The four quality goals (Fault isolation between pro... (QG_0001)–Uniform observable health a... (QG_0004)) form the root
of the quality tree. Concrete quality scenarios that test them are
authored as test directives in Connector framework — verification —
the verification artefacts are the operational form of the quality
tree. A future spec round may add an explicit quality-tree
architecture element if measurement targets (latency budgets,
throughput) become first-class.
11. Risks and technical debt¶
|
iceoryx2 0.8.x is itself pre-1.0 and changes shape between minor
versions. Mitigation: workspace pins |
|
The taktora-executor↔tokio bridge adds a channel hop on every message in both directions. Mitigation: the bridge stays in the gateway process (not crossing SHM); benchmarks at plan stage characterise added latency; if intolerable, a follow-on can move the rumqttc EventLoop directly onto a taktora-executor item triggered from a raw socket. |
MQTT wildcard subscriptions ( |
12. Glossary¶
A pair of (plugin, gateway) that bridges a taktora-executor application to one external protocol family (MQTT, OPC UA, gRPC, ADS, …). One concrete crate per protocol; all connectors share the framework’s five contracts. |
The in-app side of a connector. A type implementing
|
The out-of-app side of a connector. Hosts the actual protocol
stack (e.g. |
The on-wire form of every message crossing the plugin↔gateway
boundary. |
A type implementing |
A protocol-typed struct ( |
The bounded-channel pair connecting taktora-executor’s WaitSet
thread to the tokio runtime inside a connector crate. Outbound
bridge is |
The four-state observable lifecycle of a connector
( |
A |
A logical bidirectional or unidirectional flow named by
|
Automotive Safety Integrity Level (ISO 26262). Cited only for context in Fault isolation between pro... (QG_0001) — the connector framework is a useful shape for keeping non-deterministic protocol code OUT of an ASIL-rated control loop, but the framework itself makes no safety integrity claims. |
13. Implementations¶
The framework’s building blocks (taktora-connector-core (BB_0001), taktora-connector-transport... (BB_0002),
taktora-connector-codec (BB_0003), taktora-connector-host (BB_0005)) and the EtherCAT reference connector
(taktora-connector-ethercat (BB_0030) and its sub-blocks) ship as five workspace crates.
Each crate has its own impl:: directive recording which BB it
realises, which requirements it refines, and any deviations from the
spec text that needed amendment during implementation.
Crate. Surface.
Tests. TEST_0100 ( |
Crate. Surface.
Tests. Integration tests against real iceoryx2 services: TEST_0120 (round-trip), TEST_0121 (sequence monotonicity), TEST_0122 (timestamp at send), TEST_0123 (correlation id verbatim + default zero), TEST_0125 (payload overflow rejection + no sequence advance). |
Crate. Surface. Decode delegates to Tests. TEST_0110 (round-trip proptest), TEST_0111 (encode
error paths — see the amended Codec encode error on under... (TEST_0111) text routing
buffer-too-small to |
Crate. Surface.
Deviation from :need:`REQ_0273`. The default-off
Tests. Integration test using a minimal in-tree
|
Crate. Status. C5a + C5b land the protocol-agnostic core:
routing, options builder, bridges, health monitor, tokio
runtime lifecycle, C5d takes the second path: defines the C5e lands Surface.
Verification posture. Every REQ covered by IMPL_0050 has
a passing unit / integration test on every CI push.
Tests. Cases pass: TEST_0201 (routing round-trip),
TEST_0204 + TEST_0206 (options builder), TEST_0205-partial
(SDO write sequence shape), TEST_0207 (cycle scheduler
skip-not-catch-up), TEST_0208 (DC opt-in flag), TEST_0209 +
TEST_0210 (WKC policy), TEST_0211-partial (gateway tokio
runtime ownership and clean drop), TEST_0212-0214 (bridge
bounded capacity, BackPressure, DroppedInbound), TEST_0216-
0218 (PDI bit-slice byte-aligned / unaligned round-trips,
adjacent-slice preservation), TEST_0219 (registry
alloc-free iter), TEST_0220 (outbound end-to-end), TEST_0221
(inbound end-to-end), TEST_0222 (loopback round-trip via
mock), plus surface-shape checks for TEST_0200 (Connector
trait surface, |
Crate. Status. Planned surface only — the crate has not been
scaffolded. This directive locks in the public API that the
forthcoming implementation will be measured against. Once the
crate exists, its surface, dependencies, and test coverage are
reconciled against this directive; divergences are recorded as
amendments. Status flips from Surface.
Tests. The corpus authored alongside this directive in
Connector framework — verification includes TEST_0300
( |
Crate. Status. Planned surface only — the crate has not been
scaffolded. This directive locks in the public API that the
forthcoming implementation will be measured against. Once the
crate exists, its surface, dependencies, and test coverage are
reconciled against this directive; divergences are recorded as
amendments. Status flips from Surface.
Tests. The corpus authored alongside this directive in
Connector framework — verification includes TEST_0500
( |
Cross-cutting traceability¶
ID |
Title |
Status |
Implements |
|---|---|---|---|
taktora-connector-core |
open |
||
taktora-connector-transport-iox |
open |
||
taktora-connector-codec |
open |
||
taktora-connector-mqtt |
open |
||
taktora-connector-host |
open |
||
ConnectorEnvelope (sub-block of BB_0002) |
open |
||
ServiceFactory (sub-block of BB_0002) |
open |
||
MqttConnector (sub-block of BB_0004, plugin side) |
open |
||
MqttGateway (sub-block of BB_0004, gateway side) |
open |
||
Tokio bridge (sub-block of BB_0021) |
open |
||
Dispatch scratch (pre-allocated) |
open |
||
taktora-bounded-alloc crate |
open |
||
taktora-connector-ethercat |
open |
||
EthercatConnector (sub-block of BB_0030, plugin side) |
open |
||
EthercatGateway (sub-block of BB_0030, gateway side) |
open |
||
PDO mapping (sub-block of BB_0030) |
open |
||
Tokio bridge for ethercrab (sub-block of BB_0030) |
open |
||
taktora-connector-zenoh |
open |
||
ZenohConnector (sub-block of BB_0040, plugin side) |
open |
||
ZenohGateway (sub-block of BB_0040, gateway side) |
open |
||
Zenoh query handles (sub-block of BB_0041) |
open |
||
Tokio bridge for zenoh (sub-block of BB_0042) |
open |
||
Per-task cycle statistics |
open |
||
Statistics snapshot view |
open |
||
xtask-preempt-rt harness |
open |
||
ethercat-esi (parser crate) |
open |
||
ethercat-esi-codegen (IR + backend trait) |
open |
||
ethercat-esi-codegen-ethercrab (concrete backend) |
open |
||
ethercat-esi-rt (runtime trait crate) |
open |
||
ethercat-esi-build (build.rs glue) |
open |
||
ethercat-esi-cli (cargo subcommand) |
open |
||
ethercat-esi-verify (EEPROM diff tool) |
open |
||
taktora-connector-ethercat EsiDevice adapter |
open |
||
taktora-connector-can crate |
open |
||
CanConnector (sub-block of BB_0070, plugin side) |
open |
||
CanGateway (sub-block of BB_0070, gateway side) |
open |
REQ_0613; REQ_0614; REQ_0620; REQ_0624; REQ_0625; REQ_0630; REQ_0631 |
|
Tokio bridge for CAN (sub-block of BB_0072) |
open |
||
Per-iface filter compiler (sub-block of BB_0072) |
open |
||
MockCanInterface (sub-block of BB_0070) |
open |
||
fieldbus-od-core |
open |
||
canopen-eds parser crate |
open |
||
canopen-eds-codegen |
open |
||
canopen-eds-codegen-taktora |
open |
||
canopen-eds-rt |
open |
||
canopen-eds-build |
open |
||
canopen-eds-cli |
open |
||
canopen-eds-verify |
open |
||
taktora-connector-can adapter (follow-on) |
open |
ID |
Title |
Status |
Refines |
|---|---|---|---|
System context |
open |
||
Level-1 building block decomposition |
open |
BB_0001; BB_0002; BB_0003; BB_0004; BB_0005; BB_0030; BB_0040 |
|
Send path (app → broker) |
open |
||
Receive path (broker → app) |
open |
||
Health and reconnect lifecycle |
open |
||
Shutdown coordination |
open |
||
In-process gateway deployment |
open |
||
Separate-process gateway deployment |
open |
||
Codec — compile-time generic |
open |
||
Error handling — single error type, explicit origins |
open |
||
Observability — Observer + ExecutionMonitor adapter |
open |
||
Back-pressure — explicit at every bounded buffer |
open |
||
EtherCAT bus bring-up sequence |
open |
||
Cyclic process-data exchange and working-counter health |
open |
||
Optional Distributed Clocks bring-up |
open |
||
Toolchain layering (crate dependency graph) |
open |
||
Build-time vs runtime separation |
open |
||
Build-time generation flow |
open |
||
Preop bring-up flow (per device) |
open |
||
Toolchain crate placement in workspace |
open |
||
CAN frame send path (app → bus) |
open |
||
CAN receive path with multi-iface demux |
open |
||
CAN bus health and bus-off recovery |
open |
||
Toolchain layering (crate dependency graph) |
open |
||
Build-time vs runtime separation |
open |