Navigation

Strategy: RC-First

The rulebook permits remote-controlled robots, and remote control is the only mode this firmware has. The autonomous FSM was deleted — with no encoders and no IMU it could never have been coordinate-accurate, and an operator tagging sectors by eye produces better-labelled data than a dead-reckoned guess.

Mode How to enter What it does
RC (the only mode) Boots straight into RC — no build flag, no mode select RobotAP + phone joystick page; operator drives, samples, and tags sectors. An ultrasonic collision guard cuts forward drive before an impact

The WiFi AP and web server run in both modes so judges can always pull /data immediately after a run.


RC Run Procedure

  1. Power on → join RobotAP → open the RC page (192.168.4.1 / envirobot.local).
  2. Drive to a zone with the D-pad — the run clock starts on the first drive/sample and shows in the posbar ⏱ (amber 7:00, red 8:00). Select the current sector (S1–S4) on the page.
  3. Stop next to the zone (never over a water slot). Press Sample Water or Sample Soil (~6s, blocking).
  4. The arm deploys, reads a 3s median window, retracts; the reading is logged with your sector tag, printed to serial, and appears in the live table.
  5. Repeat for all 12 zones (1 water + 2 soil per sector). Press End Run to freeze the clock and finalise results.json.

Full step-by-step with failsafes and the live data station: Operating Guide.


Position Estimation (and its limits)

The robot has no wheel encoders and no IMU. Both heading and position are estimated open-loop from the commanded wheel speeds (differential-drive model):

  • Heading — integrated turn rate ω = (cmdR − cmdL) / WHEEL_BASE_MM. No gyro to correct wheel slip, so it drifts.
  • Distance — commanded motor speed × time (MM_PER_SEC_AT_DRIVE, calibrated by timing a 1m drive). Rough.
// position.cpp — dead-reckoning update, every 20ms
float omega = (cmdR_mm_s - cmdL_mm_s) / WHEEL_BASE_MM;  // commanded, not measured
pos.heading_rad += omega * dt;
float v = (cmdL_mm_s + cmdR_mm_s) / 2.0f;
pos.x_mm += v * dt * cosf(pos.heading_rad);
pos.y_mm += v * dt * sinf(pos.heading_rad);

The estimate feeds the path[] log for visualisation only. It is never used for safety decisions — the collision guard is always a live ultrasonic read, and zones are identified by the operator's eyes.

If the robot gets stuck, officials place it at the arena centre. With no IMU, both heading and position are lost on a reposition — send REZERO (RC page ⌖ button) with the eyeballed heading to re-zero the pose at the new position. Quadrant-level sector estimates survive; the fine path plot is best-effort.


Collision Guard

The HC-SR04 is the robot's only obstacle sensor, and its only job is to stop a crash. It does not steer, and it does not detect zones.

every loop (wifi_server.cpp -> updateCollisionGuard)
 |
 |  one ping every OBSTACLE_PING_MS (100ms)
 |
 |  reading < obstacleStopMm ?
 |     yes -> count a hit (capped at OBSTACLE_TRIP_HITS)
 |     no, and >= obstacleStopMm + OBSTACLE_HYST_MM (40mm) -> reset hits
 |     in between -> change nothing (this is the sticky band)
 |
 |  hits reached OBSTACLE_TRIP_HITS (2) ?
 |     on the rising edge only: brake -> stop, cancel any timed cal run
 |     while blocked: POST /rc FWD or CAL_FWD -> HTTP 409 {"error":"obstacle","mm":N}
 |                    BWD, LEFT, RIGHT all still pass

Why each part exists

Mechanism Constant Why
2-hit debounce OBSTACLE_TRIP_HITS One glitched HC-SR04 read must not halt a scored run
Hysteresis band OBSTACLE_HYST_MM A bare threshold chatters on and off at the boundary
Edge-triggered cut Motors are cut once, when the guard trips. Cutting every tick would brake the reverse you are using to escape
Reverse/turns never blocked A guard that also blocks escaping strands the robot, which costs more than the collision
Master switch collisionGuardOn The guard is optional. Uncheck it in the Diag config editor for deliberate close work; distance is still measured and reported
Ping rate OBSTACLE_PING_MS pulseIn blocks up to 25ms on a missing echo — 10Hz keeps RC latency low

The decision logic is a pure function in collision.h, unit-tested on the host by arduino/tools/test_collision.cpp.

What it does NOT protect against

Recessed water slots. The ultrasonic looks forward, not down, so it cannot see a slot at all. Nothing in the firmware keeps a wheel out of one — that is entirely the operator's job: stop short, sample, reverse away, never drive across.

Angled or soft walls also reflect the ping away and read as "nothing in range". Treat the guard as a backstop for the operator's eyes, not a replacement for them.

Sector labels

The operator selects S1–S4 on the RC page before each sample, and each button carries its terrain name (S1 URBAN … S4 STONE). There is no estimation involved — which is exactly why this is the accuracy path. REZERO re-zeroes the dead-reckoned pose after an official re-centres a stuck robot.