Calibration

Tuning values live in the runtime config on the robot (SPIFFS /config.json, edited from the Diag page — no reflash). Only values describing physical hardware, like TURBIDITY_DIVIDER_RATIO, stay compile-time in hardware_pins.h. Shipped values are estimates — measure and update each one before Testing Day. Category 1 (30 pts) requires readings within ±10% of the judges' reference values; calibration is the highest-value engineering time in this project.

Run the steps in order — later steps depend on earlier ones. (Canonical checklist: arduino/CALIBRATION.md in the repo.)

# What Tool Config keys When
1 Soil moisture endpoints Diag → Soil probe wizard (or tests/test_soil) soilDryVal, soilWetVal Bench, any time
2 Turbidity zero + divider check Diag → Turbidity wizard (or tests/test_turbidity) turbidityZeroRaw (+ verify TURBIDITY_DIVIDER_RATIO) Bench, before Testing Day
3 Drive speed Diag → Motion wizard mmPerSecAtDrive Arena-like surface, charged battery
4 Wheel base (turn rate) Same wizard wheelBaseMm After step 3
5 Collision stop distance Diag → Collision distance wizard (or tests/test_ultrasonic) collisionGuardOn, obstacleStopMm On the real arena, after step 3

1. Soil Moisture Endpoints

  1. Flash tests/test_soil. Serial monitor @115200.
  2. Probe in dry air (or bone-dry soil), wait for the raw value to flatten → SOIL_DRY_VAL.
  3. Probe in a cup of water up to the marked line (never the electronics) → SOIL_WET_VAL.
  4. Sanity: dry raw must be higher than wet raw (capacitive probes fall when wet). Verify air ≈ 0%, water ≈ 100% after reflashing.

2. SEN0189 Turbidity (highest priority)

Two things must be right: the divider ratio and the curve.

  1. Flash tests/test_turbidity. Dunk the probe in clear water: Vsensor should read ~4.1–4.3V and NTU near 0.
    • Vsensor way off (e.g. half of expected) → the hardware divider isn't the configured ratio. Recompute (R_top + R_bottom) / R_bottom from the actual resistors and fix TURBIDITY_DIVIDER_RATIO.
  2. Test a murky sample (stir soil in): NTU should rise clearly.
  3. For absolute accuracy: prepare reference samples (clear water + 2–3 diluted-milk or standard steps of known NTU), record firmware NTU vs reference across the range.
  4. Within ±10% → done. Outside → refit the quadratic in readTurbidityNTU() from your measured (voltage, NTU) points — the DFRobot curve is only trusted ~2.5–4.2V and assumes direct 5V output.
  5. On Testing Day, note readings against the judges' reference sample and offset in analysis — don't bend the curve blind.

3. Drive Speed → mmPerSecAtDrive

Robot on battery, untethered, arena-like surface, race-day charge (open-loop speed sags with voltage).

  1. Phone → Diag page (🔧) → Motion calibration wizard.
  2. Tape-mark the floor at the robot's front edge.
  3. The "Run" step drives ~3s at driveSpeed and auto-stops.
  4. Measure start mark → front edge in mm; enter it in the wizard.
  5. The wizard computes mmPerSecAtDrive and, at the end, saves it straight to the runtime config (SPIFFS) — no reflash to apply.

4. Wheel Base → wheelBaseMm

Continues in the same Motion wizard — it uses the drive speed you just measured (read live from the config, no hardcoded mirror).

  1. Tape an arrow on the robot; note its start direction against a floor mark.
  2. The "Run" step spins ~2s in place, auto-stops.
  3. Enter the total rotation = full turns × 360 + final offset (e.g. 2 turns + 90° = 810°). The wizard computes wheelBaseMm.
  4. This is the effective wheel base (includes spin friction) — expect slightly larger than the ruler measurement. "Save to editor" writes both values to the runtime config; "Spin again to verify" re-runs the check.

Verify the pair: drive a ~1m square via RC watching the x/y/h posbar readout. Back at start: x/y within ~±150mm, heading near 0°. Drift is normal and grows with jerky driving — the path log is decoration, never navigation.


5. Collision Stop Distance → obstacleStopMm

This is the safety number, and it must be larger than the robot's stopping distance — a threshold below it means the robot brakes into what it just detected.

  1. Measure the stopping distance: drive forward at full driveSpeed on the arena, hit ■ STOP, measure the overshoot. Repeat 3×, take the worst.
  2. Diag page → Collision distance wizard. Step 1: park the robot facing an arena wall at the gap you want it to hold, capture the ping. A "no echo" result means the wall is not square to the sensor or the ECHO divider is wrong — fix that before continuing.
  3. Step 2: enter the stopping distance. The wizard sets obstacleStopMm = standoff + stopping distance.
  4. Verify live: the RC page shows the ping in mm and flips to red ■ BLOCKED when the guard trips. Walk a board toward the robot and watch it cut.

The guard is optional. collisionGuardOn (checkbox in the config editor, default on) switches the intervention off for deliberate close work — creeping up to a wall, or sampling hard against an edge. Distance is still measured and shown (amber ⚠ guard off); re-enabling resets the debounce so it cannot trip on a stale reading.

Angled and soft walls reflect the ping away and read as "nothing in range", and the sensor cannot see into a recessed water slot. The guard is a backstop, not a substitute for watching the robot.


Timing Constants Reference

Constant Shipped Tune when
sampleSettleMs 2000 Reading unstable after arm deploy → increase (covers ~600ms arm travel + probe settle)
sampleWindowMs / SAMPLE_TICK_MS 3000 / 50 Median window: ~60 readings per sample. Longer = smoother but slower
obstacleStopMm 120 Collision-guard threshold — see §5
BRAKE_MS 80 Active short-brake pulse on STOP. Too short = coasting; too long = heats the driver
OBSTACLE_PING_MS 100 Guard ping rate (compile-time)
OBSTACLE_TRIP_HITS / OBSTACLE_HYST_MM 2 / 40 Guard debounce and hysteresis (compile-time)

Caveats: recalibrate speed after a battery swap or heavy drain (10% voltage drop is a visible speed drop) · calibrate on the surface you'll drive on · the Diag Motion wizard reads driveSpeed/turnSpeed live from the runtime config, so changing them needs no code edit — the old hardcoded mirror in the RC page is gone.


Pre-Run Sequence (Testing Day morning)

  1. Full battery. Flash latest firmware.
  2. Power on → join RobotAP → RC page loads at 192.168.4.1 / envirobot.local.
  3. live_plot.py --port COMx on the laptop — verify sample lines parse (this is the judged output).
  4. Dip test SEN0189 in known water → NTU plausible.
  5. Press soil sensor into test medium → % plausible.
  6. Walk a board at the robot → guard trips → reverse clears it.
  7. One full rehearsal sample of each type → ⏱ starts on first command → ⬇ Download JSON → drop into the visualiser.