Files
DoorCounter/README.md
Peter Woolery 24aaae6ff2 docs: add Troubleshooting section + serial_monitor.py diagnostic tool
- README: note NVS may be cleared by firmware uploads (requires re-running
  flash_device.py); new Troubleshooting table covering the fast-blink fatal
  state, captive-portal fallback, and no-counts cases.
- tools/serial_monitor.py: ESP32 RTS/DTR reset + serial capture with
  per-line elapsed-time prefix. Used to distinguish "unprovisioned" vs
  "WiFi failed" boot states (fast-blink LED alone is ambiguous).
- README project-tree updated to include lib/cv, docs/server-prompt-…,
  and the new tool.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 07:05:21 -07:00

5.4 KiB
Raw Blame History

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 with per-direction cooldown
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

Crossing cooldown

To suppress double-counts from track churn (a blob briefly dropping below the minimum-blob-pixel threshold, causing the tracker to kill and respawn a track that then re-crosses the line), each direction enforces a cooldown window between counted crossings. Default: CV_CROSSING_COOLDOWN_FRAMES = 5, which suppresses any second crossing in the same direction whose frame gap is < 5 — ≈0.8s at 5 fps. Entries and exits maintain separate cooldowns, so a real entry immediately followed by a real exit still counts both. See firmware/lib/cv/cv.h.

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.

Re-provision after firmware uploads. Flashing firmware via pio run -t upload may clear the NVS partition on this board. If the device boots into a ~1 Hz LED blink (the "not provisioned" fatal state) after a firmware update, re-run flash_device.py with the same credentials. See Troubleshooting.

3. OTA updates

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 for full API shapes and auth scheme.

Project Structure

DoorCounter/
├── firmware/
│   ├── platformio.ini
│   ├── lib/
│   │   ├── cv/            — CV pipeline (blob tracking, line cross, cooldown)
│   │   └── 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
│   └── serial_monitor.py  — reset + read serial with timestamps (diagnostic)
├── docs/
│   ├── server-prompt-crossing-cooldown.md — server-side coordination notes
│   └── superpowers/specs/2026-04-13-door-counter-design.md
└── server/                — API server (separate deployment)

Troubleshooting

Symptom Likely cause Remedy
~1 Hz LED blink after boot, no serial beyond esp_core_dump_flash: No core dump partition found! NVS missing device_id / location_id / hmac_secret. Commonly triggered by a firmware upload wiping NVS. Re-run flash_device.py with the device's known credentials.
Device stays on DoorCounter-Setup AP instead of joining customer WiFi SSID/password in NVS wrong, or network out of range. Connect phone to DoorCounter-Setup → captive portal → re-enter WiFi. Or reflash NVS with correct --wifi-ssid / --wifi-password.
No entries/exits counted for a known-walking doorway WiFi captive portal still up (camera task starts only after connect); or camera blocked/unfocused. Check LED: solid on = booting/uploading, off = counting. Run serial_monitor.py to see [CV] entry/exit log lines.

Capture a boot log with timestamps:

python tools/serial_monitor.py --port /dev/ttyUSB0 --reset --timestamp --seconds 30