chore: initial commit — spec and implementation plan
This commit is contained in:
2054
docs/superpowers/plans/2026-04-13-door-counter.md
Normal file
2054
docs/superpowers/plans/2026-04-13-door-counter.md
Normal file
File diff suppressed because it is too large
Load Diff
238
docs/superpowers/specs/2026-04-13-door-counter-design.md
Normal file
238
docs/superpowers/specs/2026-04-13-door-counter-design.md
Normal 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
|
||||
Reference in New Issue
Block a user