# DoorCounter Retail door traffic counter using M5Stack TimerCamera-F (ESP32 + OV3660). Counts entries/exits via overhead camera CV, passively scans BLE foot traffic, and reports hourly to `logs.research.bike`. ## Hardware - **Device**: M5Stack TimerCamera-F (ESP32-S, OV3660, PSRAM, WiFi/BLE) - **Mount**: Overhead, camera pointing straight down, centered above doorway - **Power**: USB (any phone charger) ## Firmware Built with PlatformIO. Target: `timercam`. ```bash cd firmware pio run -t upload --upload-port /dev/ttyUSB0 ``` ### What it does | Module | Behavior | |--------|----------| | CV pipeline | 5 fps, 96×96 grayscale, blob tracking, line-crossing count | | BLE scanner | Continuous passive scan; deinits during hourly upload to free heap | | Reporter | Hourly HMAC-signed POST; 60s boot report for fast connectivity check | | Provisioning | Captive portal AP on first boot for WiFi setup | | OTA | Arduino OTA; operator push via `ota_push.py` | ### Reporting intervals - **First report**: 60 seconds after NTP sync (connectivity check) - **Subsequent reports**: every 3600 seconds ## Operator Setup ### 1. Flash firmware ```bash cd firmware pio run -t upload --upload-port /dev/ttyUSB0 ``` ### 2. Provision device identity ```bash python tools/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" ``` WiFi credentials are optional — if omitted, device starts captive portal on boot. ### 3. OTA updates ```bash python tools/ota_push.py \ --host dc-0042.local \ --firmware firmware/.pio/build/timercam/firmware.bin ``` ## End User Setup 1. Mount device overhead, camera pointing straight down 2. Plug into USB power 3. Connect phone to `DoorCounter-Setup` WiFi 4. Browser opens automatically → enter store WiFi password → done **LED indicators**: Red = no WiFi · Blue = counting · Yellow = uploading ## API Endpoint: `http://logs.research.bike` | Endpoint | Data | |----------|------| | `POST /api/v1/camera/events/batch` | Hourly entry/exit counts | | `POST /api/v1/events/batch` | Hourly BLE proximity records | | `POST /api/v1/heartbeat` | Device health (uptime, RSSI, pending records) | All requests are HMAC-SHA256 signed. See [design spec](docs/superpowers/specs/2026-04-13-door-counter-design.md) for full API shapes and auth scheme. ## Runtime Configuration The backend can push CV tuning parameters to individual devices in the response to `POST /api/v1/heartbeat`. No HTTP server runs on the device — updates ride the existing outbound, HMAC-authenticated channel. ### Configurable fields | Field | Type / Range | Meaning | |-------|--------------|---------| | `cfg_version` | uint32, non-zero | Monotonic version; device ignores updates with version ≤ stored. | | `diff_thresh` | 5–120 | Per-pixel motion threshold; higher = less sensitive. | | `min_blob_px` | 16–4096 | Minimum connected foreground pixels to count as a blob; higher = fewer false positives from small motion. | | `max_move` | 2.0–50.0 | Max inter-frame track displacement, in pixels on the 96×96 frame. | | `max_missed` | 1–60 | Frames a track can be missed before dropped. | | `line_offset` | 0–100 | Virtual counting line, as percent of frame height. | ### Push flow The heartbeat response MAY include a `config` object. All fields except `cfg_version` are optional; missing fields retain the device's current value. ```json { "config": { "cfg_version": 7, "diff_thresh": 25, "min_blob_px": 200, "max_move": 12.0, "max_missed": 8, "line_offset": 55 } } ``` ### Validation and apply rules - Missing response body, non-200, malformed JSON, or missing `config` object → silent no-op. - Missing `cfg_version` → rejected, logged `[CFG] missing cfg_version`. - `cfg_version` ≤ stored → rejected as stale, logged. - Any present field with wrong JSON type → whole update rejected, logged `[CFG] rejected malformed config`. - Any field out of range → whole update rejected, logged `[CFG] rejected invalid config`. - Valid update → applied atomically under mutex, persisted to NVS, logged `[CFG] applied v=N`. ### Persistence Tuning is stored in the `doorcounter` NVS namespace and survives reboot. On boot, the device loads the persisted values; if none present, compiled defaults apply. ### Trust model The reporting channel is plain HTTP today. The HMAC scheme signs only outbound **requests** (method + path + timestamp + sha256(body)) — it does not authenticate response bodies. A network attacker with access to the customer LAN can rewrite a heartbeat response and push any config that passes the device's range validator (`diff_thresh` 5–120, `min_blob_px` 16–4096, `max_move` 2.0–50.0, `max_missed` 1–60, `line_offset` 0–100). The validator is the last line of defense: malicious-but-in-range pushes can still degrade counting (e.g., `min_blob_px = 16` makes the detector noisy). Per-device targeting (keyed by `device_id`) still works correctly and is unaffected by the integrity gap — each device only applies updates addressed to itself. Operators should treat customer LANs as untrusted and rely on monitoring heartbeat cadence and count anomalies to detect tampering. No inbound HTTP surface is exposed on customer LANs by design — the device only makes outbound requests. ## Roadmap **Gated local config portal.** Holding the BOOT button for ~3 seconds would raise a WiFiManager-style captive portal on the local network for ~5 minutes, exposing a tuning page for field techs operating without backend connectivity. Deferred because (a) the server-push mechanism above covers routine tuning, (b) an always-on HTTP server on customer LANs is an undesirable attack surface, and (c) the gated-by-physical-access model needs additional auth design to be safe. **Authenticated config push.** Move reporting to HTTPS, or include a signed envelope on pushed config (e.g., `config_sig = HMAC(secret, cfg_version || canonical_json(config))` verified on device) so pushed tuning is tamper-evident over plain HTTP. ## Project Structure ``` DoorCounter/ ├── firmware/ │ ├── platformio.ini │ ├── lib/hmac/ — HMAC-SHA256 signing library │ └── src/ │ ├── main.cpp — FreeRTOS tasks, boot sequence │ ├── config.* — NVS read/write │ ├── provisioning.* — captive portal │ ├── camera.* — frame capture + CV pipeline │ ├── ble_scanner.* — BLE passive scan │ └── reporter.* — hourly batch POST + local buffer ├── tools/ │ ├── flash_device.py — NVS provisioning script │ └── ota_push.py — OTA push script ├── docs/superpowers/specs/ │ └── 2026-04-13-door-counter-design.md └── server/ — API server (separate deployment) ```