chore: initial commit — spec and implementation plan

This commit is contained in:
2026-04-13 13:04:55 -07:00
commit 95d9c7ef4c
2 changed files with 2292 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,238 @@
# Door Counter — System Design
**Date:** 2026-04-13
**Device:** M5Stack TimerCamera-F (ESP32 + OV3660, PSRAM, WiFi/BLE)
**Target:** Retail door traffic counting, non-tech-savvy end users
**Framework:** PlatformIO + Arduino
---
## 1. Architecture Overview
```
[TimerCamera-F Device]
├── Provisioning module — captive portal AP on first boot
├── Config store — NVS: device_id, location_id, HMAC secret, WiFi creds, line_offset
├── Camera + CV module — captures frames, runs line-crossing counter
├── BLE scanner — continuous passive scan (WiFi coexistence mode)
├── Report buffer — accumulates counts in RAM, flushes hourly
└── HTTP client — HMAC-signed POSTs to logs.research.bike
[logs.research.bike API] (additions needed)
├── POST /api/v1/camera/events/batch — new endpoint, mirrors BLE batch shape
└── camera_records table — new DB table
[Operator tooling]
├── flash_device.py — serial script to burn NVS config before shipping
└── ota_push.py — push firmware update over mDNS/HTTP
```
All firmware modules run as FreeRTOS tasks. Config survives firmware updates (stored in NVS). OTA updates supported via Arduino OTA — no physical access needed after initial flash.
---
## 2. Provisioning & Configuration
### First-boot flow (end user)
1. Device powers on, no WiFi credentials in NVS
2. Starts AP: `DoorCounter-Setup` (open, no password)
3. User connects phone → captive portal opens automatically in browser
4. Single-page form: WiFi network dropdown (scanned) + password field
5. On submit: credentials saved to NVS → device reboots → begins counting
6. On connection failure: automatically returns to AP mode
### Pre-provisioning flow (operator)
```bash
python flash_device.py \
--port /dev/ttyUSB0 \
--device-id dc-0042 \
--location-id retailer-123 \
--hmac-secret <32-byte-hex> \
[--wifi-ssid "StoreWiFi"] \
[--wifi-password "secret"]
```
Writes directly to NVS over serial. WiFi credentials are optional — if omitted, device falls through to captive portal on first boot.
### NVS config keys
| Key | Set by | Required to operate |
|---------------|---------------|---------------------|
| `device_id` | Operator | Yes |
| `location_id` | Operator | Yes |
| `hmac_secret` | Operator | Yes |
| `wifi_ssid` | User/operator | Yes |
| `wifi_pass` | User/operator | Yes |
| `line_offset` | Default 50% | No |
### Factory reset
Hold built-in button 5 seconds → wipes WiFi credentials (preserves device_id, location_id, HMAC secret) → returns to captive portal. Allows redeployment to a new store without operator intervention.
---
## 3. People Counting Algorithm
**Mounting:** Overhead, camera pointing straight down, centered above doorway.
**Frame pipeline** (~5 fps, FreeRTOS task):
```
Capture → Grayscale → Downscale 96×96 → Frame diff → Threshold → Blob detect → Centroid track → Line cross check
```
| Step | Detail |
|------|--------|
| Capture | OV3660 configured QVGA (320×240), grayscale |
| Downscale | Bilinear to 96×96 (~11× compute reduction) |
| Frame diff | Absolute difference against rolling background (updated every ~2s when no motion) |
| Threshold | Pixels > 30 intensity delta = foreground |
| Blob detect | Connected components; blobs < 8×8 px discarded as noise |
| Centroid track | Nearest-centroid matching frame-to-frame (max 15px), tracks persist up to 10 missed frames |
| Line crossing | Virtual horizontal line at configurable vertical position (default: 50% of frame height) |
**Counting logic:**
- Centroid crosses line top→bottom = **entry**
- Centroid crosses line bottom→top = **exit**
Counts accumulate as `{entries, exits}` in RAM and reset each hour on report.
---
## 4. BLE Scanning
Uses ESP32 built-in WiFi+BLE coexistence mode — BLE scans continuously while WiFi remains available. The only pause is a ~3s window during the hourly HTTP POST.
- Passive BLE scan, accumulates unique device hashes, near/mid/far counts per hour
- Reports to existing `/api/v1/events/batch` endpoint (no server changes needed for BLE)
- Data shape identical to existing BLE-only devices — same `ble_records` schema
---
## 5. API & Authentication
### HMAC scheme
Each request includes header:
```
X-HMAC-Signature: <hex>
```
Computed as:
```
HMAC-SHA256(secret, device_id + ":" + unix_timestamp + ":" + body_sha256)
```
Timestamp prevents replay attacks. Server validates within ±5 minute window.
### New endpoint: POST /api/v1/camera/events/batch
Request body:
```json
{
"location_id": "retailer-123",
"records": [
{
"period_start": 1712000000,
"period_end": 1712003600,
"entries": 42,
"exits": 39
}
]
}
```
Success response:
```json
{ "status": "ok", "accepted": 1 }
```
### New DB table: camera_records
```sql
CREATE TABLE camera_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_id TEXT NOT NULL,
location_id TEXT NOT NULL,
period_start INTEGER NOT NULL,
period_end INTEGER NOT NULL,
entries INTEGER NOT NULL,
exits INTEGER NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(device_id, period_start)
);
```
Idempotent on `(device_id, period_start)` — duplicate submissions are silently ignored.
### Existing endpoints (unchanged)
- `POST /api/v1/heartbeat` — device health ping, reused as-is
- `POST /api/v1/events/batch` — BLE data, reused as-is
### Local buffering
If WiFi is unavailable at report time, counts are held in RAM (up to 24 records / 24 hours). Flushed in chronological order on reconnect.
---
## 6. Deployment & OTA
### Physical installation (end user steps)
1. Mount device overhead, centered above doorway, camera pointing straight down
2. Plug into USB power (any phone charger)
3. Connect phone to `DoorCounter-Setup` WiFi network
4. Browser opens automatically → enter store WiFi password → done
**LED indicators:**
- Red — no WiFi connection
- Blue — connected, counting
- Yellow — uploading hourly report
The captive portal page is a single HTML file served from flash (no internet required), written in plain language with a mounting orientation diagram.
### OTA updates (operator)
Devices announce via mDNS as `<device_id>.local`. Push firmware:
```bash
python ota_push.py \
--host dc-0042.local \
--firmware build/firmware.bin
```
Future: devices can poll for updates on heartbeat via a `firmware_version` field in the heartbeat response — server signals when a newer version is available.
---
## 7. Firmware Project Structure
```
DoorCounter/
├── firmware/
│ ├── platformio.ini
│ └── src/
│ ├── main.cpp
│ ├── config.h / config.cpp — NVS read/write
│ ├── provisioning.h / .cpp — captive portal
│ ├── camera.h / .cpp — frame capture + CV pipeline
│ ├── ble_scanner.h / .cpp — BLE passive scan
│ ├── reporter.h / .cpp — hourly batch POST + local buffer
│ └── hmac.h / .cpp — HMAC-SHA256 signing
├── tools/
│ ├── flash_device.py — NVS provisioning script
│ └── ota_push.py — OTA push script
└── docs/
└── superpowers/specs/
└── 2026-04-13-door-counter-design.md
```
---
## 8. Open Questions / Future Work
- Confirm HMAC validation window (±5 min) matches existing server implementation
- Line offset calibration: consider a web UI at `<device_id>.local/config` for adjusting the virtual line position after install
- Multi-zone counting (two lines = zone dwell time) — out of scope for v1
- Dashboard / analytics UI — out of scope for v1