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) <noreply@anthropic.com>
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
.pio
|
||||||
|
.vscode
|
||||||
58
CLAUDE.md
Normal file
58
CLAUDE.md
Normal file
@@ -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)
|
||||||
12
platformio.ini
Normal file
12
platformio.ini
Normal file
@@ -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
|
||||||
284
src/main.cpp
Normal file
284
src/main.cpp
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
#include <Wire.h>
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <WebServer.h>
|
||||||
|
#include <Preferences.h>
|
||||||
|
#include <VL53L1X.h>
|
||||||
|
|
||||||
|
// --- 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(
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>Parking Sensor</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: sans-serif; max-width: 400px; margin: 20px auto; padding: 0 15px; background: #1a1a1a; color: #eee; }
|
||||||
|
h1 { font-size: 1.3em; }
|
||||||
|
.dist { font-size: 3em; font-weight: bold; text-align: center; padding: 20px; border-radius: 12px; margin: 15px 0; }
|
||||||
|
.zone { text-align: center; padding: 8px; margin: 4px 0; border-radius: 6px; font-size: 0.9em; }
|
||||||
|
.btn { display: block; width: 100%; padding: 14px; margin: 10px 0; border: none; border-radius: 8px; font-size: 1.1em; cursor: pointer; }
|
||||||
|
.btn-cal { background: #e74c3c; color: white; }
|
||||||
|
.btn-set { background: #3498db; color: white; }
|
||||||
|
input[type=number] { width: 80px; padding: 8px; font-size: 1em; border-radius: 6px; border: 1px solid #555; background: #333; color: #eee; }
|
||||||
|
label { display: block; margin-top: 12px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Parking Sensor</h1>
|
||||||
|
<div class="dist" id="dist">-- mm</div>
|
||||||
|
<div class="zone" id="zones"></div>
|
||||||
|
|
||||||
|
<button class="btn btn-cal" onclick="calibrate()">Set Current Distance as STOP</button>
|
||||||
|
|
||||||
|
<hr style="border-color:#333; margin:20px 0">
|
||||||
|
<label>Stop distance (mm):
|
||||||
|
<input type="number" id="sd" min="10" max="3000" value="%STOP%">
|
||||||
|
<button class="btn-set btn" style="display:inline;width:auto;padding:8px 16px" onclick="setManual()">Set</button>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div id="zones-detail" style="margin-top:20px; font-size:0.85em; color:#999"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function update() {
|
||||||
|
fetch('/api/status').then(r=>r.json()).then(d=>{
|
||||||
|
let el = document.getElementById('dist');
|
||||||
|
el.textContent = d.distance + ' mm';
|
||||||
|
el.style.background = d.color;
|
||||||
|
el.style.color = d.color === '#f1c40f' ? '#000' : '#fff';
|
||||||
|
document.getElementById('sd').value = d.stop;
|
||||||
|
document.getElementById('zones-detail').innerHTML =
|
||||||
|
'Flashing red: < ' + d.stop + 'mm<br>' +
|
||||||
|
'Red solid: ' + d.stop + ' - ' + (d.stop+%ZONE_RED%) + 'mm<br>' +
|
||||||
|
'Yellow+Red: ' + (d.stop+%ZONE_RED%) + ' - ' + (d.stop+%ZONE_YELLOW%) + 'mm<br>' +
|
||||||
|
'Green+Yellow: ' + (d.stop+%ZONE_YELLOW%) + ' - ' + (d.stop+%ZONE_GREEN%) + 'mm<br>' +
|
||||||
|
'Green: > ' + (d.stop+%ZONE_GREEN%) + 'mm';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function calibrate() {
|
||||||
|
if(confirm('Set current distance as the STOP point?'))
|
||||||
|
fetch('/api/calibrate',{method:'POST'}).then(()=>update());
|
||||||
|
}
|
||||||
|
function setManual() {
|
||||||
|
let v = document.getElementById('sd').value;
|
||||||
|
fetch('/api/set?stop='+v,{method:'POST'}).then(()=>update());
|
||||||
|
}
|
||||||
|
setInterval(update, 300);
|
||||||
|
update();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user