IO10: Capstone: a tank monitor that lasts, copes and updates safely
Project 1: a year on one battery
🎯You'll be able to
Complete Project 1: a sensor driver that's correct and robust, a device that sleeps nearly all the time, and a battery life you can show with a budget and a measurement.
🏠The everyday picture
A night watchman who sits awake all night is worn out in a week. One who sleeps and checks the gate every fifteen minutes, for a moment, can keep going for months.
📘What it really is
-
The driver (
firmware/sensor.py): check the WHO_AM_I register reads 0x5A, start a conversion, wait for the status bit (the LV-20 takes about 30 ms), and read the distance as two bytes, low byte first, from the datasheet indocs/lv20-datasheet.md. -
Robustness: retry a bus error a few times, and if the sensor is dead, report it rather than crash (IO4, IO6).
- Standby: put the sensor back in standby after each reading.
- Deep sleep (
firmware/main.py): each wake ends in deep sleep until the next 15-minute slot. -
State (
firmware/state.py): what must survive deep sleep (counters, the last level, backoff) goes in RTC memory with a check, not in ordinary variables. -
The radio: only every fourth wake, or straight away for a low-tank alarm.
- The budget (
docs/power-budget.md): the charge per wake for each part, the total per day, and the battery life, within 15% of whattools/simulate.pymeasures.
📝A worked example
Input: test 02 failing: the driver returns a distance of 0 mm and 0.0 °C, where the test expects 766 mm and -3.5 °C.
Steps:
- Read the starter: it starts a conversion and reads straight away, before the LV-20 has finished, so the registers are still 0.
- Poll the status register until the result is ready, giving up after about 100 ms, and run the test again.
- Now it reads 65,026 mm and 124.5 °C. 766 is 0x02FE; reading the high byte first gives 0xFE02 (65,026). The temperature byte 0xF9
is signed, so it's -7 half-degrees, not 249. Read low byte first and treat the temperature as signed, as
docs/lv20-datasheet.mdsays, and runpytest tests/test_power.py.
Expected output: test 02 passes with 766 mm and -3.5 °C.
🌍Where it's used
- Water and gas meters that run for years on one battery.
- Farm and weather sensors in places with no power.
- Asset trackers that sleep between reports.
📝Case scenario
A learner's power budget said 900 days, but the simulation said 280. The budget had assumed the radio was used every fourth wake, but their code joined Wi-Fi on every wake. Fixing the code, not the budget, brought both to about 870 days in the practice kit.
🖱️Try it
Run python tools/simulate.py --days 7 on the starter and note the battery life it reports. Success: a figure of a few days, and the
part of the report that uses the most charge.
⚠️Common mistakes
- Fixing the budget to match the code. Sign: a budget that hides a wasteful wake. Fix: change the code, then measure again.
- State in ordinary variables. Sign: counters reset after every deep sleep. Fix: RTC memory with a check.
- Waiting with a fixed long delay. Sign: wakes longer than they need. Fix: poll the status bit.
🛠️For your project
Project 1's evidence: the output of pytest tests/test_power.py with 10 passing, docs/power-budget.md, and the simulation report.
✅Quick check
Why does JalSetu keep its counters in RTC memory?
Show the answer
Deep sleep clears ordinary memory, and RTC memory keeps them between wakes.
How does the starter's level reading go wrong?
Show the answer
It reads the two distance bytes in the wrong order, and doesn't wait for the conversion.
