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