feat(adr-115): P3 — state encoder + rate limiter + rumqttc publisher (45 tests)
Implements ADR-115 §3.5 (QoS/retain matrix), §3.6 (LWT/availability
heartbeat), §3.7 (per-entity rate limits) as three new submodules:
- `mqtt::state` — `RateLimiter` (per-entity HashMap of last-emitted
Duration; allow() returns false within the configured 1/Hz gap),
`StateEncoder` rendering binary/numeric/event payloads from a
`VitalsSnapshot` projection, `StateMessage` carrying topic + payload
+ QoS + retain bits keyed off `DiscoveryComponent` so the wire-level
matrix from §3.5 is enforced in one place. Compiles without rumqttc
so it's testable under --no-default-features.
- `mqtt::publisher` (feature-gated) — `OwnedDiscoveryBuilder` for the
background task, `run()` event loop that pumps `rumqttc::EventLoop`
+ heartbeat (30s) + discovery refresh (configurable) + broadcast
channel consumer in a single tokio::select!. Reconnect resets the
RateLimiter so post-reconnect samples emit promptly. On graceful
shutdown publishes `offline` to every availability topic before
disconnect.
- `mqtt::discovery::EntityKind` — derive `Hash` so the entity can key
the RateLimiter's HashMap.
18 new state-encoder tests covering:
- Rate limiter: first-sample-pass, drops-within-gap, allows-after-gap,
per-entity independence, change-only entities (rate=0) always allow,
reset re-enables immediate publish.
- Boolean encoder: ON/OFF payload, QoS 1 + retain (per §3.5), rejects
non-binary entities, topic matches discovery state topic.
- Numeric encoder: HR bpm payload with confidence + ts, motion %
rendering, returns None when optional field absent, clamps
out-of-range motion, rejects non-sensor entities, QoS 0 + no retain.
- Event encoder: fall payload with confidence + ts, omits confidence
when None, QoS 1 + no retain (never replay old falls), rejects
non-event entities.
- iso_ts: RFC 3339 UTC with millisecond fraction.
Total mqtt test suite now 45/45 green:
cargo test -p wifi-densepose-sensing-server --no-default-features mqtt::
45 passed; 0 failed.
Compile-checked under --features mqtt + rumqttc 0.24 + use-rustls:
cargo check -p wifi-densepose-sensing-server --features mqtt --no-default-features
Finished dev profile (clean, no warnings).
Refs #776.
Co-Authored-By: claude-flow <ruv@ruv.net>