Skip to content

Sensors first implementation

Historical plan from 22 September 2026. This records the original sensor-only scope; it is not the current feature list or development workflow. See development and manual pump commands for current instructions.

Implement the user-approved Grow plan: one Rust sensor-only application, native macOS tests, cached ARMv6 cross-build and bounded SSH smoke test on pi@192.168.1.213, calibrated and raw moisture readings, MQTT discovery/availability, examples and systemd packaging. No pumps or output GPIO. No live service replacement. No commit or publication required.

Task 1: Sensor model, configuration and hardware

Own src/config.rs, src/sensor.rs, src/hardware.rs, src/lib.rs and examples/*.toml only. Root owns manifest, main, tooling and docs. Build small tested abstractions, no general framework.

Implement TOML Config with fields device_id (stable ASCII identifier), name, backend (simulated or hardware), sample_interval_ms (default 5000), stale_after_ms (default 15000), measurement_window_ms (default 1000), gpio_chip (default /dev/gpiochip0), sensors (Vec), optional mqtt (MqttConfig). SensorConfig has channel 1..3, name, optional Calibration {dry_hz, wet_hz}, simulated_hz default 10. MqttConfig has host, port default1883, topic_prefix default growhat, discovery_prefix default homeassistant, optional username, optional password_file PathBuf; allow no inline password. No credentials in Debug output or error dumps. All structs use serde deny_unknown_fields. Config::load(path) and validate() reject zero/unbounded timings, duplicate channels, invalid IDs/topics and nonfinite/equal calibration endpoints. Resolve password_file relative to the TOML file. Hardware configuration requires explicit hardware_confirmed = true; no automatic probe of arbitrary GPIO.

Sensor interface: trait SensorSource: Send { fn read(&mut self) -> Vec<(u8, Result)>; }. Snapshot structs serializable: SensorReading { channel:u8, raw_hz:Option, moisture_percent:Option, status:ReadingStatus, age_ms:Option, error:Option }; statuses valid, uncalibrated, invalid, stale. Track monotonic last successful sample per channel; invalid/missing/nonfinite/nonpositive samples must not appear healthy; stale after configured limit, retain last raw only with explicit nonhealthy status and age, never publish old moisture as healthy. Deterministic tests accept explicit elapsed Duration rather than sleeping. Expose SensorTracker::new(&Config), update(samples, now:Duration)->Vec. SimulatedSource::new(&Config) implements SensorSource. Library exports config/sensor/hardware modules.

Linux hardware driver: HardwareSource::new(&Config)->anyhow::Result, implements SensorSource; non-Linux constructor returns helpful unsupported error. Verify official Pimoroni schematic and reference grow/moisture.py before coding pins. Sensors are frequency-output, not ADC. Use Linux GPIO character device edge events with gpiocdev 0.7, rising-edge input-only requests, bounded blocking waits, concurrent capture across configured channels (not busy polling). Never access pump outputs. Count timestamped pulses to derive Hz with sufficient resolution; report missing pulses/overflow as invalid. Confirm APIs through Context7 or official crate docs. Compile on macOS and ARMv6. Document sources and physical board uncertainty in report; do not connect to Pi or actuate hardware. Main will call source.read() from a blocking worker to keep MQTT live.

Examples: simulated.toml runnable without MQTT and hardware.toml with hardware_confirmed=false and uncalibrated channels 1..3; no invented plant calibration. Focused unit tests for config, calibration (both directions/clamping), invalid data and stale transitions. Prefix terminal commands rtk; use Zsh. No commits, no subagents. Report APIs and test evidence at .superpowers/sdd/implementation-plan/task-1-report.md.

Task 2: MQTT and application

Add bounded diagnostic CLI and long-lived run command. Use rumqttc 0.25 without TLS native dependencies, async Tokio single-threaded MQTT event loop and blocking sensor worker. Stable device IDs and per-channel discovery; raw Hz always exposed, calibrated percent only when configured; per-entity health/availability avoids presenting old samples as current. Retained discovery, nonretained state with expiry, retained availability and Last Will, reconnect and HA birth republish. Never replay a queue of stale measurements after reconnection. Test with a disposable local MQTT broker, including outage/reconnect and process death. No live credentials committed.

Task 3: Build loop, packaging and commissioning

Native cargo-zigbuild + Zig installed in ignored .tools; ARMv6 target arm-unknown-linux-gnueabihf with glibc2.28. Cross-smoke minimal executable, then full application. One bounded command builds/copies changed runtime files/runs isolated Pi diagnostics, keeps prior executable and records elapsed time. Ship docs, example credentials instructions and hardened systemd service. Confirm actual sensor and HA behaviour if commissioning inputs and Pi connectivity available; clearly report unavailable evidence without inventing success.