Operating Guide
End-to-end instructions: flashing, boot, the run clock, driving a full RC run, the live data station, and troubleshooting.
1. One Build
There is one firmware build and one mode: remote control. The autonomous FSM was removed, so there is no build flag, no BOOT-button mode select, and no possibility of the robot moving on its own at power-on.
WiFi is AP-only — the robot is the network (RobotAP at 192.168.4.1), see §4. The web server is always up, so judges can always pull /data.
2. Flashing
Board: ESP32 Dev Module / DOIT ESP32 DevKit v1 (esp32:esp32:esp32doit-devkit-v1).
Libraries: ESP32Servo · ArduinoJson. (The Adafruit TCS34725 library is no longer needed.)
arduino-cli compile --fqbn esp32:esp32:esp32doit-devkit-v1 arduino/envirobot
arduino-cli upload --fqbn esp32:esp32:esp32doit-devkit-v1 -p COM5 arduino/envirobot
- Sketch is ~98% (approximate — re-measure at the next build) of the 1.3MB default app partition (RC + Diag + Data pages compiled in). Switch Partition Scheme to "Huge APP (3MB No OTA/1MB SPIFFS)" if you need headroom — it erases SPIFFS on the next flash, so never between a run and its
/datadownload. - ArduinoJson 7 prints deprecation warnings for the v6 API — expected, harmless.
- If you edited
arduino/rc_page/index.html: runnode arduino/tools/gen-pages.cjsfirst — it regenerates all three PROGMEM headers (rc_page.h,test_page.h,data_page.h) — then compile. The RC page is compiled into flash — editing the HTML alone changes nothing on the robot. - After any firmware change, re-sync the public code browser:
node scripts/sync-code.mjs --push.
3. Boot & LED States
| LED (GPIO 2) | Meaning |
|---|---|
| Solid ON | Booted, RC ready |
Power-on sequence: SPIFFS mount → config load → motors/servo/sensors init → RobotAP up → web server up → LED solid. The servo swings to 90° (neutral) at boot — keep hands clear of the arms. The robot never drives at power-on.
4. Connecting
WiFi is AP-only — the robot is the access point. Nothing to provision at the venue, no router, no hotspot, no internet, and nothing that can fail because the venue WiFi is congested.
1. Power the robot. Watch serial @115200 for:
[WiFi] AP "RobotAP" up at http://192.168.4.1
2. Join WiFi RobotAP / enviro123 from the phone (and the laptop).
3. Open http://192.168.4.1 or http://envirobot.local
- Phone and laptop can both be joined at once — phone drives on
/, laptop shows/viewand runslive_plot.py. - Your phone will warn that this network has no internet. Stay connected anyway; tell it not to switch back to mobile data.
- There is no cloud path. Run data leaves the robot only as a downloaded
results.json(§9).
5. The Run Clock (read this once)
All judge-facing timing (total_duration_s, sample t_s, the path log, the RC page ⏱) counts from run start, not power-on:
- RC mode: the clock starts on the first drive or sample command after boot (or after an End Run). STOP, calibration commands and Re-zero do not start it.
- End Run / run complete freezes the clock and the JSON. The next drive/sample opens a fresh run document — back-to-back practice runs don't bleed into each other.
- The RC page ⏱ shows minutes:seconds — amber at 7:00, red at 8:00. Over 8 minutes costs −5 pts; in RC mode the operator is the timekeeper.
6. RC Run — Full Procedure (competition mode)
Setup (before the slot):
- Battery at full charge (open-loop drive speed sags with voltage).
- Power on. LED solid = RC ready.
- Phone → join
RobotAP→ open192.168.4.1. Status shows "connected"; the posbar shows x/y/h, ⏱--:--, and the live collision-guard distance. - Laptop: join
RobotAPtoo, open 📊/viewfor the live judge dashboard, and/or start the live data station (§8) so judges see the table fill during the run.
The run (12 samples, ≤8 min):
- First D-pad press starts the ⏱ — from here you're on the clock.
- Drive to the first zone. Never drive at a water slot — they're recessed wheel traps. Approach so the probe side faces the slot; stop with wheels clear.
- Tap the sector the robot is in (S1–S4) — this labels the next sample.
- Tap 💧 Sample Water (or 🌱 Sample Soil). The robot stops motors, deploys the arm, settles 2s, reads a 3s median window, retracts — ~6s total, blocking. The reading appears in the page log, on serial, and in the live table.
- Repeat: per sector 1 water + 2 soil. Watch the ⏱ — amber at 7:00 means finish the sample you're on and End Run.
- 🏁 End Run: freezes the clock and saves
results.jsonto SPIFFS.
Immediately after: save the run JSON (RC page ⬇ Download JSON, or <robot-ip>/data) on two devices, keep samples.csv from the live station. Judges review data now.
Failsafes while driving:
- Dead-man: the page repeats the drive command every 120ms while held. If no command arrives for 500ms (WiFi drop, phone sleep) the motors stop.
- A missed status poll during the ~6s blocking sample is normal — the page suppresses the false "offline".
7. Collision Guard
Forward drive is cut automatically when the HC-SR04 reads closer than obstacleStopMm. Details in Navigation; what the operator sees:
- The posbar readout shows the live distance, and turns red ■ BLOCKED when the guard trips. The ▲ FWD button greys out and the log records the distance.
- Reverse and both turns still work — back out, then forward is re-enabled once the reading clears by 40mm.
- The guard is optional: uncheck Collision guard in the Diag page config editor to switch it off (distance is still displayed, amber ⚠ guard off). Turn it off for deliberate close work against a wall or edge.
- It cannot see into a recessed water slot. Wheels-into-a-slot is still entirely on the operator.
8. Live Data (two views)
/view (pretty, on the robot) — open http://<robot-ip>/view on a laptop/projector: live arena + robot position + path trail, NTU/moisture bars, sample table, event log. Self-contained, no install; runs alongside phone RC on the same LAN. The polished judge-facing display — a WiFi "alternative method" that needs §4.6 written approval.
arduino/tools/live_plot.py (the always-legal judged output) — a live table + labelled NTU/moisture bar charts plus a live position/path panel (matplotlib, a rulebook §4.6 named example), with every sample appended to samples.csv (a second approved format).
pip install matplotlib requests pyserial
# During the run — laptop joined to RobotAP:
python live_plot.py --url http://192.168.4.1/samples
# Bench / inspection — robot on USB:
python live_plot.py --port COM5
- URL mode polls
/samplesevery 2s (samples only, no 48KB path array). A missed poll while the robot is mid-sample is normal. - Serial mode parses the
[SAMPLE] S<n> <TYPE> <value>lines — never change that serial format in firmware. - For inspection, serial mode doubles as the "one live reading" demonstration.
9. Data Retrieval
| What | How |
|---|---|
| Full run JSON | RC page ⬇ Download JSON, or GET 192.168.4.1/data — live during the run, frozen after End Run, survives reboot (SPIFFS) |
| Live samples feed | GET 192.168.4.1/samples — polled by live_plot |
| Visualiser | Drag the saved JSON into /viz — arena replay + charts |
| Backup | Save the downloaded JSON to two devices before leaving the arena. SPIFFS survives a reboot but not a reflash |
10. Troubleshooting
| Symptom | Cause → Fix |
|---|---|
RobotAP not in the WiFi list |
Robot not powered or crashed at boot — check serial @115200 for the [WiFi] AP ... up line and for brownout resets |
| RC page won't load | Wrong network on phone; or stale cached page — hard-refresh 192.168.4.1 |
⏱ stays --:-- |
Run clock hasn't started — it starts on the first drive/sample, not at boot |
| Motors dead, page connected | Dead-man fired (normal after 500ms without input) — press a drive button again |
| NTU pinned at 0 or 3000 | Divider ratio wrong or probe dry — verify TURBIDITY_DIVIDER_RATIO matches the resistors (Calibration §2) |
| Soil % stuck 0/100 | SOIL_DRY_VAL/SOIL_WET_VAL not calibrated for this probe |
| Forward drive refused, nothing ahead | Collision guard tripped on a phantom echo — check the posbar distance; raise obstacleStopMm, or uncheck Collision guard to work without it |
| Guard never trips | collisionGuardOn unchecked, or ECHO not wired — run the obstacle self-test on the Diag page; "no echo" means check the 5V→3.3V divider |
| Samples missing after reboot | Expected: /data serves the last saved run — always End Run before power-off |