Sensors
SEN0189 — Water Turbidity
Interface: ADC on GPIO 34 (ADC1 — WiFi-safe), via voltage divider. The sensor runs from 5V and outputs up to ~4.5V; the ESP32 ADC maximum is 3.3V. Without the divider, clean-water readings clip flat and the pin can be damaged.
Reading strategy — median window: every sample is the median of sampleWindowMs / 50 readings (~60 at the 3-second default) — one 10-average read every 50ms — after a 2-second settle for arm travel + probe stabilisation. The window is runtime-configurable 500–10000 ms (sampleWindowMs), as is the settle (sampleSettleMs). Median, not mean, so a single ADC spike can't drag the reported value. Blocking by design — the robot is always stationary with the probe deployed.
// sensors.cpp
static float turbidityCurve(float rawAdc) {
float v = rawAdc * (3.3f / 4095.0f) * TURBIDITY_DIVIDER_RATIO; // undo divider → true sensor volts
return -1120.4f * v * v + 5742.3f * v - 4353.8f; // DFRobot curve
}
float readTurbidityNTU(float* rawOut = nullptr) {
float raw = medianAnalogWindow(PIN_TURBIDITY); // median window
if (rawOut) *rawOut = raw; // diag page reports raw + volts
float ntu = turbidityCurve(raw);
// Zero-offset: shift the curve so calibrated clear water reads 0.
// turbidityZeroRaw == 0 means uncalibrated — curve used as-is.
if (config.turbidityZeroRaw > 0) ntu -= turbidityCurve(config.turbidityZeroRaw);
return constrain(ntu, 0.0f, 3000.0f);
}
The DFRobot quadratic is a starting point only, valid roughly 2.5–4.2V. It must be verified against reference samples before Testing Day — Category 1 requires ±10% accuracy. See Calibration.
NTU Classification (visualiser)
| Range | Classification | Colour |
|---|---|---|
| < 40 NTU | Clean | Blue (#0277BD) |
| 40–100 NTU | Moderate | Orange (#F57C00) |
| > 100 NTU | Turbid | Red (#C62828) |
Capacitive Soil Moisture v2.0
Interface: ADC on GPIO 35 (ADC1 — WiFi-safe), direct (output is 3.3V-safe). Lower raw reading = wetter soil. Uses the same 3s median window as turbidity.
// Endpoints live in the runtime config — measure per unit, edit at /test,
// no reflash. Defaults: soilDryVal 2850 (ADC in dry air), soilWetVal 1200.
float readSoilMoisturePct() {
float raw = medianAnalogWindow(PIN_SOIL_SENSE); // median window
float pct = (config.soilDryVal - raw) / (float)(config.soilDryVal - config.soilWetVal) * 100.0f;
return constrain(pct, 0.0f, 100.0f);
}
soilDryVal/soilWetValare runtime-config fields, captured by the Soil wizard on the Diag page — see Runtime Config. They are no longer#defines and changing them does not require a reflash.
HC-SR04 — Ultrasonic Distance Sensor
Interface: two plain GPIOs — TRIG 16, ECHO 17. Not on I2C. The ECHO line returns 5V, so it must pass through a divider to 3.3V before the ESP32 pin (e.g. 1kΩ series + 2kΩ to GND → 0.66×). TRIG accepts the ESP32's 3.3V directly.
Role: the collision guard — the robot's only obstacle sensor. Forward drive is cut when the reading stays below obstacleStopMm (runtime-configurable, default 120mm) for two consecutive pings. Full behaviour in Navigation.
// sensors.cpp — trigger a 10µs pulse, time the echo
digitalWrite(PIN_ULTRASONIC_TRIG, HIGH);
delayMicroseconds(10);
digitalWrite(PIN_ULTRASONIC_TRIG, LOW);
unsigned long us = pulseIn(PIN_ULTRASONIC_ECHO, HIGH, ULTRASONIC_TIMEOUT_US);
float mm = us ? us * 0.343f / 2.0f : 8190.0f; // timeout → "nothing in range"
Reliable range ~20–4000mm — ample reaction distance at competition drive speeds. The ULTRASONIC_TIMEOUT_US cap keeps a missing echo from stalling the web server. It cannot see down into a recessed water slot; keeping wheels out of one stays the operator's job.