{"id":50596531,"url":"https://github.com/patdhlk/taktora","last_synced_at":"2026-06-28T01:01:28.853Z","repository":{"id":356568655,"uuid":"1233122809","full_name":"patdhlk/taktora","owner":"patdhlk","description":"Rust execution framework on top of iceoryx2","archived":false,"fork":false,"pushed_at":"2026-06-05T13:27:27.000Z","size":1731,"stargazers_count":0,"open_issues_count":6,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-05T14:11:53.342Z","etag":null,"topics":["beckhoff","can","ethercat","executor-runtime","iceoryx2","industrial-automation","industrial-iot","plc","real-time","sdv","shared-memory","zenoh","zero-copy-ipc"],"latest_commit_sha":null,"homepage":"http://taktora.eu","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/patdhlk.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE-APACHE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-08T16:05:49.000Z","updated_at":"2026-06-05T13:24:00.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/patdhlk/taktora","commit_stats":null,"previous_names":["patdhlk/sonic","patdhlk/taktora"],"tags_count":129,"template":false,"template_full_name":null,"purl":"pkg:github/patdhlk/taktora","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patdhlk%2Ftaktora","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patdhlk%2Ftaktora/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patdhlk%2Ftaktora/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patdhlk%2Ftaktora/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/patdhlk","download_url":"https://codeload.github.com/patdhlk/taktora/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patdhlk%2Ftaktora/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33946818,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-05T02:00:06.157Z","response_time":120,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["beckhoff","can","ethercat","executor-runtime","iceoryx2","industrial-automation","industrial-iot","plc","real-time","sdv","shared-memory","zenoh","zero-copy-ipc"],"created_at":"2026-06-05T15:00:12.819Z","updated_at":"2026-06-28T01:01:28.840Z","avatar_url":"https://github.com/patdhlk.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# taktora\n\nA Rust workspace exploring how to build a high-level execution framework and a\nconnector framework on top of [iceoryx2](https://github.com/eclipse-iceoryx/iceoryx2).\n\nFour layered pieces:\n\n- **`taktora-executor`** — items triggered by IPC, intervals, and request/response\n  activity; sequential chains; parallel DAGs; signal/slot; lifecycle observability.\n- **`taktora-connector-*`** — typed channels with codec-pluggable payloads,\n  uniform connector health, and four reference connectors — EtherCAT (driving a\n  SubDevice's process data), Zenoh (pub/sub + query/reply over a Zenoh session),\n  CAN/SocketCAN, and J1939 (SAE J1939 PGN routing, BAM/RTS-CTS/ETP transport\n  protocol, and J1939-81 address claiming layered on CAN) — all exposing the same\n  plugin-facing `ChannelWriter` / `ChannelReader` types every other connector\n  reuses.\n- **EtherCAT build-time codegen toolchains** — turn vendor descriptions into\n  strongly-typed Rust at build time, with zero runtime parsing: a *device-driver*\n  toolchain (ESI XML → typed `EsiDevice` drivers) and a *network-config* toolchain\n  (network YAML → PDO maps / routing tables / validated SM-watchdog\n  config for the connector).\n- **Motion control** — a `no_std`, allocation-free trajectory core (CSP setpoint\n  generation, motion profiles, electronic gearing) and a cyclic NC task that drives\n  CiA 402 drives over a fieldbus-agnostic cyclic process-data seam.\n\n\u003e [!WARNING]\n\u003e **Personal experiment. Not meant for production.**\n\u003e The architecture is sound and the test suite is real, but the API has not\n\u003e stabilised, the published crates are pre-1.0, the `unsafe` story has not been\n\u003e independently audited, and there is no SLA, support, or backwards-compatibility\n\u003e guarantee. Use it to learn from, fork, or vendor in — not to ship.\n\n**Specification:** [https://taktora.dev/](https://taktora.dev/) — built from `spec/` on every push to `main`.\n\n## What's here\n\nThe workspace has grown to 41 library crates (plus per-crate `*-tests` siblings),\ngrouped by layer.\n\n**Execution \u0026 runtime support**\n\n| Crate | Purpose |\n|---|---|\n| [`taktora-executor`](crates/taktora-executor) | The execution core. Items, triggers, executor, runner, channels, services, chains, graphs, signal/slot, observer + execution monitor, optional thread tuning. |\n| [`taktora-executor-tracing`](crates/taktora-executor-tracing) | `Observer` adapter forwarding executor lifecycle and user events to the global `tracing` subscriber. |\n| [`taktora-stats`](crates/taktora-stats) | Allocation-free `no_std` statistics primitives for telemetry: a windowed-percentile rolling histogram (octave buckets) + exact windowed min/max via a monotonic deque. `ADR_0062` / `BB_0053`. |\n| [`taktora-telemetry-export`](crates/taktora-telemetry-export) | Off-RT-thread export of executor `CycleObservation`s: a single-producer/single-consumer overwrite-oldest seqlock ring drained to NDJSON for offline jitter analysis, never blocking the WaitSet thread. `REQ_0110` / `REQ_0111`. |\n| [`taktora-bounded-alloc`](crates/taktora-bounded-alloc) | Static pre-allocated `#[global_allocator]` with hard caps on per-allocation size and total live blocks. `FEAT_0040`. |\n| [`taktora-log`](crates/taktora-log) | Workspace logging facade (`log` crate facade with one-shot init, tracing-log bridge, console dev fallback). See `spec/requirements/logging.rst`. |\n| [`taktora-log-dlt`](crates/taktora-log-dlt) | Pure-Rust AUTOSAR DLT R20-11 backend for `taktora-log`. Talks to a co-located COVESA `dlt-daemon` over UDS (default) or TCP. See `spec/architecture/logging.rst`. |\n| [`taktora-replay`](crates/taktora-replay) | Empty placeholder for an eventual replay coordinator. Do not depend on it. |\n\n**Connector framework + reference connectors**\n\n| Crate | Purpose |\n|---|---|\n| [`taktora-connector-core`](crates/taktora-connector-core) | Framework-level traits and types shared by every connector — `Routing`, `ChannelDescriptor`, `PayloadCodec`, `ConnectorHealth` / `HealthEvent`, `ReconnectPolicy`, `ConnectorError`. `BB_0001`. |\n| [`taktora-connector-transport-iox`](crates/taktora-connector-transport-iox) | iceoryx2-backed `ChannelWriter` / `ChannelReader` + `ConnectorEnvelope` POD wire format + `ServiceFactory`. `BB_0002`. Also ships the additive variable-length, zero-copy `SliceChannelWriter` / `SliceChannelReader` over an iceoryx2 `[u8]` service (segment grows by `PowerOfTwo` up to a configurable ceiling) for bulk payloads like J1939 ETP. `BB_0097` / `FEAT_0097`. |\n| [`taktora-connector-codec`](crates/taktora-connector-codec) | `PayloadCodec` implementations. Ships `JsonCodec`; codec is compile-time-dispatched, so additional codecs are plug-in. `BB_0003`. |\n| [`taktora-connector-host`](crates/taktora-connector-host) | `Connector` trait + `ConnectorHost` / `ConnectorGateway` builders + `HealthSubscription`. The seam at which protocol-specific connectors plug into an `Executor`. `BB_0005`. |\n| [`taktora-connector-ethercat`](crates/taktora-connector-ethercat) | Reference EtherCAT connector. Pluggable `BusDriver` (mock or `ethercrab`), bit-slice PDI routing, gateway-side dispatcher that hops bytes between iceoryx2 and the SubDevice PDI each cycle. `BB_0030` / `FEAT_0041`. |\n| [`taktora-connector-zenoh`](crates/taktora-connector-zenoh) | Reference Zenoh connector. Pluggable `ZenohSessionLike` back-end (mock or `zenoh::Session`), pub/sub + query/reply with timeout-correct sealed-queries handling, peer-count-driven health. `BB_0040` / `FEAT_0042`. |\n| [`taktora-connector-can`](crates/taktora-connector-can) | Reference CAN / SocketCAN connector. `BB_0070` / `FEAT_0046`. |\n| [`taktora-connector-j1939`](crates/taktora-connector-j1939) | Reference SAE J1939 connector layered on `taktora-connector-can`'s driver. 29-bit PGN routing (SA/DA filters), a userspace transport-protocol engine (BAM / RTS-CTS / ETP) and full J1939-81 address claiming, with ETP riding the slice channel. `BB_0098` / `FEAT_0098`. |\n\n**EtherCAT device-driver codegen** — ESI XML → typed drivers at build time (`FEAT_0050`)\n\n| Crate | Purpose |\n|---|---|\n| [`taktora-ethercat-esi`](crates/taktora-ethercat-esi) | Parses EtherCAT ESI (slave information) XML into a faithful typed IR (identity, PDOs, sync managers, mailbox, distributed clocks, object dictionary). |\n| [`taktora-ethercat-esi-rt`](crates/taktora-ethercat-esi-rt) | Runtime contract for generated drivers: the object-safe `EsiDevice` trait + runtime `EsiError`. The only crate generated code links against. |\n| [`taktora-ethercat-esi-codegen`](crates/taktora-ethercat-esi-codegen) | Codegen layer: naming/collision policy, the `CodegenBackend` trait, and the `generate` entry point producing `proc-macro2` token streams. |\n| [`taktora-ethercat-esi-codegen-ethercrab`](crates/taktora-ethercat-esi-codegen-ethercrab) | Concrete backend: emits per-device structs (with a joint `OpMode` enum when an ESI declares multiple PDO assignments) + `EsiDevice` impl (`decode_inputs`/`encode_outputs`), a `pdo_assignment()` accessor, `Identity` consts, and a device registry. |\n| [`taktora-ethercat-esi-build`](crates/taktora-ethercat-esi-build) | `build.rs` glue: glob ESI files → parse → generate → format → `$OUT_DIR`, encoding-aware (ISO-8859-1), with `rerun-if-changed`. |\n| [`taktora-fieldbus-od-core`](crates/taktora-fieldbus-od-core) | Shared object-dictionary IR (`Identity`, `DataType`, dictionary entries) reused across fieldbus device descriptions. |\n\n**EtherCAT network-config codegen** — network YAML → PDO maps / routing tables\n\n| Crate | Purpose |\n|---|---|\n| [`taktora-ethercat-netcfg`](crates/taktora-ethercat-netcfg) | Parser + in-memory IR for the EtherCAT network-config YAML, including config-time validation of the output-slave SM-watchdog bound (AOU_0016: enabled, timeout ≤ FTTI/2). |\n| [`taktora-ethercat-netcfg-codegen`](crates/taktora-ethercat-netcfg-codegen) | Turns the netcfg IR into Rust source (PDO maps, routing tables) for the EtherCAT connector runtime. |\n| [`taktora-ethercat-netcfg-build`](crates/taktora-ethercat-netcfg-build) | `build.rs` glue turning a `network.yaml` into a generated Rust module in `$OUT_DIR`. |\n| [`taktora-ethercat-netcfg-cli`](crates/taktora-ethercat-netcfg-cli) | Command-line front end: expand a `network.yaml` to generated Rust source for inspection. |\n\n**Motion control** — `no_std` CSP setpoint generation + a cyclic NC task\n\n| Crate | Purpose |\n|---|---|\n| [`taktora-motion-core`](crates/taktora-motion-core) | Allocation-free, `no_std` real-time trajectory core: commanded CSP setpoints as bounded, panic-free functions of `(dt, master)` — motion profiles, virtual master, electronic gearing. Owns no I/O, threads, or shared state. `FEAT_0091`. |\n| [`taktora-motion`](crates/taktora-motion) | Cyclic NC task: runs `taktora-motion-core` against CiA 402 CSP drives over a `CyclicFieldbus`. Owns unit-increment scaling, the per-axis power/bumpless/command runtime, the coupling topology, the cyclic step, and a host-side virtual-drive mock. `FEAT_0090`. |\n| [`taktora-motion-proto`](crates/taktora-motion-proto) | POD NC↔commander ABI (`AxisCommand` / `AxisStatus`): `#[repr(C)]`, `Copy`, heap-free types that cross the iceoryx2 boundary as raw bytes (consumer validates every discriminant). `REQ_0855`. |\n| [`taktora-cia402`](crates/taktora-cia402) | Fieldbus-independent CiA 402 profile: statusword/controlword semantics, the per-axis power state machine, and the `Cia402Drive` process-image accessor trait. `no_std`, zero deps. |\n| [`taktora-cyclic-fieldbus`](crates/taktora-cyclic-fieldbus) | The cyclic process-data seam: the `CyclicFieldbus` trait plus per-cycle `Validity` / `CycleQuality`. Implemented by cyclic connectors (EtherCAT, later CANopen), consumed by the NC task. `no_std`. |\n\n## Executor quick start\n\n```rust,no_run\nuse core::time::Duration;\nuse taktora_executor::{item_with_triggers, ControlFlow, Executor};\n\nfn main() -\u003e Result\u003c(), Box\u003cdyn std::error::Error\u003e\u003e {\n    let mut exec = Executor::builder().worker_threads(0).build()?;\n    exec.add(item_with_triggers(\n        |d| { d.interval(Duration::from_secs(1)); Ok(()) },\n        |_| { println!(\"tick\"); Ok(ControlFlow::Continue) },\n    ))?;\n    exec.run()?;\n    Ok(())\n}\n```\n\nPress Ctrl-C; the loop exits cleanly. iceoryx2 catches the signal at the `Node`\nlevel; the WaitSet returns it; the executor honors it. No extra signal-handler\nplumbing on your side.\n\n### What an `ExecutableItem` looks like\n\nThe unit of work the executor schedules. `declare_triggers` registers what\nshould wake it (subscriber arrivals, intervals, deadlines, server requests,\nclient responses, raw listeners). `execute` runs once per wake-up and returns\neither `Continue`, `StopChain`, or an error.\n\n```rust\nstruct MyTask { /* state */ }\n\nimpl taktora_executor::ExecutableItem for MyTask {\n    fn declare_triggers(\n        \u0026mut self,\n        d: \u0026mut taktora_executor::TriggerDeclarer\u003c'_\u003e,\n    ) -\u003e Result\u003c(), taktora_executor::ExecutorError\u003e {\n        d.interval(core::time::Duration::from_millis(100));\n        Ok(())\n    }\n    fn execute(\n        \u0026mut self,\n        _ctx: \u0026mut taktora_executor::Context\u003c'_\u003e,\n    ) -\u003e taktora_executor::ExecuteResult {\n        // do work\n        Ok(taktora_executor::ControlFlow::Continue)\n    }\n}\n```\n\nOr the closure-based path: `taktora_executor::item(closure)` and\n`item_with_triggers(declare_closure, execute_closure)`.\n\n### Publishing options\n\n`Publisher\u003cT\u003e` exposes three send paths with different cost/ergonomics\ntradeoffs. iceoryx2's zero-copy promise holds across the wire in every case\n(the receiver always gets a reference to shared memory); the differences are\non the sender's side:\n\n| Method | Sender-side cost | When to reach for it |\n|---|---|---|\n| `send_copy(value)` | One move into shm | Tiny POD payloads (`u64`, small structs). Simplest. |\n| `loan_send(\\|t\\| ...)` | `T::default()` write + in-place mutation | Medium types where `Default` is cheap. |\n| `loan(\\|slot\\| ...)` | None — closure constructs directly in shm | Large types or types without a sensible `Default`. |\n\nFor large types use `loan` with `MaybeUninit::write(value)` or iceoryx2's\n`placement_default!` macro to get the full zero-copy benefit. The\n[`loan_demo`](crates/taktora-executor/examples/loan_demo.rs) example sends 1 KB\npayloads constructed entirely in shared memory.\n\n### Composition\n\n- `Executor::add(item)` — single item dispatched as one pool job.\n- `Executor::add_chain([head, mid, tail])` — sequential walk; head's triggers\n  gate the chain; `Ok(StopChain)` or `Err` from any item aborts the rest.\n- `Executor::add_graph().vertex(...).edge(...).root(...).build()` — a DAG.\n  Vertices run in parallel on the executor's thread pool when their predecessors\n  complete. The root's triggers gate the graph.\n- `wrap_with_condition(item, predicate)` — gate any item on a runtime check.\n- `signal_slot::pair(\u0026mut exec, topic)` — pre-built `ExecutableItem`s wrapping\n  a `Channel\u003cT\u003e` for chain composition with `before_send`/`after_recv` hooks.\n\nSee `crates/taktora-executor/examples/` for runnable variants of each.\n\n### Observability\n\n- **`Observer`** trait — `on_executor_up/down/error`, `on_app_start/stop/error`,\n  `on_send_event`. No-op default impls; non-blocking. The\n  `taktora-executor-tracing` crate ships a ready-made adapter to the `tracing`\n  ecosystem.\n- **`ExecutionMonitor`** trait — `pre_execute(task, at)` /\n  `post_execute(task, at, took, ok)`. Raw timestamps; build expectations on top.\n\nBoth are configured via `ExecutorBuilder::observer(...)` /\n`ExecutorBuilder::monitor(...)`.\n\n## Connector framework\n\nA protocol-agnostic surface for getting data into and out of an `Executor`.\nEach concrete connector implements the `Connector` trait from\n`taktora-connector-host`:\n\n```rust,ignore\npub trait Connector: Send + 'static {\n    type Routing: Routing;\n    type Codec: PayloadCodec;\n\n    fn name(\u0026self) -\u003e \u0026str;\n    fn health(\u0026self) -\u003e ConnectorHealth;\n    fn subscribe_health(\u0026self) -\u003e HealthSubscription;\n    fn register_with(\u0026mut self, executor: \u0026mut Executor) -\u003e Result\u003c(), ConnectorError\u003e;\n    fn create_writer\u003cT, const N: usize\u003e(\u0026self, descriptor: \u0026ChannelDescriptor\u003cSelf::Routing, N\u003e)\n        -\u003e Result\u003cChannelWriter\u003cT, Self::Codec, N\u003e, ConnectorError\u003e\n    where T: serde::Serialize;\n    fn create_reader\u003cT, const N: usize\u003e(\u0026self, descriptor: \u0026ChannelDescriptor\u003cSelf::Routing, N\u003e)\n        -\u003e Result\u003cChannelReader\u003cT, Self::Codec, N\u003e, ConnectorError\u003e\n    where T: serde::de::DeserializeOwned;\n}\n```\n\nThe plugin side calls `create_writer` / `create_reader` to get typed handles.\nEach handle is backed by an iceoryx2 service; payloads are codec-encoded\non `send` and decoded on `try_recv`. The connector's gateway side moves bytes\nbetween iceoryx2 and the protocol-specific I/O surface.\n\n### EtherCAT reference connector\n\n[`taktora-connector-ethercat`](crates/taktora-connector-ethercat) is the first\nconcrete protocol. After `register_with`, calling `ChannelWriter::send(value)`\non the plugin side causes a bit to flip on the addressed SubDevice's PDI\neach cycle:\n\n1. Plugin's `ChannelWriter::send` encodes the value via the connector's codec\n   and publishes it on an iceoryx2 service.\n2. Gateway-side dispatcher drains the publish, runs `pdi::write_routing` to\n   place the bytes at the channel's `EthercatRouting` (subdevice address +\n   bit offset + bit length), and the cycle's `tx_rx` ships the PDI out.\n3. On the inbound leg, the dispatcher reads the SubDevice's inputs slice,\n   slices out the routing's bits via `pdi::read_routing`, and publishes the\n   raw bytes back on a paired iceoryx2 service.\n4. Plugin's `ChannelReader::try_recv` decodes those bytes.\n\nThe dispatcher is driven by a pluggable `BusDriver`:\n\n- **`MockBusDriver`** — programmable working-counter sequences, configurable\n  per-SubDevice PDI buffers, optional loopback. End-to-end tests (`TEST_0220`\n  / `TEST_0221` / `TEST_0222`) exercise the full iceoryx2 ↔ PDI ↔ iceoryx2\n  hop in CI without hardware.\n- **`EthercrabBusDriver`** (under the `bus-integration` feature) — wraps\n  `ethercrab::MainDevice`, spawns the `tx_rx_task` on the gateway's tokio\n  runtime, drives PRE-OP → SAFE-OP → OP bring-up, applies the configured\n  PDO mapping via SDO writes to `0x1C12` / `0x1C13`. Awaits hardware\n  (`ETHERCAT_TEST_NIC`) for the real-bus tests.\n\nEach plugin-side channel is opened on `\"{descriptor.name()}.out\"` (outbound,\nplugin → gateway → SubDevice outputs PDI) or `\"{descriptor.name()}.in\"`\n(inbound, SubDevice inputs PDI → gateway → plugin). Adjacent routing slices\non the same SubDevice are preserved across writes via bit-level\nread-modify-write.\n\n### Zenoh reference connector\n\n[`taktora-connector-zenoh`](crates/taktora-connector-zenoh) is the second\nconcrete protocol. It exposes pub/sub on the standard `ChannelWriter` /\n`ChannelReader` handles and query/reply through the connector's\n`ZenohQuerier` / `ZenohQueryable` handles, all over a single Zenoh session:\n\n1. Plugin's `ChannelWriter::send` encodes the value via the connector's\n   codec and publishes it on an iceoryx2 service.\n2. Gateway-side dispatcher drains the publish, applies the channel's\n   `ZenohRouting` (key expression, congestion control, reliability,\n   priority), and forwards via the back-end session.\n3. Declared subscribers feed inbound bytes back through the connector to\n   `ChannelReader::try_recv`.\n4. Query traffic flows through `ZenohQuerier::query` and\n   `ZenohQueryable::reply`, with a `sealed_queries` sidecar that\n   coordinates the timeout/late-reply race so a reply arriving after the\n   query's deadline is dropped instead of leaking to the plugin.\n\nThe session back-end is pluggable:\n\n- **`MockZenohSession`** — in-process programmable session. Exposes a\n  `peer_count` knob so health-watcher tests (`REQ_0442`) can drive\n  `Healthy` ↔ `Degraded` transitions deterministically and verify the\n  query lifecycle without a running router.\n- **`RealZenohSession`** (under the `zenoh-integration` feature) — wraps\n  `zenoh::Session` (zenoh 1.x), owns its own tokio runtime, and enables\n  `transport_tcp` so it can open `tcp/...` locators.\n\n`ZenohHealthMonitor` polls the back-end's peer count against a\nconfigurable `min_peers` threshold and emits `HealthEvent`s through the\nsame `subscribe_health` surface every connector exposes.\n\n### J1939 reference connector\n\n[`taktora-connector-j1939`](crates/taktora-connector-j1939) layers SAE J1939 on\ntop of CAN, reusing `taktora-connector-can`'s `CanInterfaceLike` driver\n(`MockCanInterface` for layer-1 tests, the feature-gated `RealCanInterface` for\nlayer-2) and adding its own PGN-aware dispatcher (`ADR_0108`). `J1939Routing`\naddresses a channel by PGN with optional source/destination-address filters and\na transport class:\n\n1. The dispatcher decodes the 29-bit extended identifier into priority / PDU\n   format / PGN / SA / DA (PDU1 vs PDU2 derived from the PF field) and demuxes\n   single-frame PGNs straight to the matching channels.\n2. Multi-packet messages flow through a clock-stepped userspace\n   transport-protocol engine: **BAM** broadcast, **RTS/CTS** connection-mode\n   flow control across multiple windows, and **ETP** for payloads above\n   1785 bytes. The J1939-21 timers (Tr, Th, T1–T4) are enforced and the\n   inbound-session pool is bounded; every timeout or connection abort surfaces\n   as a `HealthEvent` rather than a silent drop.\n3. Bounded traffic (single-frame, BAM/RTS-CTS ≤ 1785 B) rides the fixed-`N`\n   `ConnectorEnvelope\u003cN\u003e` channels; **ETP** rides the variable-length slice\n   channel (`BB_0097`), zero-copy and bounded by a configurable `max_etp_bytes`\n   so reassembly never grows unbounded toward the ~117 MB protocol maximum\n   (`ADR_0109`).\n\nA full J1939-81 address-claim state machine runs per interface: it claims a\nconfigured source address using a 64-bit NAME, arbitrates by NAME priority on\ncontention, falls back to the null address (254) as cannot-claim, answers\nRequest-for-Address-Claimed (PGN 59904), and honours Address-Commanded\n(PGN 65240). Claim state maps onto `ConnectorHealth` (Claiming → `Connecting`,\nClaimed → `Up`, CannotClaim → `Down`) and gates outbound transmission until\nClaimed — `J1939Writer::send` returns `ConnectorError::Down` beforehand\n(`ADR_0110`). A `MockJ1939Interface` over `MockCanInterface` drives the full\nPGN-routing, transport-protocol, and address-claim test pyramid deterministically\non any host OS, with no kernel CAN module.\n\n## EtherCAT codegen toolchains (build-time)\n\nTwo toolchains turn vendor descriptions into strongly-typed Rust entirely in\n`build.rs`, with zero runtime parsing — consumers `include!` the generated module\nand ship no XML/YAML.\n\n- **Device-driver codegen (`FEAT_0050`).** `taktora-ethercat-esi-build` reads a\n  vendor ESI XML file and generates per-device structs implementing the\n  object-safe `EsiDevice` trait (`taktora-ethercat-esi-rt`): typed\n  `decode_inputs` / `encode_outputs` over the SubDevice's process image, an\n  `Identity` const per device, and a registry for identity-based dispatch. Devices\n  that declare multiple `\u003cAlternativeSmMapping\u003e` PDO configurations generate a joint\n  per-device `OpMode` enum — one variant per resolved assignment, each carrying its\n  own exact-length codec — plus a `pdo_assignment()` accessor that yields the active\n  mode's `0x1C12` / `0x1C13` index lists. The parser copes with real-world ESI quirks\n  (localized `\u003cName\u003e`, CDATA, ISO-8859-1, vendor data types). Real Beckhoff terminals\n  (EL1008 / EL2004 / EL3602 / EL7047, …) generate compiling, bit-exact drivers; the\n  [`ethercat-real-bus`](examples/ethercat-real-bus) and\n  [`ethercat-stepper`](examples/ethercat-stepper) examples drive them through\n  generated drivers, validated on real hardware.\n- **Network-config codegen.** `taktora-ethercat-netcfg-build` turns a network\n  `.yaml` into the PDO maps and routing tables the EtherCAT connector consumes,\n  with a `cargo`-style CLI (`taktora-ethercat-netcfg-cli`) for inspection.\n\n## Threading\n\nSingle executor-owned worker pool (M1 model). The thread that calls\n`Executor::run()` becomes the WaitSet driver; pool workers run `execute()`.\nFor parallel graphs, use `worker_threads(N)` with `N \u003e= 2`.\n\n`Runner::new(exec, RunnerFlags::empty())` hosts the executor on a dedicated\nOS thread; `Runner::stop()` joins it and re-throws any item error.\n\nConnectors that need an async I/O loop (e.g. the EtherCAT gateway around\n`ethercrab`) own their own tokio runtime internally — it never leaks into\nthe WaitSet thread.\n\n## Cargo features\n\n| Flag             | Crate | Default | Effect                                     |\n|------------------|---|---------|--------------------------------------------|\n| `tracing`        | `taktora-executor` | off     | Add the `tracing` crate as a dependency for adapter integrations. |\n| `thread_attrs`   | `taktora-executor` | off     | Core-affinity, thread name prefix, and (Linux) `SCHED_FIFO` priority on the executor's worker pool. |\n| `json`           | `taktora-connector-codec` | **on** | `JsonCodec` via `serde_json`. |\n| `bus-integration`| `taktora-connector-ethercat` | off | Pull `ethercrab` and expose `EthercrabBusDriver`. Off by default so consumers that only want the framework types and pure-logic helpers don't pull ethercrab's transitive dependencies. |\n| `zenoh-integration`| `taktora-connector-zenoh` | off | Pull `zenoh` 1.x (with `transport_tcp` enabled) and expose `RealZenohSession`. Off by default so consumers that only want the framework types and `MockZenohSession` don't pull zenoh's transitive dependencies. |\n| `socketcan-integration`| `taktora-connector-can`, `taktora-connector-j1939` | off | Pull the Linux-only `socketcan` stack and expose `RealCanInterface` for real `PF_CAN` I/O. The `taktora-connector-j1939` flag is a passthrough to `taktora-connector-can`'s. Off by default; `MockCanInterface` / `MockJ1939Interface` ship ungated for layer-1 tests on any host OS. |\n\niceoryx2 itself handles SIGINT/SIGTERM natively — no `ctrlc` feature is\nneeded and the loop exits cleanly on either signal.\n\n## Detecting dropped notifications\n\nWhen a publisher sends, iceoryx2 wakes each attached listener via a per-listener\nUnix datagram socket. If the listener's kernel socket buffer is full, that\nspecific notification is dropped (the **data** still goes through the pub/sub\nchannel reliably; only the wakeup is lost). iceoryx2 logs a verbose\n`FailedToDeliverSignal` warning when this happens.\n\n**This usually means the consumer is falling behind.** Either it can't drain\nfast enough, or the producer is bursting faster than the listener's socket\nbuffer can absorb. Lost wakeups can usually be tolerated (the listener will\nstill wake from a *previous* pending notification, drain everything, and\ncatch up), but if you have a deadline-sensitive consumer or zero-buffered\nevent semantics, every drop matters.\n\nThe publisher's send methods return `NotifyOutcome` so callers can detect\nthis programmatically without parsing logs:\n\n```rust,no_run\n# use taktora_executor::Publisher;\n# fn run(publisher: Publisher\u003cu64\u003e) -\u003e Result\u003c(), Box\u003cdyn std::error::Error\u003e\u003e {\nlet outcome = publisher.send_copy(42_u64)?;\nif !outcome.delivered_to_any_listener() {\n    // No listener received the wakeup. Either no subscribers are attached\n    // (normal during startup), or every subscriber's socket was full.\n    eprintln!(\"warn: send dropped, listeners_notified={}\", outcome.listeners_notified);\n}\n# Ok(()) }\n```\n\nIf you want to silence iceoryx2's verbose logging anyway (e.g. in production),\ncall `iceoryx2::prelude::set_log_level(LogLevel::Error)` once at startup. But\ninspect `NotifyOutcome::listeners_notified` first — silencing the log without\nchecking the return is how dropped wakeups become silent bugs.\n\n## Examples\n\nTwo kinds, depending on what you want to see.\n\n### Per-crate examples (cargo workspace)\n\nTight, focused demos that live inside their crate's `examples/`\ndirectory and build from the workspace itself:\n\n| Example | What it shows |\n|---|---|\n| `cargo run -p taktora-executor --example interval_loop`   | one tick per second; Ctrl-C to exit |\n| `cargo run -p taktora-executor --example pubsub_pipeline` | producer + consumer over a `Channel\u003cu64\u003e` |\n| `cargo run -p taktora-executor --example diamond_graph`   | 4-vertex DAG fired by an interval |\n| `cargo run -p taktora-executor --example signal_slot`     | signal/slot pair driven by a chain |\n| `cargo run -p taktora-executor --example loan_demo`       | zero-copy 1 KB payloads via `Publisher::loan` |\n\n### Integration examples (standalone crates)\n\nCross-crate mini-apps under [`examples/`](examples/) that depend on the\npublished `taktora-*` crates from crates.io. Each is its own Cargo\ncrate with its own `Cargo.lock`, so they read like what an external\nuser would write:\n\n| Example | What it shows |\n|---|---|\n| [`zenoh-pubsub-mock`](examples/zenoh-pubsub-mock)   | executor + zenoh connector (`MockZenohSession`), in-process pub/sub |\n| [`zenoh-pubsub-real`](examples/zenoh-pubsub-real)   | same shape over a real `zenoh::Session`; two-terminal peer-to-peer |\n| [`ethercat-mock-loop`](examples/ethercat-mock-loop) | 1 kHz control loop over the EtherCAT connector with PDI bit-slice routing through `MockBusDriver` |\n| [`ethercat-wago-coupler`](examples/ethercat-wago-coupler) | mirrors a WAGO 750-430's 8 inputs to a 750-530's 8 outputs through a 750-354 coupler over a real NIC |\n| [`ethercat-real-bus`](examples/ethercat-real-bus)   | drives a real EK1100 + EL1008 + EL2004 over a Linux NIC (Pi-friendly), decoding inputs / encoding outputs via **ESI-generated typed drivers** |\n| [`ethercat-stepper`](examples/ethercat-stepper)     | drives a Beckhoff EL7047 stepper (Beckhoff **positioning interface**, not CiA 402) off EL1008 button presses with an EL2004 status lamp — all three terminals through ESI-generated typed drivers, the EL7047 in its generated `PositioningInterface` mode |\n\nSee [`examples/README.md`](examples/README.md) for the full index and\nthe local-paths toggle (debugging an in-tree change against an\nexample).\n\n## Building\n\nWorkspace builds on stable Rust (edition 2024, MSRV 1.85). iceoryx2 0.8.x\nis the underlying IPC layer.\n\n```bash\ncargo build --workspace\ncargo test  --workspace --all-features -- --test-threads=1\ncargo clippy --workspace --all-targets --all-features -- -D warnings\n```\n\nPre-push hooks (cargo-clippy, cargo-doc, complexity-gate) mirror the CI jobs.\nInstall with `pre-commit install \u0026\u0026 pre-commit install --hook-type pre-push`.\nPer-tool notes:\n\n- **Complexity gate (optional locally):** install once with\n  `cargo install --locked rust-code-analysis-cli` (the `--locked` is required;\n  a plain `cargo install` fails to build on current toolchains) so the\n  `complexity-gate` pre-push hook runs. Without it the hook self-skips; CI\n  enforces it regardless.\n\nTests run single-threaded in CI because each test creates its own iceoryx2\nservice in shared memory (parallel runs would contend on the same names) and\nthe `CountingAllocator` used by the zero-alloc tests is process-wide.\n\n## Status\n\nThis is **pre-1.0 personal experiment code.** Concretely:\n\n- The API has not been audited by anyone other than the author.\n- The `unsafe` blocks (cross-thread send of iceoryx2 ports, raw-pointer\n  dispatch in the WaitSet callback, raw-pointer envelope construction in\n  the connector transport's zero-copy publish path) are documented but\n  have not been reviewed by an `unsafe`-Rust expert or run under Miri.\n- Several known polish items remain (see the design notes for the punch\n  list); none are correctness-blocking, but the API surface should be\n  considered unstable until they're addressed.\n- iceoryx2 0.8.x is itself pre-1.0 and changes shape between versions;\n  this workspace is pinned to 0.8.x and will need adaptation for later\n  releases.\n- The EtherCAT connector's real-bus path (`bus-integration` feature) is\n  compile-checked only — hardware tests run under `ETHERCAT_TEST_NIC`\n  and are not part of the default CI matrix.\n- The Zenoh connector's real-session path (`zenoh-integration` feature)\n  follows the same model: the test matrix exercises `MockZenohSession`\n  in CI; `RealZenohSession` is compile-checked and validated against a\n  running router out-of-band.\n- The safety concept under [`spec/safety/`](spec/safety) is **SEooC\n  sketch-level**. It captures an assumed item, an illustrative HARA,\n  two assumed safety goals, five AFSRs, ten TSRs allocated across the\n  workspace, a Freedom-From-Interference argument, and a nine-item\n  Assumption-of-Use contract. ASIL D is claimed via ISO 26262-9 §5\n  decomposition (taktora as `ASIL B(D)`, a diverse integrator-supplied\n  monitor as the second `ASIL B(D)`) — the independence argument is\n  **not closed by taktora** and the whole concept has not been assessed.\n- The crates are published to crates.io (pre-1.0, on a release-plz cadence),\n  but there is no support, no SLA, and no backwards-compatibility guarantee.\n\nIf any of those caveats matter for your use case, **don't ship it**.\n\nRead the source, fork it, vendor it, or treat it as a worked example for\nhow to wire iceoryx2 into a higher-level execution framework — but don't\nmistake it for a maintained library.\n\n## License\n\nApache-2.0 OR MIT, at your option.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpatdhlk%2Ftaktora","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpatdhlk%2Ftaktora","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpatdhlk%2Ftaktora/lists"}