feat(knx): ACK-retransmit knob — retransmit once, disconnect on second miss (036 W4)#144
Merged
Conversation
…6 W1 inbound) Replace the per-message Box<dyn Any> inbound path (DeserializerKind -> produce_any downcast) with a fused IngestFn built in InboundConnectorBuilder::finish() where T is known: deserialize + produce in one typed closure, no erasure crossing and no boxed future per message (Producer::produce is sync + infallible, design 029). - IngestFn / IngestFactoryFn replace DeserializerFn/ContextDeserializerFn/ DeserializerKind and ProducerTrait/ProducerFactoryFn (all deleted) - InboundConnectorLink carries the ingest factory (non-optional; finish() validates the deserializer before registering, unchanged error strings) - Router::route is now a sync fn taking &RuntimeContext (every production caller already passed Some(&ctx); the context-skip branch is unrepresentable with fused closures, its test removed) - collect_inbound_routes returns Vec<(String, IngestFn)> - pump_source / pump_client inbound / ws dispatch drop one .await - delete dead TypedRecord::create_producer_trait Registrar API (with_deserializer/_raw, link_from) is source-compatible; MQTT/KNX/WS builders pass routes opaquely and compile unchanged. Part of design 036 W1 (data-plane de-Any). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ion (036 W1 outbound)
Replace the per-message Box<dyn Any> outbound path (subscribe_any ->
recv_any -> SerializerFn(&dyn Any) downcast, plus topic_any(&dyn Any))
with a fused SerializedSource built in OutboundConnectorBuilder::finish()
where T is known: its readers yield destination + serialized payload
directly (subscribe -> recv -> resolve topic -> serialize, all typed).
- SerializedSource / SerializedReader / SerializedValue / SourceFactoryFn
replace ConsumerTrait/AnyReader/SerializerKind/SerializerFn/
ContextSerializerFn/ConsumerFactoryFn and the erased TopicProviderAny/
TopicProviderWrapper/TopicProviderFn (all deleted; the typed
TopicProvider<T> trait is now stored as-is)
- OutboundRoute is { topic, source, config }; ConnectorLink carries the
source factory (non-optional; finish() validates the serializer before
registering, unchanged error strings; the "skip links without
serializer" branch in collect_outbound_routes is gone)
- RuntimeContext is threaded into SerializedReader::recv per call (026
context serializers), not captured; raw serializers skip the ctx clone
- Buffer errors propagate unchanged (BufferLagged => pump continues,
other => pump stops); serialize failures are logged and skipped inside
the reader, observably identical to the old pump-side continue
- pump_sink / pump_client outbound collapse to recv + publish
What disappears per message: the Box<dyn Any> allocation, two downcasts,
the topic_any erasure crossing, and the subscribe_any boxed future. The
one remaining Box::pin per recv is the object-safe-async cost that
already existed.
Registrar API (with_serializer/_raw, with_topic_provider, link_to) is
source-compatible; the KNX fake-gateway and session smoke tests pass
unmodified.
Part of design 036 W1 (data-plane de-Any).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…asure (036 W1 wrap-up) - delete SerializeError::TypeMismatch — both constructors died with the W1 fusion (the downcasts are gone); DbError::TypeMismatch is unrelated and stays - delete dead ConnectorClient (held Arc<dyn Any>, zero users) and OutboundConnectorLink (zero users) - document on JoinTrigger why the join fan-in deliberately keeps its Box<dyn Any + Send> (the erasure is the multi-type join API) - CHANGELOG entries (global + aimdb-core + websocket-connector) - check in design docs 034/035/036 and amend the 036 W1 acceptance grep: DynBuffer::as_any and the session auth ext slots are setup-time hits the original literal grep missed Part of design 036 W1 (data-plane de-Any). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AnyRecord carried ~22 methods spanning four concerns; every remote-access or profiling change churned the core storage contract. Split by consumer: - AnyRecord (6 methods): storage/lifecycle only — validate, as_any(_mut), drain_config_errors, set_writable_erased, plus the cfg-gated json_access() accessor so the remote-access gate lives in one place. - RecordIntrospect (supertrait): graph/metadata introspection consumed by the builder's dependency-graph construction, route collection, and list_records. - JsonRecordAccess (cfg remote-access): latest_json / subscribe_json / set_from_json, reached via json_access(); the runtime .with_remote_access() checks stay inside the methods, so behavior is unchanged. Gate is `remote-access` (the 036 sketch said json-serialize). - RecordMetricsReset (supertrait): the cfg-gated no-op-default resets. Supertrait wiring keeps every dyn AnyRecord call site compiling unchanged; only the four JSON call sites switch to the accessor. Registry storage stays Vec<Box<dyn AnyRecord>>. Drops the dead outbound_connector_urls (cfg std) — zero callers in-tree and in aimdb-pro. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…036 W5) intern() leaked a fresh allocation on every call, so re-interning the same key leaked again. A global dedup table (std Mutex / spin Mutex, same pattern as the TypedRecord field locks) now returns the existing 'static allocation for a known key, bounding the leak by the number of *distinct* dynamic keys — the actual lifetime contract of a record key. The contract is documented loudly on intern(), and the debug-build tripwire now counts distinct keys instead of calls. The &'static str / Copy design stays; an Arc<str> key variant remains rejected (forks RecordKey into two shapes — the 034 §3.1 mistake). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion (036 W6) The 18-line no-op #[defmt::global_logger] + panic handler + host time driver block existed in three copies (embassy-adapter session_smoke, embassy-adapter buffer.rs tests, serial-connector embassy_smoke). Per 035 §2.4, an exported host_test_stubs! macro in aimdb-embassy-adapter now holds the single definition; each test binary expands it once, preserving the once-per-binary requirement of #[global_logger]/time_driver_impl!. The time driver uses the wake_by_ref variant (buffer.rs's superset behavior: zero-duration sleeps complete; the smokes never sleep). The serial-connector defmt/embassy-time-driver dev-deps stay — the expansion references them at the invocation site; the Cargo.toml comment now says so. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, W7 skipped - W3: Nucleo firmware build verified (thumbv8m.main-none-eabihf); single run on a main build is the bar now that #140 is merged. Bench session is the remaining work and the gate for closing 035. - W4: deferred pending W3 scenario-1 AckTimeout evidence (decision 2026-06-12). Design pre-decided for the trigger: buffer the sent frame (option a) — GroupWrite already carries a 254-byte APDU buffer, so the semantic-content variant saves ~350 B total, no real RAM argument. - W7: skipped entirely (decision 2026-06-12); no audit will be run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d miss (036 W4) KNXnet/IP 3.8.4 says: repeat an unACKed TUNNELING_REQUEST once after the timeout, then tear the connection down on the second miss. The engine previously expired and warned only — hardware bench evidence (W3, 2026-06-12) showed ten button-press writes silently lost during a link outage's heartbeat-detection window (~65 s). TunnelConfig gains ack_retransmits (default 1 = spec behavior; 0 = the old expire-and-warn for the previous semantics). The pending-ACK slot buffers the sent frame (option (a) per the 036 W4 decision record: GroupWrite already carries a 254-byte APDU, so rebuilding from semantic content saves ~350 B total — not worth the rebuild path). On expiry with retries left the identical frame is re-sent (same sequence counter) and the timer re-armed; on final expiry AckTimeout is reported and the engine disconnects so queued commands flush after the re-handshake instead of vanishing into a dead tunnel. Eviction on map overflow stays warn-only (overflow is not confirmed loss). Behavior change at default config: a tunnel with a persistently unanswered write now reconnects within ~2× ack_timeout_ms instead of staying connected. Retransmit delay is ack_timeout_ms (3 s, the pre-engine constant); strict spec timing is one config knob away (ack_timeout_ms = 1_000). Tests: engine units (identical-frame retransmit, ack-after-retransmit, disconnect on second miss, legacy mode pinned at ack_retransmits=0) + fake-gateway test dropping the first ACK and asserting the byte-identical repeat and tunnel survival. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dings Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… W3 scope Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jun 12, 2026
lxsaah
added a commit
that referenced
this pull request
Jun 13, 2026
…#145) * refactor(core)!: fuse inbound deserialize+produce at registration (036 W1 inbound) Replace the per-message Box<dyn Any> inbound path (DeserializerKind -> produce_any downcast) with a fused IngestFn built in InboundConnectorBuilder::finish() where T is known: deserialize + produce in one typed closure, no erasure crossing and no boxed future per message (Producer::produce is sync + infallible, design 029). - IngestFn / IngestFactoryFn replace DeserializerFn/ContextDeserializerFn/ DeserializerKind and ProducerTrait/ProducerFactoryFn (all deleted) - InboundConnectorLink carries the ingest factory (non-optional; finish() validates the deserializer before registering, unchanged error strings) - Router::route is now a sync fn taking &RuntimeContext (every production caller already passed Some(&ctx); the context-skip branch is unrepresentable with fused closures, its test removed) - collect_inbound_routes returns Vec<(String, IngestFn)> - pump_source / pump_client inbound / ws dispatch drop one .await - delete dead TypedRecord::create_producer_trait Registrar API (with_deserializer/_raw, link_from) is source-compatible; MQTT/KNX/WS builders pass routes opaquely and compile unchanged. Part of design 036 W1 (data-plane de-Any). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(core)!: fuse outbound subscribe+serialize+topic at registration (036 W1 outbound) Replace the per-message Box<dyn Any> outbound path (subscribe_any -> recv_any -> SerializerFn(&dyn Any) downcast, plus topic_any(&dyn Any)) with a fused SerializedSource built in OutboundConnectorBuilder::finish() where T is known: its readers yield destination + serialized payload directly (subscribe -> recv -> resolve topic -> serialize, all typed). - SerializedSource / SerializedReader / SerializedValue / SourceFactoryFn replace ConsumerTrait/AnyReader/SerializerKind/SerializerFn/ ContextSerializerFn/ConsumerFactoryFn and the erased TopicProviderAny/ TopicProviderWrapper/TopicProviderFn (all deleted; the typed TopicProvider<T> trait is now stored as-is) - OutboundRoute is { topic, source, config }; ConnectorLink carries the source factory (non-optional; finish() validates the serializer before registering, unchanged error strings; the "skip links without serializer" branch in collect_outbound_routes is gone) - RuntimeContext is threaded into SerializedReader::recv per call (026 context serializers), not captured; raw serializers skip the ctx clone - Buffer errors propagate unchanged (BufferLagged => pump continues, other => pump stops); serialize failures are logged and skipped inside the reader, observably identical to the old pump-side continue - pump_sink / pump_client outbound collapse to recv + publish What disappears per message: the Box<dyn Any> allocation, two downcasts, the topic_any erasure crossing, and the subscribe_any boxed future. The one remaining Box::pin per recv is the object-safe-async cost that already existed. Registrar API (with_serializer/_raw, with_topic_provider, link_to) is source-compatible; the KNX fake-gateway and session smoke tests pass unmodified. Part of design 036 W1 (data-plane de-Any). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(core)!: drop unrepresentable error surface; document join erasure (036 W1 wrap-up) - delete SerializeError::TypeMismatch — both constructors died with the W1 fusion (the downcasts are gone); DbError::TypeMismatch is unrelated and stays - delete dead ConnectorClient (held Arc<dyn Any>, zero users) and OutboundConnectorLink (zero users) - document on JoinTrigger why the join fan-in deliberately keeps its Box<dyn Any + Send> (the erasure is the multi-type join API) - CHANGELOG entries (global + aimdb-core + websocket-connector) - check in design docs 034/035/036 and amend the 036 W1 acceptance grep: DynBuffer::as_any and the session auth ext slots are setup-time hits the original literal grep missed Part of design 036 W1 (data-plane de-Any). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(tests): clean up use statements and formatting in tests * docs(design): mark 036 W1 implemented in PR #141 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(core)!: split AnyRecord into capability traits (036 W2) AnyRecord carried ~22 methods spanning four concerns; every remote-access or profiling change churned the core storage contract. Split by consumer: - AnyRecord (6 methods): storage/lifecycle only — validate, as_any(_mut), drain_config_errors, set_writable_erased, plus the cfg-gated json_access() accessor so the remote-access gate lives in one place. - RecordIntrospect (supertrait): graph/metadata introspection consumed by the builder's dependency-graph construction, route collection, and list_records. - JsonRecordAccess (cfg remote-access): latest_json / subscribe_json / set_from_json, reached via json_access(); the runtime .with_remote_access() checks stay inside the methods, so behavior is unchanged. Gate is `remote-access` (the 036 sketch said json-serialize). - RecordMetricsReset (supertrait): the cfg-gated no-op-default resets. Supertrait wiring keeps every dyn AnyRecord call site compiling unchanged; only the four JSON call sites switch to the accessor. Registry storage stays Vec<Box<dyn AnyRecord>>. Drops the dead outbound_connector_urls (cfg std) — zero callers in-tree and in aimdb-pro. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(design): mark 036 W2 implemented in PR #142 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(core): dedup StringKey::intern; document the leak contract (036 W5) intern() leaked a fresh allocation on every call, so re-interning the same key leaked again. A global dedup table (std Mutex / spin Mutex, same pattern as the TypedRecord field locks) now returns the existing 'static allocation for a known key, bounding the leak by the number of *distinct* dynamic keys — the actual lifetime contract of a record key. The contract is documented loudly on intern(), and the debug-build tripwire now counts distinct keys instead of calls. The &'static str / Copy design stays; an Arc<str> key variant remains rejected (forks RecordKey into two shapes — the 034 §3.1 mistake). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(tests): host_test_stubs! macro for the defmt logger triplication (036 W6) The 18-line no-op #[defmt::global_logger] + panic handler + host time driver block existed in three copies (embassy-adapter session_smoke, embassy-adapter buffer.rs tests, serial-connector embassy_smoke). Per 035 §2.4, an exported host_test_stubs! macro in aimdb-embassy-adapter now holds the single definition; each test binary expands it once, preserving the once-per-binary requirement of #[global_logger]/time_driver_impl!. The time driver uses the wake_by_ref variant (buffer.rs's superset behavior: zero-duration sleeps complete; the smokes never sleep). The serial-connector defmt/embassy-time-driver dev-deps stay — the expansion references them at the invocation site; the Cargo.toml comment now says so. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(design): mark 036 W5+W6 implemented in PR #143 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(design): 036 status round — W3 prep done, W4 deferred on W3 data, W7 skipped - W3: Nucleo firmware build verified (thumbv8m.main-none-eabihf); single run on a main build is the bar now that #140 is merged. Bench session is the remaining work and the gate for closing 035. - W4: deferred pending W3 scenario-1 AckTimeout evidence (decision 2026-06-12). Design pre-decided for the trigger: buffer the sent frame (option a) — GroupWrite already carries a 254-byte APDU buffer, so the semantic-content variant saves ~350 B total, no real RAM argument. - W7: skipped entirely (decision 2026-06-12); no audit will be run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(knx): ACK-retransmit knob — retransmit once, disconnect on second miss (036 W4) KNXnet/IP 3.8.4 says: repeat an unACKed TUNNELING_REQUEST once after the timeout, then tear the connection down on the second miss. The engine previously expired and warned only — hardware bench evidence (W3, 2026-06-12) showed ten button-press writes silently lost during a link outage's heartbeat-detection window (~65 s). TunnelConfig gains ack_retransmits (default 1 = spec behavior; 0 = the old expire-and-warn for the previous semantics). The pending-ACK slot buffers the sent frame (option (a) per the 036 W4 decision record: GroupWrite already carries a 254-byte APDU, so rebuilding from semantic content saves ~350 B total — not worth the rebuild path). On expiry with retries left the identical frame is re-sent (same sequence counter) and the timer re-armed; on final expiry AckTimeout is reported and the engine disconnects so queued commands flush after the re-handshake instead of vanishing into a dead tunnel. Eviction on map overflow stays warn-only (overflow is not confirmed loss). Behavior change at default config: a tunnel with a persistently unanswered write now reconnects within ~2× ack_timeout_ms instead of staying connected. Retransmit delay is ack_timeout_ms (3 s, the pre-engine constant); strict spec timing is one config knob away (ack_timeout_ms = 1_000). Tests: engine units (identical-frame retransmit, ack-after-retransmit, disconnect on second miss, legacy mode pinned at ack_retransmits=0) + fake-gateway test dropping the first ACK and asserting the byte-identical repeat and tunnel survival. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(design): mark 036 W4 implemented in PR #144; record W3 bench findings Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(design): record W4 hardware validation (partial) and pre-release W3 scope Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: fix AimX v1/v2 rot, dead spec links, and removed-API references Doc-rot fixes from the post-036 debt scan; no wire or API behavior changes (the one wire-visible byte change — the client hello version — is corrected from a stale "1.0" to the "2.0" the server already announces; the server never validates it). - remote::PROTOCOL_VERSION corrected to "2.0", documented, and exported; the AimX dispatch Welcome and aimdb-client both use it now, so client and server can no longer drift. The dead, never-exported v1 untagged Message envelope and its helpers are deleted. - remote module docs: "AimX v1" + link to the nonexistent docs/design/remote-access/aimx-v1.md replaced with the v2 NDJSON tagged-frame description pointing at session::aimx and remote-access-via-connectors.md; stale .build()? example fixed to the (db, runner) = build().await? shape. - connector module docs: removed-`.link()` API replaced with the real configure/link_to pattern (the old example used a RecordConfig::builder API that never existed); ConnectorUrl no longer advertises Kafka/HTTP connector semantics for connectors that don't exist — documented as scheme-agnostic with real schemes (mqtt, knx, ws, uds, serial). - builder.rs AimDb example fixed: register_record returns &mut Self, so the old chained .build() could not compile. - aimdb-client README rewritten to match reality (AimxConnection, v2 wire, endpoint URLs incl. serial); crate doc + aimdb-cli doc updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
lxsaah
added a commit
that referenced
this pull request
Jun 13, 2026
…#146) (#147) * refactor(core)!: fuse inbound deserialize+produce at registration (036 W1 inbound) Replace the per-message Box<dyn Any> inbound path (DeserializerKind -> produce_any downcast) with a fused IngestFn built in InboundConnectorBuilder::finish() where T is known: deserialize + produce in one typed closure, no erasure crossing and no boxed future per message (Producer::produce is sync + infallible, design 029). - IngestFn / IngestFactoryFn replace DeserializerFn/ContextDeserializerFn/ DeserializerKind and ProducerTrait/ProducerFactoryFn (all deleted) - InboundConnectorLink carries the ingest factory (non-optional; finish() validates the deserializer before registering, unchanged error strings) - Router::route is now a sync fn taking &RuntimeContext (every production caller already passed Some(&ctx); the context-skip branch is unrepresentable with fused closures, its test removed) - collect_inbound_routes returns Vec<(String, IngestFn)> - pump_source / pump_client inbound / ws dispatch drop one .await - delete dead TypedRecord::create_producer_trait Registrar API (with_deserializer/_raw, link_from) is source-compatible; MQTT/KNX/WS builders pass routes opaquely and compile unchanged. Part of design 036 W1 (data-plane de-Any). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(core)!: fuse outbound subscribe+serialize+topic at registration (036 W1 outbound) Replace the per-message Box<dyn Any> outbound path (subscribe_any -> recv_any -> SerializerFn(&dyn Any) downcast, plus topic_any(&dyn Any)) with a fused SerializedSource built in OutboundConnectorBuilder::finish() where T is known: its readers yield destination + serialized payload directly (subscribe -> recv -> resolve topic -> serialize, all typed). - SerializedSource / SerializedReader / SerializedValue / SourceFactoryFn replace ConsumerTrait/AnyReader/SerializerKind/SerializerFn/ ContextSerializerFn/ConsumerFactoryFn and the erased TopicProviderAny/ TopicProviderWrapper/TopicProviderFn (all deleted; the typed TopicProvider<T> trait is now stored as-is) - OutboundRoute is { topic, source, config }; ConnectorLink carries the source factory (non-optional; finish() validates the serializer before registering, unchanged error strings; the "skip links without serializer" branch in collect_outbound_routes is gone) - RuntimeContext is threaded into SerializedReader::recv per call (026 context serializers), not captured; raw serializers skip the ctx clone - Buffer errors propagate unchanged (BufferLagged => pump continues, other => pump stops); serialize failures are logged and skipped inside the reader, observably identical to the old pump-side continue - pump_sink / pump_client outbound collapse to recv + publish What disappears per message: the Box<dyn Any> allocation, two downcasts, the topic_any erasure crossing, and the subscribe_any boxed future. The one remaining Box::pin per recv is the object-safe-async cost that already existed. Registrar API (with_serializer/_raw, with_topic_provider, link_to) is source-compatible; the KNX fake-gateway and session smoke tests pass unmodified. Part of design 036 W1 (data-plane de-Any). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(core)!: drop unrepresentable error surface; document join erasure (036 W1 wrap-up) - delete SerializeError::TypeMismatch — both constructors died with the W1 fusion (the downcasts are gone); DbError::TypeMismatch is unrelated and stays - delete dead ConnectorClient (held Arc<dyn Any>, zero users) and OutboundConnectorLink (zero users) - document on JoinTrigger why the join fan-in deliberately keeps its Box<dyn Any + Send> (the erasure is the multi-type join API) - CHANGELOG entries (global + aimdb-core + websocket-connector) - check in design docs 034/035/036 and amend the 036 W1 acceptance grep: DynBuffer::as_any and the session auth ext slots are setup-time hits the original literal grep missed Part of design 036 W1 (data-plane de-Any). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(tests): clean up use statements and formatting in tests * docs(design): mark 036 W1 implemented in PR #141 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(core)!: split AnyRecord into capability traits (036 W2) AnyRecord carried ~22 methods spanning four concerns; every remote-access or profiling change churned the core storage contract. Split by consumer: - AnyRecord (6 methods): storage/lifecycle only — validate, as_any(_mut), drain_config_errors, set_writable_erased, plus the cfg-gated json_access() accessor so the remote-access gate lives in one place. - RecordIntrospect (supertrait): graph/metadata introspection consumed by the builder's dependency-graph construction, route collection, and list_records. - JsonRecordAccess (cfg remote-access): latest_json / subscribe_json / set_from_json, reached via json_access(); the runtime .with_remote_access() checks stay inside the methods, so behavior is unchanged. Gate is `remote-access` (the 036 sketch said json-serialize). - RecordMetricsReset (supertrait): the cfg-gated no-op-default resets. Supertrait wiring keeps every dyn AnyRecord call site compiling unchanged; only the four JSON call sites switch to the accessor. Registry storage stays Vec<Box<dyn AnyRecord>>. Drops the dead outbound_connector_urls (cfg std) — zero callers in-tree and in aimdb-pro. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(design): mark 036 W2 implemented in PR #142 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(core): dedup StringKey::intern; document the leak contract (036 W5) intern() leaked a fresh allocation on every call, so re-interning the same key leaked again. A global dedup table (std Mutex / spin Mutex, same pattern as the TypedRecord field locks) now returns the existing 'static allocation for a known key, bounding the leak by the number of *distinct* dynamic keys — the actual lifetime contract of a record key. The contract is documented loudly on intern(), and the debug-build tripwire now counts distinct keys instead of calls. The &'static str / Copy design stays; an Arc<str> key variant remains rejected (forks RecordKey into two shapes — the 034 §3.1 mistake). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(tests): host_test_stubs! macro for the defmt logger triplication (036 W6) The 18-line no-op #[defmt::global_logger] + panic handler + host time driver block existed in three copies (embassy-adapter session_smoke, embassy-adapter buffer.rs tests, serial-connector embassy_smoke). Per 035 §2.4, an exported host_test_stubs! macro in aimdb-embassy-adapter now holds the single definition; each test binary expands it once, preserving the once-per-binary requirement of #[global_logger]/time_driver_impl!. The time driver uses the wake_by_ref variant (buffer.rs's superset behavior: zero-duration sleeps complete; the smokes never sleep). The serial-connector defmt/embassy-time-driver dev-deps stay — the expansion references them at the invocation site; the Cargo.toml comment now says so. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(design): mark 036 W5+W6 implemented in PR #143 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(design): 036 status round — W3 prep done, W4 deferred on W3 data, W7 skipped - W3: Nucleo firmware build verified (thumbv8m.main-none-eabihf); single run on a main build is the bar now that #140 is merged. Bench session is the remaining work and the gate for closing 035. - W4: deferred pending W3 scenario-1 AckTimeout evidence (decision 2026-06-12). Design pre-decided for the trigger: buffer the sent frame (option a) — GroupWrite already carries a 254-byte APDU buffer, so the semantic-content variant saves ~350 B total, no real RAM argument. - W7: skipped entirely (decision 2026-06-12); no audit will be run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(knx): ACK-retransmit knob — retransmit once, disconnect on second miss (036 W4) KNXnet/IP 3.8.4 says: repeat an unACKed TUNNELING_REQUEST once after the timeout, then tear the connection down on the second miss. The engine previously expired and warned only — hardware bench evidence (W3, 2026-06-12) showed ten button-press writes silently lost during a link outage's heartbeat-detection window (~65 s). TunnelConfig gains ack_retransmits (default 1 = spec behavior; 0 = the old expire-and-warn for the previous semantics). The pending-ACK slot buffers the sent frame (option (a) per the 036 W4 decision record: GroupWrite already carries a 254-byte APDU, so rebuilding from semantic content saves ~350 B total — not worth the rebuild path). On expiry with retries left the identical frame is re-sent (same sequence counter) and the timer re-armed; on final expiry AckTimeout is reported and the engine disconnects so queued commands flush after the re-handshake instead of vanishing into a dead tunnel. Eviction on map overflow stays warn-only (overflow is not confirmed loss). Behavior change at default config: a tunnel with a persistently unanswered write now reconnects within ~2× ack_timeout_ms instead of staying connected. Retransmit delay is ack_timeout_ms (3 s, the pre-engine constant); strict spec timing is one config knob away (ack_timeout_ms = 1_000). Tests: engine units (identical-frame retransmit, ack-after-retransmit, disconnect on second miss, legacy mode pinned at ack_retransmits=0) + fake-gateway test dropping the first ACK and asserting the byte-identical repeat and tunnel survival. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(design): mark 036 W4 implemented in PR #144; record W3 bench findings Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(design): record W4 hardware validation (partial) and pre-release W3 scope Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: fix AimX v1/v2 rot, dead spec links, and removed-API references Doc-rot fixes from the post-036 debt scan; no wire or API behavior changes (the one wire-visible byte change — the client hello version — is corrected from a stale "1.0" to the "2.0" the server already announces; the server never validates it). - remote::PROTOCOL_VERSION corrected to "2.0", documented, and exported; the AimX dispatch Welcome and aimdb-client both use it now, so client and server can no longer drift. The dead, never-exported v1 untagged Message envelope and its helpers are deleted. - remote module docs: "AimX v1" + link to the nonexistent docs/design/remote-access/aimx-v1.md replaced with the v2 NDJSON tagged-frame description pointing at session::aimx and remote-access-via-connectors.md; stale .build()? example fixed to the (db, runner) = build().await? shape. - connector module docs: removed-`.link()` API replaced with the real configure/link_to pattern (the old example used a RecordConfig::builder API that never existed); ConnectorUrl no longer advertises Kafka/HTTP connector semantics for connectors that don't exist — documented as scheme-agnostic with real schemes (mqtt, knx, ws, uds, serial). - builder.rs AimDb example fixed: register_record returns &mut Self, so the old chained .build() could not compile. - aimdb-client README rewritten to match reality (AimxConnection, v2 wire, endpoint URLs incl. serial); crate doc + aimdb-cli doc updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: purge never-compiled doc examples — compile, delete, or justify (#146) Resolves the ignored-doctest debt from the post-036 scan with the revised policy (maintainer decision on #146): remove `rust,ignore` examples unless there is a good reason to keep one. Result: 108 ignored fences → 24, and every survivor carries an explicit "Illustrative (not compiled: …)" reason. - Converted to compiled doctests (`no_run`/`rust`) where the crate's deps allow it: the crate-level quick starts of aimdb-sync, aimdb-uds-connector, aimdb-websocket-connector, aimdb-mqtt-connector, aimdb-knx-connector, aimdb-persistence(+sqlite); core's producer/consumer module examples, TopicProvider, extensions, AimxConfig, RecordKey Hash-contract; the websocket AuthHandler and MQTT link-ext trait docs. ~30 examples now compile under `make test` (186 doctests passing suite-wide). - Conversion surfaced real rot, now fixed in the surviving examples: key-less `configure()`/`producer()`/`consumer()` calls (API takes a key), builder chains that can't compile (`&mut Self` → by-value `build()`), `with_buffer`/`finish()` on the wrong receiver, a deserializer returning `Box<T>` instead of `T`, sync's `AimDbSyncExt` example calling the async `build()` synchronously, and `DbError::RecordNotFound` vs `RecordKeyNotFound`. - Deleted ~50 method-level fragments that restated the signature (builder key-access helpers, registrar setters, connector constructors, internal SPI sketches) plus duplicates of the crate-level examples. - Kept 24 as `ignore`, each with a written reason: downstream-crate types core cannot depend on (adapter/connector wiring, `.buffer()` ext trait), proc-macro expansion targeting aimdb-core (circular dev-dependency), embedded/wasm-only code (Embassy peripherals, `#[wasm_bindgen]`), and macro grammar sketches with placeholder types. - Dev-deps: aimdb-uds-connector gains serde/serde_json for its quick start; aimdb-sync's example uses its existing aimdb-tokio-adapter dependency. Closes #146. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: remove deprecated settings.json file --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements W4 from 036-followup-refactoring.md. The deferral trigger fired during the W3 hardware bench (2026-06-12): ten button-press GroupWrites issued during a link outage's heartbeat-detection window (~65 s) were sent into the void, expired with warn-only
AckTimeouts, and were silently lost — the exact gap KNXnet/IP 3.8.4's retransmit-then-disconnect rule exists to close.TunnelConfig::ack_retransmits(new, default1= spec behavior): on ACK expiry the engine re-sends the byte-identical frame (same sequence counter) and re-arms; when the repeat also misses, it reportsAction::AckTimeoutand disconnects, so subsequent commands queue and flush after the re-handshake instead of vanishing.0restores the previous expire-and-warn semantics exactly (pinned by the existing tests, now underack_retransmits: 0).GroupWritealready carries a 254-byte APDU, so semantic-rebuild would save only ~350 B total). With retransmits disabled no bytes are copied; the 16-slot heapless capacity (~4.5 KiB) is statically reserved either way.ack_timeout_ms(3 s, the pre-engine constant); strict 1 s spec timing is one knob away.TunnelConfig::default()— no shim code changes.Behavior change at defaults: a tunnel with a persistently unanswered write now reconnects within ~2×
ack_timeout_ms(~6 s) instead of staying nominally connected for up to ~65 s.Tests
ack_retransmits: 0) keeps warn-and-continue.cargo test -p aimdb-knx-connector --features "std,tokio-runtime": 31 passed ✅; clippy-D warnings+ fmt clean ✅;embassy-knx-connector-demobuilds forthumbv8m.main-none-eabihf✅.Validation on hardware
Re-run the 035 §3 scenario 2 (writes while gateway is down) with this build: expect the first unACKed write to force a reconnect within ~6 s, subsequent presses to queue, and the queue to flush after re-handshake. Scenario 1's soak bar (zero AckTimeouts on healthy link) is unchanged.
Stacked on #143 (→ #142 → #141); retarget down the stack as bases merge.
🤖 Generated with Claude Code