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 / soilWetVal are 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.