Wattplot v2 — Test & Validation Checklist
STALE — written for the v2.4 architecture (BMI160 IMU, GPIO34/35 limit switches, GPIO19 grow-light relay, WS2812B status LED, BEDSUN 90° mode, “Watchdog (IMU disconnect) → FOLDING after 30s”). The current hardware is ESP32-S3, schematic rev B (2026-08-03) with the IMU / limit switches / WS2812B removed, IPROPI-based homing instead of physical switches, current-based endstops, and the 90° BEDSUN mode retired. The mechanical test sequence is still valid; the electrical test sequence is not. Cross-check against:
- Pin map:
docs/pinmap.html- Schematic:
docs/schematic.html(rev B, 2026-08-03)- Firmware config:
firmware/wattplot.yaml- Firmware quick-start:
firmware/README.mdA future pass will regenerate this doc. Tracked in ROADMAP.md.
What to verify, in what order, and how to interpret the results. The
build guide (docs/build_guide.md) covers the mechanical assembly. This
doc covers the testing and validation at every level.
Phase A: PCB bench test (before installing in enclosure)
Power the PCB on the bench with a 12V bench supply (current limit set to 1A). Verify each subsystem before installing in the enclosure.
A.1 Power rails
| Test | Expected | Pass? |
|---|---|---|
| 12V input (TP1) | 11.5-12.6 V | ☐ |
| 5V output (TP2) | 4.9-5.1 V | ☐ |
| 3.3V output (TP3) | 3.2-3.4 V | ☐ |
| Quiescent current (no peripherals) | 80-150 mA | ☐ |
| 5V under load (ESP32 + sensors) | < 500 mA | ☐ |
If the 5V rail is missing or low, check the MP1584 enable and feedback network. If the 3.3V rail is missing, check the AMS1117 input.
A.2 ESP32 boot
| Test | Expected | Pass? |
|---|---|---|
| USB-C connection detected | Yes | ☐ |
| ESP32 visible in esptool | Yes | ☐ |
| Flash via USB-C (test sketch) | Success | ☐ |
| Reset button (SW2) | Reboot | ☐ |
| BOOT button (SW1) | Enter download mode | ☐ |
| WiFi connection (with credentials) | Connect | ☐ |
A.3 I2C bus scan
Run an I2C scanner (Arduino sketch or ESPHome dump config). Expected devices on the bus:
| Address | Device | Found? |
|---|---|---|
| 0x40 | INA219 (current sensor) | ☐ |
| 0x68 | BMI160 (IMU) | ☐ |
| 0x70 (or similar) | (optional) external I2C breakout | ☐ |
If a device is missing, check the wiring, the 4.7 kΩ pullups, and the solder joints.
A.4 INA219 (current sensor)
| Test | Expected | Pass? |
|---|---|---|
| Bus voltage reading (3.3V rail) | 3.30 ± 0.05 V | ☐ |
| Current reading with no load | 0 ± 0.01 A | ☐ |
| Current reading with 1A load | 1.0 ± 0.05 A | ☐ |
| Power calculation | V × I (correct) | ☐ |
Calibration: the INA219’s shunt resistor value is configured in software (default is 0.1 Ω on most breakouts, 1 A max). Verify the calibration register matches your hardware.
A.5 BMI160 (IMU)
| Test | Expected | Pass? |
|---|---|---|
| Accel X (frame flat, X = bed long axis) | 0 ± 0.05 g | ☐ |
| Accel Y (frame flat, Y = bed short axis) | 0 ± 0.05 g | ☐ |
| Accel Z (frame flat, Z = up) | 1.0 ± 0.05 g | ☐ |
| Tilt 35° (manually tilt the IMU): atan2(accel_z, accel_x) | 35° ± 2° | ☐ |
| Gyro X, Y, Z (no motion) | 0 ± 0.5 °/s | ☐ |
| Gyro drift (1 minute, no motion) | < 1° total | ☐ |
If the tilt reading is off by 90° or 180°, the IMU’s orientation on
the board is different from the firmware’s expectation. Update the
firmware’s accel_x: ... etc. configuration.
A.6 DS18B20 (1-Wire temperature)
| Test | Expected | Pass? |
|---|---|---|
| Sensor detected on 1-Wire bus | Yes | ☐ |
| Temperature reading (room temp ~70°F) | 68-75°F | ☐ |
| Temperature reading (touch sensor with finger) | Rises 2-5°F | ☐ |
| Resolution = 12-bit | Yes | ☐ |
If the sensor is not detected, check the 4.7 kΩ pullup. If the reading is -196.6°F (the “disconnected” default), the pullup is missing.
A.7 DRV8871 (motor driver)
| Test | Expected | Pass? |
|---|---|---|
| Quiescent current (motor idle) | < 10 mA | ☐ |
| IN1=HIGH, IN2=LOW, EN=HIGH → motor forward | Yes | ☐ |
| IN1=LOW, IN2=HIGH, EN=HIGH → motor reverse | Yes | ☐ |
| EN=LOW → motor stop (coast) | Yes | ☐ |
| Current limit trip (motor stalled) | DRV8871 limits at ~3.6 A | ☐ |
| Overcurrent fault flag (nFAULT pin) | Goes LOW on overcurrent | ☐ |
Test with a small DC motor (e.g., 12V gearmotor) or the actual actuator. Verify the motor direction matches the firmware’s expectation (forward = extend, reverse = retract).
A.8 Sunapex 10A MPPT (charge controller)
v2.4 mini: the Sunapex has no host connection (no UART). The ESP32 does not query or command it. Verify physical install and charge behavior instead.
| Test | Expected | Pass? |
|---|---|---|
| Sunapex BAT+ / BAT− wired to battery (through 3 A fuse) | Polarity correct, fuse within 6” of battery + | ☐ |
| Sunapex PV+ / PV− wired to panel MC4 pigtails | Red MC4 → PV+, black MC4 → PV− | ☐ |
| Sunapex LCD lights up when battery is connected | Yes (the Sunapex is powered by the battery, not the panel) | ☐ |
| Sunapex MODE button set to LiFePO4 chemistry | LCD shows “Li” or “LiFePO4” | ☐ |
| Panel in sun → Sunapex charging LED on | Yes | ☐ |
| Sunapex output voltage (LCD) when charging | ~14.4 V during bulk | ☐ |
| Sunapex output voltage (LCD) when battery full | ~13.4 V float | ☐ |
ESPHome sensor.battery_voltage matches Sunapex LCD (±0.1 V) |
Yes | ☐ |
If charging LED does not come on in sun: check panel polarity (reversed polarity is protected, but the Sunapex won’t start), check the panel’s Voc is > 6 V (the Sunapex’s PV startup threshold), and verify the battery is at > 9 V (the Sunapex’s low-voltage cutoff).
For the full-size v2 build with a larger MPPT (Victron SmartSolar 100/30 or EPEver Tracer 4210AN), an additional UART/RS-485 test section will be added — the comms use the J4 footprint that is DNP on the v2.4 mini.
A.9 Soil moisture (analog input)
| Test | Expected | Pass? |
|---|---|---|
| Sensor in dry air | ~2.5-3.0 V (high = dry for capacitive) | ☐ |
| Sensor in water | ~1.0-1.5 V (low = wet) | ☐ |
| Sensor in moist soil | ~1.5-2.5 V | ☐ |
The exact values depend on the sensor. Calibrate in your soil.
A.10 Battery voltage sense
| Test | Expected | Pass? |
|---|---|---|
| 12V input (battery) | ADC reads ~3.0 V (12V × 10/40) | ☐ |
| 13.5V input (full charge LiFePO4) | ADC reads ~3.375 V | ☐ |
| 11.0V input (low battery) | ADC reads ~2.75 V | ☐ |
The 10k/30k divider (revised from 10k/10k) scales 12V to 3.0V, within the ESP32 ADC range.
A.11 Status LED (WS2812B)
| Test | Expected | Pass? |
|---|---|---|
| LED lights up at boot | Yes (any color) | ☐ |
| Color = state mapping (red=normal, blue=folding, etc.) | Yes | ☐ |
A.12 Relay (grow light)
| Test | Expected | Pass? |
|---|---|---|
| GPIO19 HIGH → relay clicks, K1.NO connects to K1.COM | Yes | ☐ |
| GPIO19 LOW → relay releases | Yes | ☐ |
| 12V at J5 pin 2 (when relay is energized) | 12V | ☐ |
Phase B: Mechanical integration test (PCB in enclosure, frame mounted)
After Phase A passes, install the PCB in the enclosure, mount the enclosure on the bed, and connect all the wiring. Re-verify each subsystem end-to-end.
B.1 IMU on the frame
| Test | Expected | Pass? |
|---|---|---|
| Tilt = 0° when frame is flat (visually) | 0° ± 1° | ☐ |
| Tilt = 35° when frame is at 35° (manually set) | 35° ± 1° | ☐ |
| Tilt = 90° when frame is vertical | 90° ± 1° | ☐ |
| Tilt reading is stable (no jitter > 0.1°/s) | Yes | ☐ |
If the tilt reading drifts, recalibrate the IMU (place the frame flat, trigger the calibration command, wait for the calibration to complete).
B.2 Limit switches
| Test | Expected | Pass? |
|---|---|---|
| 0° switch pressed when frame is at 0° | GPIO34 = LOW | ☐ |
| 0° switch released when frame is at 5°+ | GPIO34 = HIGH | ☐ |
| 90° switch pressed when frame is at 90° | GPIO35 = LOW | ☐ |
| 90° switch released when frame is at 80°- | GPIO35 = HIGH | ☐ |
Verify the firmware uses the limit switches as end-stops (the actuator should stop when either limit is reached, not over-drive).
B.3 Actuator end-to-end
| Test | Expected | Pass? |
|---|---|---|
| Command: extend to 35° → actuator moves, frame tilts to 35° | 5-10 sec | ☐ |
| Command: extend to 90° → frame goes to vertical | 5-10 sec | ☐ |
| Command: retract to 0° → frame goes flat | 5-10 sec | ☐ |
| Current during motion | 0.5-1.5 A | ☐ |
| Stall current (forcefully stop the actuator) | > 2.5 A, firmware stops | ☐ |
B.4 Cable carrier
| Test | Expected | Pass? |
|---|---|---|
| Cables don’t snag when tilting | Yes | ☐ |
| Cables don’t kink or bind | Yes | ☐ |
| Min bend radius respected (> 5× cable diameter) | Yes | ☐ |
If cables snag, adjust the carrier position or add slack.
Phase C: Software integration test (full system, WiFi connected)
With the build mechanically and electrically complete, test the software state machine end-to-end.
C.1 Connectivity
| Test | Expected | Pass? |
|---|---|---|
| ESP32 connects to WiFi | Yes | ☐ |
| Home Assistant auto-discovers device | Yes | ☐ |
| API encryption key accepted | Yes | ☐ |
| Web server (port 80) accessible | Yes | ☐ |
| OTA update works | Yes | ☐ |
C.2 Sensor values in HA
| Sensor | Expected HA value | Pass? |
|---|---|---|
| Tilt angle | 0-90° | ☐ |
| Motor current | 0-2.5 A | ☐ |
| Battery voltage | 11-14 V | ☐ |
| Soil temperature | 50-90°F (Phoenix) | ☐ |
| Soil moisture | 0-100% | ☐ |
| Outdoor temperature (from NWS) | -20 to 120°F | ☐ |
| Wind speed (from NWS) | 0-100 mph | ☐ |
C.3 State machine transitions
| Test | Expected | Pass? |
|---|---|---|
| Initial state at boot | FOLDING | ☐ |
| NORMAL command → frame tilts to 35° | Yes | ☐ |
| BEDSUN command → frame tilts to 90° | Yes | ☐ |
| Storm (mock NWS high winds) → frame goes to 0° | Yes | ☐ |
| Battery low (< 11V) → FOLDING | Yes | ☐ |
| Watchdog (IMU disconnect) → FOLDING after 30s | Yes | ☐ |
| After storm passes → state returns to commanded | Yes | ☐ |
C.4 Sunapex MPPT (charge controller)
| Test | Expected | Pass? |
|---|---|---|
| Sunapex holds bulk charge voltage at 14.4 V (LiFePO4) | 14.4 ± 0.2 V | ☐ |
| Sunapex transitions bulk → absorption → float as battery fills | Yes (visible on Sunapex LCD) | ☐ |
| Battery voltage rises during sun exposure | Yes (visible on ESPHome sensor.battery_voltage) |
☐ |
| Sunapex charge current peaks 0.4-0.5 A at solar noon | Yes (0.58A panel Imp × ~85% derate) | ☐ |
| Sunapex exits charge at sunset (no phantom drain) | Yes | ☐ |
| ESPHome panel-power = Sunapex charge-power (within ~0.5 W) | Yes | ☐ |
The Sunapex’s internal MPPT runs its own perturb-and-observe. There is no firmware-side MPPT loop, no UART setpoint commands, and no
mppt_stepscript — this is a simplification vs the v2.0-2.3 DPS5005-based design.
C.5 DLI grow light
| Test | Expected | Pass? |
|---|---|---|
| Light turns on when DLI < target | Yes | ☐ |
| Light turns off when DLI ≥ target | Yes | ☐ |
| Light off between midnight and 6am (8-hr dark minimum) | Yes | ☐ |
| Total photo period (light + sun) | ≤ 16 hours | ☐ |
C.6 Smart-fold safety
| Test | Expected | Pass? |
|---|---|---|
| Forcefully hold the actuator during NORMAL → motor current > 2.5 A → state = FOLDING | Yes | ☐ |
| Cut the IMU cable → watchdog triggers after 30s → state = FOLDING | Yes | ☐ |
| Cut the WiFi → state continues operating | Yes | ☐ |
| Power cycle → state comes back as FOLDING (safe) | Yes | ☐ |
Phase D: Field validation (operational, over weeks)
After all the above passes, run the system in production mode. Track metrics over time.
D.1 Daily checks (first week)
| Check | Expected | Pass? |
|---|---|---|
| Frame at 35° in morning (NORMAL) | Yes | ☐ |
| Frame at 35° in afternoon (NORMAL) | Yes | ☐ |
| Frame flat at night (storm watch) | Yes | ☐ |
| Daily kWh from Sunapex (mini: 10 W panel) | 0.03-0.05 kWh/day | ☐ |
| Battery voltage at end of day | 12.5-13.5 V | ☐ |
| Soil moisture reading | 30-60% | ☐ |
| No false FOLDING events (wind < 30 mph) | Yes | ☐ |
| No missed storm folds (wind > 50 mph) | Yes | ☐ |
D.2 Weekly checks
| Check | Expected | Pass? |
|---|---|---|
| IMU calibration drift | < 1°/week | ☐ |
| Hinge pin (no rust) | Clean | ☐ |
| Panel frame (no cracks) | Clean | ☐ |
| Bed walls (no rot) | Clean | ☐ |
| Sunapex efficiency (charging amps × 14.4V) / (panel V × panel A) | > 90% | ☐ |
| Tomato plant height | Growing | ☐ |
D.3 Monthly checks
| Check | Expected | Pass? |
|---|---|---|
| Frame lubrication (spray the hinge pin with silicone) | Yes | ☐ |
| Soil test (pH, NPK) | OK | ☐ |
| Battery state of health | > 95% | ☐ |
| Total kWh this month (target: ~120 kWh in summer) | Yes | ☐ |
| Total tomato yield this month (target: ~10 kg/plant) | Yes | ☐ |
Phase E: Long-term validation (3-12 months)
After 3 months, evaluate whether the design is meeting goals.
| Metric | Target | Actual |
|---|---|---|
| Total kWh produced (3 mo) | 360 kWh | ___ |
| Total tomato yield (3 mo) | 30 kg | ___ |
| System uptime | > 99% | ___ |
| False-fold events (wind < 30 mph) | < 1/week | ___ |
| Missed storm folds (wind > 50 mph) | 0 | ___ |
| Mechanical issues (broken parts) | 0 | ___ |
| Software bugs | < 1/quarter | ___ |
If metrics are off, see docs/control_law.md for tuning the state machine.
If mechanical issues, see docs/build_guide.md for build quality.
Validation summary
After completing all the above, the system is ready for production. Final sign-off requires:
- All Phase A bench tests pass
- All Phase B mechanical integration tests pass
- All Phase C software integration tests pass
- Phase D daily checks pass for 1 week
- Phase D weekly checks pass for 1 month
- No outstanding issues in the GitHub issue tracker
The apparatus is ready. Sign off and enjoy the tomatoes.