From 713b8ccb9fce5a22d5cfb5a7377a2997ef7c0ec5 Mon Sep 17 00:00:00 2001 From: flowcool Date: Tue, 20 Oct 2026 21:15:09 +0200 Subject: [PATCH] docs: document generic sampling and bounded display recovery --- docs/display-recovery.md | 49 +++++++++++++++++++++++++++++ docs/soil-sampling-qualification.md | 49 +++++++++++++++++++++++++++++ examples/multi-device/README.md | 40 +++++++++++++++++++++++ 3 files changed, 138 insertions(+) create mode 100644 docs/display-recovery.md create mode 100644 docs/soil-sampling-qualification.md diff --git a/docs/display-recovery.md b/docs/display-recovery.md new file mode 100644 index 0000000..e2a52b2 --- /dev/null +++ b/docs/display-recovery.md @@ -0,0 +1,49 @@ +# Display recovery timing and validation + +The package checks the live BUSY pin before normal sleep. If BUSY returns HIGH +after a successful refresh or recovery, it clears the cached safety flag and +starts recovery again. One independent `display_recovery_budget` timer covers +all recovery attempts and the final one-second sleep delays. A successful +sleep request cancels that timer. A new refresh cancels the old recovery, +timer, pending sleep, reset and fault notification before display SPI starts. +Cancellation releases RST if the old reset pulse was still LOW. + +With the default 30-second budget, terminal handling reaches +`deep_sleep.enter` within 31.510 seconds for MQTT or 31.010 seconds for API, +plus event-loop scheduling latency, measured from the first recovery start. +This includes the 10 ms reset pulse, 500 ms reset guard and transport fault +notification delay. A failed recovery guard can select terminal handling +earlier. Low-battery sleep has priority over Storage Mode; both retain their +configured durations. + +This bound ends at the automation action. It does not prove physical sleep or +zero SPI after a fault. On pinned ESPHome 2026.9.0: + +- `deep_sleep/deep_sleep_component.cpp` calls safe-shutdown hooks, then component + teardown with a 5000 ms budget. The hooks execute before that budget starts. +- `waveshare_epaper/waveshare_epaper.cpp` implements `on_safe_shutdown()` by + calling the display driver's `deep_sleep()`. +- `WaveshareEPaperTypeA::deep_sleep()` sends command `0x10` and data `0x01` for + `2.90inv2`, then waits for idle. That model uses the 1000 ms idle timeout. + The check uses `elapsed > timeout`, so include polling/scheduling overhead. + +The YAML terminal path starts no display refresh or SPI operation itself. The +stock driver still sends its sleep command during orderly shutdown. Neither a +strict no-SPI claim nor an exact physical-sleep deadline follows from these +automation tests. The normal shutdown path remains enabled for OTA image +confirmation and orderly network teardown. + +Run the isolated regression suite with: + +```bash +python3 -m unittest discover -s tests -p 'test_display_recovery*.py' -v +``` + +The executor reads package YAML and compiles its actual C++ lambdas. It models +only the required automation primitives from ESPHome 2026.9.0. GPIO, time and +transport outputs are local substitutes. Unsupported actions fail the test. +Tests execute the actual refresh-boundary action order from both normal and +maintenance callers. They cover permanent HIGH, guard oscillation, late LOW, +BUSY reassertion during the final sleep delay, cancellation during reset and +notification, fresh budgets, and battery/storage duration priority. They do not +execute the display driver, ESP-IDF, network shutdown or physical hardware. diff --git a/docs/soil-sampling-qualification.md b/docs/soil-sampling-qualification.md new file mode 100644 index 0000000..2f65c1d --- /dev/null +++ b/docs/soil-sampling-qualification.md @@ -0,0 +1,49 @@ +# Soil sampling interval qualification + +## Decision + +Keep the existing 3-second interval until a separately authorized bench comparison measures the analog signal. The current evidence does not establish that this interval is required. It also does not establish that 100-200 ms gives equivalent noise rejection. + +This is an offline qualification. It changes no firmware, calibration, MQTT value or production device. The source review request is [upstream PR #29](https://github.com/JGAguado/Smart_Plant/pull/29#pullrequestreview-5584638551). + +## Evidence and limits + +The current `acquire_normal` script in `examples/multi-device/packages/smart_plant_core.yaml` performs five soil ADC updates. Each update is followed by a 3-second delay, including the last update. The programmed dwell is therefore 15 seconds. The first-to-fifth sample span is 12 seconds. Neither number is the complete Wi-Fi-on or boot-to-sleep duration. + +The earlier cadence change separated AHT20 and VEML7700 acquisition while preserving soil spacing. It did not test 100-200 ms spacing. A publication span is only a lower-bound proxy for the complete awake time. + +The exact locally installed ESPHome 2026.9.0 implementation was inspected: + +- `components/adc/adc_sensor_esp32.cpp`: `ADCSensor::sample_fixed_attenuation_` reads the configured samples through the synchronous ADC oneshot driver. It does not impose a 3-second interval between component updates. +- `components/sensor/filter.cpp`: `SlidingWindowFilter::new_value` advances on input count. `MedianFilter::compute_result` computes the median of the accepted window. Neither method uses elapsed time to select its output. +- The configured median uses `window_size: 5`, `send_every: 5` and `send_first_at: 5`. It emits after the fifth input. The `soil_voltage` ADC sensor publishes the filtered voltage. The `soil` copy sensor derives its percentage from that publication. No first-sample value is intentionally published. + +These software facts explain the sample-count contract. They cannot prove probe settling, noise correlation, switched-supply behavior or unchanged accuracy at a shorter interval. `tests/test_soil_voltage.py` executes the pinned median, rejection and calibration code with the actual package filter settings. It checks the software pipeline only. A later implementation must recheck the selected ESPHome build source. + +## Candidate timing + +For the same five-update loop, including its trailing delay: + +| Delay | First-to-fifth span | Programmed dwell | +| --- | ---: | ---: | +| 3 s, current | 12 s | 15 s | +| 200 ms, candidate | 800 ms | 1 s | +| 100 ms, candidate | 400 ms | 500 ms | + +Removing only the trailing delay would save 3 seconds without changing sample timestamps. That remains a separate source change: the existing delay also allows asynchronous transport and other components to run. It must be checked against the complete lifecycle before publication. + +## Smallest remaining evidence gap + +A firmware timing recommendation requires representative analog measurements. Use one spare board and one probe on an isolated bench. Do not use a production device or shared MQTT namespace. This document is a proposal, not authorization to run the bench. + +1. Record the board/probe revision, supply voltage, firmware/source version, soil load and power-on age. Include a representative stable load and the post-power-on settling period. +2. Compare paired windows at 3 s, 200 ms and 100 ms. Retain every raw voltage with a relative sample timestamp, the five-sample median and the final calibrated percentage. Repeat the comparison at different battery voltages and representative dry/wet loads. Randomize the order to separate drift from timing. After each cadence change, acquire and discard a full five-sample warm-up window before recording any comparison window. Discard its median output too. The next recorded median must contain only samples taken at the candidate cadence; do not reuse a median that still contains samples from the previous trial. +3. Measure boot-to-deep-sleep duration directly from serial events or supply current. Report it separately from publication span. Compute the possible energy reduction from measured current and duration, not from the programmed delay alone. +4. Check invalid/out-of-range inputs separately with synthetic fixtures. The five-sample publication count and current calibration must remain unchanged. Reject a candidate if it adds a first-sample publication or changes invalid-value handling. +5. Before the bench, agree an acceptable voltage/percentage deviation and repeatability limit from the calibration use case. Use the calibration use case to choose these limits. A noise observation alone is not an acceptance threshold. + +Select the longest candidate interval that meets the agreed analog and publication limits. If no candidate meets them, retain 3 seconds and record the observed reason. Any later fleet use needs its own canary/build/OTA authorization and rollback to the last validated firmware. + +## Acceptance and rollback + +Offline qualification is complete when the source mechanism, timing arithmetic, historical evidence and remaining analog gap are explicit. It does not claim a shorter interval is safe. This document has no runtime effect. Revert its documentation commit to roll it back. diff --git a/examples/multi-device/README.md b/examples/multi-device/README.md index cab6ff9..f449811 100644 --- a/examples/multi-device/README.md +++ b/examples/multi-device/README.md @@ -154,3 +154,43 @@ Run the offline package tests with `python3 -B -m unittest discover -s tests -v` They execute the package actions and compile the policy lambdas as C++ with simulated sensors, time and GPIO. Firmware compilation checks ESPHome schemas and generated code. Neither check proves physical panel or network behavior. + +## API connection window + +The API profile samples the battery before a best-effort state-subscription +window. The default `api_connection_timeout` is 10 seconds. A logs-only client +does not release it. Critical battery protection bypasses this optional wait. +The device still completes its normal cycle if no subscriber connects. +Read [the API wake contract](../../docs/api-wake-window.md) for timing and limits. + +## Measurement diagnostics + +`Soil Voltage` exposes the median-filtered, plausibility-checked ADC reading. +`Soil Moisture` derives its percentage from the same publication. The package +keeps its existing wet/dry calibration and takes five samples spaced 3 seconds +apart. Each window publishes once, after its fifth sample. Read [the soil +qualification](../../docs/soil-sampling-qualification.md) before changing timing. +It defines the remaining isolated bench work. It does not approve a shorter +interval or a production experiment. + +`ESPHome Version` exposes the running version and its native configuration +hash. The hash identifies the effective build, not a Git commit. A local build +or dashboard record alone does not prove which image a device is running. + +Numeric gauge labels retain the measured value. Only needle coordinates are +clamped to the display range. Missing and nonfinite readings show `--`. + +## Display recovery limits + +The existing independent recovery timer covers all BUSY retries and final +sleep delays. A new refresh cancels stale recovery and releases an interrupted +reset pulse. With the default 30-second recovery budget, the sleep action is +reached within 31.510 seconds for MQTT or 31.010 seconds for API, plus scheduler +latency. These bounds start at the first recovery attempt. + +Read [the recovery contract](../../docs/display-recovery.md) for the action +ordering, tests and limits. The bounds end at `deep_sleep.enter`, not physical +sleep. Native shutdown hooks remain active, including the stock driver's final +SPI sleep command. Both normal and emergency paths retain explicit OTA image +confirmation for compatibility. No new application display refresh follows a +terminal fault.