Solution strategy¶
arc42 §4 — the toolchain’s shape is the consequence of eight
architectural decisions. Each is captured as an ADR that :refines:
the requirement or feature it answers.
Context. A monolithic “ESI to ethercrab driver” crate would
conflate three concerns — XML parsing, IR shape and naming
policy, and ethercrab-specific code emission. Downstream tools
that need only one of these (a TwinCAT importer, a network
configurator, the EEPROM verifier) would be forced to compile
ethercrab and Decision. Three crates: Consequences. ✅ Each layer has one job and one
reason-to-change. ✅ Future backends (soem, minimal, no_std-only)
are net-additive — no parser or codegen churn.
❌ Three crates instead of one — more |
Context. The hot path (cyclic Decision. Two traits. Consequences. ✅ Hot path stays sync and zero-cost. ✅ Devices that don’t need preop SDO writes don’t need to implement the async trait. ❌ Two trait names instead of one; the consumer-side dispatcher has to handle both shapes. Acceptable cost; the generated code shoulders most of the burden. |
Context. An ESI device commonly declares 2–4 PDO
assignment alternatives (e.g. “Standard 16-bit”, “Compact
8-bit”). One representation in Rust is a single struct with
Decision. Emit an enum Consequences. ✅ “Two alternatives at once” is
unrepresentable. ✅ Per-variant PDO structs have |
Architecture Decision: Joint per-device OpMode enum supersedes per-direction PDO-assignment alternatives ADR_0104
|
Context. PDO assignment alternatives... (ADR_0072) modelled PDO-assignment choice
per-direction, on the assumption that an ESI device offers a small
menu of interchangeable RxPDO / TxPDO alternatives. Two facts about
real Beckhoff ESI break that model. First, Beckhoff marks selectable
PDOs Decision. Resolve each device to a set of selectable
assignments (one per Consequences. ✅ |
Context. EtherCAT’s CoE inherits the CANopen Object Dictionary (CiA 301) wholesale: index/subindex addressing, PDO mapping objects, the data-type table. EDS (CANopen) and ESI (EtherCAT) describe overlapping object dictionaries. Decision. When CANopen support is in scope, the OD IR is
extracted to a shared Consequences. ✅ ~70 % of code is shared at the IR level
when CANopen lands. ✅ Each transport keeps its honest runtime
shape. ❌ Future refactor required to lift OD types out of
Closure. The OD-core lift (Lift OD IR to fieldbus-od-c... (ADR_0078)) is realised by the
|
Context. Vendor-specific Decision. Capture as opaque blobs. The IR carries a
Consequences. ✅ Real-world ESI files parse without bespoke patches per vendor. ✅ Information is preserved, not discarded. ❌ The IR carries a payload nobody on the ethercrab-backend side reads. Negligible cost; the alternative (parse-and-discard) is worse. |
Context. OD-heavy devices (e.g. Beckhoff ELxxx coupling modules with 200+ entries) can balloon generated code by 10–50× if every OD entry becomes a match arm. Three options: match arms (large, fast lookup), static table (smaller, linear lookup but O(log n) with binary search on sorted index), no emission (smallest, fails the “OD-aware diagnostics” use case). Decision. Emit OD entries as a sorted
Consequences. ✅ Default builds carry zero OD-related
code or rodata. ✅ Diagnostic tools that need OD lookup
|
Context. Emitting raw Decision. Consequences. ✅ Build helper has no external command dependency. ✅ Output is stable across rustfmt versions. ❌ Slightly different formatting than the rest of the workspace’s rustfmt-formatted code (prettyplease is opinionated but not 100 % rustfmt-compatible). Acceptable — generated files are not human-edited. |
Context. Two inspection mechanisms were on the table.
Option A: an Decision. Cargo subcommand only. Proc-macro form is rejected per NO proc-macro front-end (REQ_0591). Consequences. ✅ One codegen path; tests run once. ✅ No
proc-macro compile-time cost on every workspace |
Context. no_std + alloc compatible (REQ_0501) and no_std + alloc baseline for... (CON_0013) specified a
Decision. Consequences. no_std + alloc compatible (REQ_0501) is rejected and no_std + alloc baseline for... (CON_0013)’s
scope narrows to the runtime-trait crate (Runtime trait surface (FEAT_0054)),
revisited in its own round. The parser gains located,
|
Context. The runtime-trait surface as first specified carried
three latent contradictions. (1) EsiDevice trait shape (REQ_0530) put
Decision. Resolve all three at the trait crate:
Consequences. ✅ The |
Context. Several ESI constructs describe a space of possible device configurations rather than one concrete configuration: PDO assignment alternatives (multiple PDOs competing for a sync manager), MDP module slots (pluggable terminals chosen per installation), and the SII source data (whose meaning depends on verifier policy). Each parser extension re-raises the same question: should the parser resolve these into something concrete — a flat process image, an effective PDO list, decoded PDI fields? Decision. No. The parser captures what the document declares, structurally and faithfully, and stops there: PDO alternatives are kept per declared element with no bit offsets (IR carries identity, PDO ma... (REQ_0504)), the MDP catalog and slot constraints are kept unexpanded (MDP module catalog and slot... (REQ_0850)), and EEPROM payloads are decoded to bytes but not interpreted (EEPROM (SII source) data ca... (REQ_0849)). Resolution — picking alternatives, expanding a plugged lineup, assembling or checking an SII image — is consumer work (codegen, network configurator, verifier), where the missing inputs (the user’s terminal lineup, the verifier’s policy) actually live. Consequences. ✅ One parse result serves every consumer
regardless of its policy. ✅ Parser tests need no configuration
fixtures, only documents. ✅ Consumer resolution logic is testable
against a stable IR. ❌ Every consumer that needs a process image
must implement (or share) resolution logic — the netcfg modular
round will design that layer against this boundary. Lossless
re-representations (hex → bytes, |