commit b3deb0036fda0fd0e39fac4e5d8bdbb085510a95 Author: Richard Date: Mon Apr 6 12:31:15 2026 +0200 Initial commit — car parking distance sensor ESP32-C3 Zero + VL53L1X ToF sensor + 3x LEDs with WiFi AP web calibration UI, rolling average smoothing, auto-sleep, and NVS persistence. Co-Authored-By: Claude Opus 4.6 (1M context) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b9f3806 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.pio +.vscode diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..24897ea --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,58 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Car parking distance sensor using an ESP32-C3 Zero, VL53L1X Time-of-Flight laser sensor, and 3x 10mm LEDs (green/yellow/red). Wall-mounted in a garage, it measures distance to an approaching car and displays proximity via LED states. Includes a WiFi AP with web UI for remote calibration. + +## Build & Flash Commands + +- `pio run` — compile firmware +- `pio run -t upload` — flash to ESP32-C3 (port: `/dev/ttyACM0`) +- `pio device monitor` — serial monitor (115200 baud) +- `pio run -t upload && pio device monitor` — flash and monitor in one shot + +Note: close the serial monitor before flashing, or the port will be busy. + +## Architecture + +Single-file firmware (`src/main.cpp`) using Arduino framework on PlatformIO. Three subsystems: + +1. **Sensor** — VL53L1X on I2C (GPIO 5 SDA, GPIO 6 SCL), continuous mode at 50ms intervals +2. **LEDs** — 3x 10mm through-hole on GPIO 2 (green), GPIO 3 (yellow), GPIO 4 (red), active HIGH with 220Ω resistors +3. **WiFi/Web** — ESP32 runs as AP (SSID: `ParkingSensor`, pass: `carpark1`). Web server on `192.168.4.1` provides: + - Live distance readout (polls `/api/status` at 300ms) + - One-tap calibration (`POST /api/calibrate`) — sets current distance as the stop point + - Manual stop distance input (`POST /api/set?stop=N`) + +## LED Behaviour + +LEDs off when distance > stop + 1000mm (ZONE_GREEN + 200mm headroom). Within range, zones are calculated as offsets above the calibrated stop distance: + +| Zone | Distance | LEDs | +|------|----------|------| +| Far | > stop + 800mm | Green | +| Approaching | stop + 400 – stop + 800mm | Green + Yellow | +| Close | stop + 100 – stop + 400mm | Yellow + Red | +| Very close | stop – stop + 100mm | Red solid | +| STOP | < stop | Red flashing | + +## Signal Processing + +- **Rolling average**: 5-sample moving average of valid readings smooths jitter +- **Bad reading filter**: readings with `range_status != 0` are discarded (status 7 = wrap-around is common at range edges) +- **Zero filter**: distance 0 (no target) is discarded +- **Auto-sleep**: LEDs turn off after 30s of stable distance (30mm tolerance), wake on movement + +## Calibration & Persistence + +The stop distance is stored in NVS (ESP32 flash) via the `Preferences` library. It survives power cycles. Zone offsets are `#define` constants (`ZONE_RED` 100, `ZONE_YELLOW` 400, `ZONE_GREEN` 800) at the top of `src/main.cpp`. Stable tolerance (`STABLE_TOLERANCE` 30mm) and sleep timeout (`SLEEP_TIMEOUT` 30000ms) are also tuneable. + +## Hardware + +- **Board**: ESP32-C3 Zero (board target: `esp32-c3-devkitm-1`) +- **Sensor**: VL53L1X ToF (I2C, Pololu library `pololu/VL53L1X@^1.3.1`) +- **LEDs**: 10mm through-hole (green, yellow, red) with 220Ω resistors +- **Power**: USB-C +- **Build flags**: `-DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=1` (required for serial over USB on C3 Zero) diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..af28f91 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,12 @@ +; PlatformIO Project Configuration File +; Car Parking Distance Sensor +; ESP32-C3 Zero + VL53L1X ToF sensor + 3x LEDs (green/yellow/red) + +[env:esp32-c3-devkitm-1] +platform = espressif32 +board = esp32-c3-devkitm-1 +framework = arduino +lib_deps = pololu/VL53L1X@^1.3.1 +monitor_speed = 115200 +upload_speed = 921600 +build_flags = -DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=1 diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..c187a59 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,284 @@ +#include +#include +#include +#include +#include +#include + +// --- Pin definitions (ESP32-C3 Zero) --- +#define SDA_PIN 5 +#define SCL_PIN 6 +#define LED_GREEN 2 +#define LED_YELLOW 3 +#define LED_RED 4 + +// --- WiFi AP settings --- +#define AP_SSID "ParkingSensor" +#define AP_PASS "carpark1" + +// --- Zone offsets above stop distance (mm) --- +#define ZONE_RED 100 // stop → stop+100 = red solid +#define ZONE_YELLOW 400 // stop+100 → stop+400 = yellow+red +#define ZONE_GREEN 800 // stop+400 → stop+800 = green+yellow + // > stop+800 = green only + +// --- Sleep timeout --- +#define STABLE_TOLERANCE 30 // mm — jitter threshold +#define SLEEP_TIMEOUT 30000 // ms — LEDs off after 30s of no movement + +// --- Globals --- +VL53L1X sensor; +WebServer server(80); +Preferences prefs; + +uint16_t stopDist = 100; // default stop distance (mm), loaded from flash +volatile uint16_t currentDist = 0; +unsigned long lastFlash = 0; +bool flashState = false; +uint16_t lastStableDist = 0; +unsigned long lastMovement = 0; +bool sleeping = false; + +// Rolling average buffer +#define AVG_SIZE 5 +uint16_t avgBuf[AVG_SIZE] = {0}; +uint8_t avgIdx = 0; +uint8_t avgCount = 0; + +uint16_t getSmoothed(uint16_t newVal) { + avgBuf[avgIdx] = newVal; + avgIdx = (avgIdx + 1) % AVG_SIZE; + if (avgCount < AVG_SIZE) avgCount++; + uint32_t sum = 0; + for (uint8_t i = 0; i < avgCount; i++) sum += avgBuf[i]; + return sum / avgCount; +} + +void setLEDs(bool green, bool yellow, bool red) { + digitalWrite(LED_GREEN, green); + digitalWrite(LED_YELLOW, yellow); + digitalWrite(LED_RED, red); +} + +void loadSettings() { + prefs.begin("parking", true); + stopDist = prefs.getUShort("stopDist", 100); + prefs.end(); + Serial.printf("Loaded stop distance: %d mm\n", stopDist); +} + +void saveSettings() { + prefs.begin("parking", false); + prefs.putUShort("stopDist", stopDist); + prefs.end(); + Serial.printf("Saved stop distance: %d mm\n", stopDist); +} + +// --- Web handlers --- + +const char HTML_PAGE[] PROGMEM = R"rawliteral( + + + + +Parking Sensor + + + +

Parking Sensor

+
-- mm
+
+ + + +
+ + +
+ + + + +)rawliteral"; + +String getColor() { + uint16_t d = currentDist; + if (d > stopDist + ZONE_GREEN) return "#2ecc71"; // green + if (d > stopDist + ZONE_YELLOW) return "#f1c40f"; // yellow + if (d > stopDist + ZONE_RED) return "#e67e22"; // orange + if (d > stopDist) return "#e74c3c"; // red + return "#c0392b"; // dark red (flashing) +} + +void handleRoot() { + String html = String(HTML_PAGE); + html.replace("%STOP%", String(stopDist)); + html.replace("%ZONE_RED%", String(ZONE_RED)); + html.replace("%ZONE_YELLOW%", String(ZONE_YELLOW)); + html.replace("%ZONE_GREEN%", String(ZONE_GREEN)); + server.send(200, "text/html", html); +} + +void handleStatus() { + String json = "{\"distance\":" + String(currentDist) + + ",\"stop\":" + String(stopDist) + + ",\"color\":\"" + getColor() + "\"}"; + server.send(200, "application/json", json); +} + +void handleCalibrate() { + stopDist = currentDist; + saveSettings(); + server.send(200, "application/json", "{\"ok\":true,\"stop\":" + String(stopDist) + "}"); +} + +void handleSet() { + if (server.hasArg("stop")) { + stopDist = server.arg("stop").toInt(); + if (stopDist < 10) stopDist = 10; + saveSettings(); + } + server.send(200, "application/json", "{\"ok\":true,\"stop\":" + String(stopDist) + "}"); +} + +void setup() { + Serial.begin(115200); + + pinMode(LED_GREEN, OUTPUT); + pinMode(LED_YELLOW, OUTPUT); + pinMode(LED_RED, OUTPUT); + + // Startup test + setLEDs(true, true, true); + delay(500); + setLEDs(false, false, false); + + // Load saved calibration + loadSettings(); + + // I2C + sensor + Wire.begin(SDA_PIN, SCL_PIN); + Wire.setClock(400000); + + sensor.setTimeout(500); + if (!sensor.init()) { + Serial.println("ERROR: VL53L1X not found!"); + while (true) { + digitalWrite(LED_RED, !digitalRead(LED_RED)); + delay(200); + } + } + sensor.setDistanceMode(VL53L1X::Long); + sensor.setMeasurementTimingBudget(50000); + sensor.startContinuous(50); + + // WiFi AP + WiFi.softAP(AP_SSID, AP_PASS); + Serial.printf("WiFi AP: %s IP: %s\n", AP_SSID, WiFi.softAPIP().toString().c_str()); + + // Web server routes + server.on("/", handleRoot); + server.on("/api/status", handleStatus); + server.on("/api/calibrate", handleCalibrate); + server.on("/api/set", handleSet); + server.begin(); + + Serial.println("Parking sensor ready."); +} + +void loop() { + server.handleClient(); + + uint16_t distance = sensor.read(); + uint8_t status = sensor.ranging_data.range_status; + + if (sensor.timeoutOccurred()) { + Serial.println("TIMEOUT"); + setLEDs(false, false, false); + return; + } + + // Ignore bad readings (0 = no target, status != 0 = out of range/error) + if (distance == 0 || status != 0) { + Serial.printf("Dist: %5d mm Status: %d (ignored)\n", distance, status); + return; // don't update LEDs or average — just skip + } + + uint16_t smoothed = getSmoothed(distance); + Serial.printf("Dist: %5d mm Avg: %5d mm Status: %d\n", distance, smoothed, status); + + currentDist = smoothed; + + // Detect movement — reset sleep timer if distance changed + if (abs((int)smoothed - (int)lastStableDist) > STABLE_TOLERANCE) { + lastStableDist = smoothed; + lastMovement = millis(); + sleeping = false; + } + + // Sleep if no movement for SLEEP_TIMEOUT + if (millis() - lastMovement > SLEEP_TIMEOUT) { + sleeping = true; + setLEDs(false, false, false); + return; + } + + uint16_t ledsOffDist = stopDist + ZONE_GREEN + 200; + if (smoothed > ledsOffDist) { + setLEDs(false, false, false); + } else if (smoothed > stopDist + ZONE_GREEN) { + setLEDs(true, false, false); + } else if (smoothed > stopDist + ZONE_YELLOW) { + setLEDs(true, true, false); + } else if (smoothed > stopDist + ZONE_RED) { + setLEDs(false, true, true); + } else if (smoothed > stopDist) { + setLEDs(false, false, true); + } else { + unsigned long now = millis(); + if (now - lastFlash > 150) { + flashState = !flashState; + lastFlash = now; + } + setLEDs(false, false, flashState); + } +}