94d74e425c9a70962cf6a6f37debc2bc214caa93
cv_process and helpers (frame_diff, extract_blob, find_centroids) now read diff_thresh, min_blob_px, max_move, max_missed, and line_offset from state.tuning instead of file-scope static const constants. The four thresholds are promoted to file-local constexpr defaults in cv.cpp (CV_DEFAULT_*) and are no longer part of the public cv.h API — external code can't depend on them. cv_process signature drops the line_pct parameter; callers use state.tuning.line_offset instead. This eliminates the drift hazard of having two sources of truth (DeviceConfig.line_offset vs CVTuning.line_offset); the former is deleted. main.cpp now calls config_load_tuning(g_cv.tuning) after cv_init on boot so previously persisted tuning survives reboot; logs whether tuning came from NVS or defaults. The legacy NVS key "line_offset" is intentionally left alone — harmless and flash_device.py may still write it during provisioning. Migration is out of scope. Tests: 12/12 passing (11 existing + 1 new test_cv_process_respects_runtime_min_blob proving the runtime-read path). Flash: 1,414,069 bytes (89.9%). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
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
cd firmware
pio run -t upload --upload-port /dev/ttyUSB0
2. Provision device identity
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
python tools/ota_push.py \
--host dc-0042.local \
--firmware firmware/.pio/build/timercam/firmware.bin
End User Setup
- Mount device overhead, camera pointing straight down
- Plug into USB power
- Connect phone to
DoorCounter-SetupWiFi - 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 for full API shapes and auth scheme.
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)
Description
Languages
C++
58.6%
Python
36.8%
C
4.6%