- 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>
143 lines
5.4 KiB
Markdown
143 lines
5.4 KiB
Markdown
# 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 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
|
||
|
||
```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.
|
||
|
||
> **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](#troubleshooting).
|
||
|
||
### 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.
|
||
|
||
## 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:
|
||
|
||
```bash
|
||
python tools/serial_monitor.py --port /dev/ttyUSB0 --reset --timestamp --seconds 30
|
||
```
|