docs(readme): add quick-start, hardware sources, power draw + latency notes

Adds a sourced parts table (M5 TimerCamera-F, USB cable, 5V adapter), the
~750 mW measured power draw, the 3-5s detection latency caveat, and a
six-step Quick Start aimed at semi-technical operators deploying their
own device.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-27 14:26:45 -07:00
parent 268b595340
commit be44299d3e

123
README.md
View File

@@ -2,13 +2,95 @@
Retail door traffic counter using M5Stack TimerCamera-F (ESP32 + OV3660). Counts walker traversals via overhead camera CV, passively scans BLE foot traffic, and reports hourly to `logs.research.bike`.
> **Known limitation — directional accuracy.** This firmware reports counts as `{entries, exits}` for API compatibility, but **per-walk direction labelling is not reliable at the current mount (7' overhead, straight down).** In bench testing, event detection was 100% (8/8 walks detected) while per-walk direction matched the physical walk only ~50% of the time — the centroid trajectories produced by entries and exits were nearly indistinguishable. **The number to trust is gross traffic: `entries + exits` ≈ total walkers through the doorway.** The directional split is an unreliable best-effort heuristic. See [Directional counting](#directional-counting) for why.
> **Known limitations.**
> - **Directional accuracy.** Counts are reported as `{entries, exits}` for API compatibility, but **per-walk direction labelling is not reliable at the current mount (7' overhead, straight down).** Bench testing: event detection 100% (8/8), per-walk direction ~50% (coin flip). **Trust gross traffic: `entries + exits` ≈ total walkers.** See [Directional counting](#directional-counting).
> - **Detection latency.** A walker takes **35 seconds** from entering the FOV to being registered as a count — the state machine waits for the walker to clear the frame (or a 5s timeout) before finalizing. Counts are not instantaneous; hourly aggregation is the intended consumption mode.
## 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)
| Component | Source | Notes |
|-----------|--------|-------|
| **Camera** | [M5Stack TimerCamera-F (OV3660 fisheye, PSRAM)](https://shop.m5stack.com/products/esp32-psram-timer-camera-fisheye-ov3660) | ESP32 + WiFi/BLE on board |
| **USB cable** | [USB-A → USB-C, right-angle](https://www.amazon.com/dp/B0DWMPVP4F) | Right-angle plug helps with overhead mounts |
| **Power supply** | [5V USB wall adapter](https://www.amazon.com/dp/B0B2WLSY9D) | Any 5V/1A+ USB charger works |
- **Mount**: Overhead, camera pointing straight down, centered above doorway (~7' / 2.1m height)
- **Power draw**: **~750 mW measured at the wall** (camera + WiFi + BLE all active). Runs cool — fanless, can be sealed in a small enclosure. Annual energy cost at US residential rates is well under $1.
## Quick Start (semi-technical)
The fastest path from "box arrived" to "counts in the dashboard." Comfortable with a terminal but not necessarily an embedded developer? Start here.
**You will need**: the camera + cable + power supply listed above, a Linux/macOS computer with USB, and ~20 minutes.
### 1. Install the toolchain (one-time)
```bash
# Python 3.10+ and pip
pip install --user platformio esptool esp-idf-nvs-partition-gen
```
PlatformIO installs the ESP32 compiler on first build — expect a few minutes the first time.
### 2. Clone this repo
```bash
git clone https://github.com/<your-org>/DoorCounter.git
cd DoorCounter
```
### 3. Plug the camera in
Connect the USB-C cable to the TimerCamera and the other end to your computer. On Linux it appears as `/dev/ttyUSB0`; on macOS as `/dev/tty.usbserial-*`. If you don't see it, install [CP210x USB drivers](https://www.silabs.com/developer-tools/usb-to-uart-bridge-vcp-drivers).
### 4. Flash the firmware
```bash
cd firmware
pio run -t upload --upload-port /dev/ttyUSB0
```
### 5. Provision the device with its credentials
Pick a unique device ID (e.g. `dc-0001`), a location ID, and generate a 32-byte HMAC secret. The server admin must record this same secret — counts won't be accepted without it.
```bash
# Generate a fresh secret
openssl rand -hex 32 > my-device-secret.txt
# Provision
python tools/flash_device.py \
--port /dev/ttyUSB0 \
--device-id dc-0001 \
--location-id my-store \
--hmac-secret "$(cat my-device-secret.txt)" \
--wifi-ssid "MyStoreWiFi" \
--wifi-password "wifi-password-here"
```
> If you skip `--wifi-ssid`/`--wifi-password`, the device opens a `DoorCounter-Setup` WiFi access point on boot. Connect a phone to it and enter the credentials in the captive portal.
### 6. Mount the device
1. Position above the doorway, camera lens pointing straight down (~7' / 2.1m up).
2. Plug into the wall adapter — that's it. The LED turns red while joining WiFi, then off once it's counting.
3. First heartbeat lands at the server within ~60 seconds; first hourly count batch arrives at the top of the next hour.
### What "working" looks like
- LED behavior: **off** = counting normally · **red** = no WiFi · **yellow** = uploading · **brief flash** when a walker is registered (1 flash = entry, 2 flashes = exit).
- A walker takes 35 seconds from entering the FOV to triggering the LED flash — this is normal.
- Hourly uploads to `logs.research.bike` (or your configured server) include the entry/exit counts since the last report.
### If something is off
| Symptom | Try |
|---------|-----|
| Red LED stays on | Wrong WiFi password — re-run step 5, or use the `DoorCounter-Setup` captive portal. |
| LED blinks ~1 Hz forever (or device reboots in a loop) | NVS got wiped — re-run step 5 with the same credentials. |
| No counts appearing on server | Run `python tools/serial_monitor.py --port /dev/ttyUSB0 --reset --timestamp --seconds 30` and watch for `[CV] entry/exit` lines as you walk under it. |
For deeper troubleshooting see [Troubleshooting](#troubleshooting) and [Operator Setup](#operator-setup).
## Firmware
@@ -123,10 +205,29 @@ python tools/flash_device.py \
WiFi credentials are optional — if omitted, device starts captive portal on boot.
**Known-good command for dc-0002** (dev device at research.bike):
```bash
python tools/flash_device.py \
--port /dev/ttyUSB0 \
--device-id dc-0002 \
--location-id retailer-123 \
--hmac-secret "$(cat .agent/dc-0002-secret)" \
--wifi-ssid Elly-Fi \
--wifi-password <ask> \
--line-offset 50
```
Secret is stored in `.agent/dc-0002-secret` (gitignored). Server must already
know this secret — do not rotate without updating the server side.
> **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
> `pio run -t upload` may clear the NVS partition on this board.
> - **FW 1.0**: device boots into a ~1 Hz LED blink (hang in "not provisioned" fatal).
> - **FW 1.1+**: device reboot-loops with `FATAL: device_id/location_id/hmac_secret not provisioned`
> followed by `rst:0xc (SW_CPU_RESET)` (FATAL paths now reboot instead of hang).
>
> Either way, re-run `flash_device.py` with the same credentials. See
> [Troubleshooting](#troubleshooting).
### 3. OTA updates
@@ -188,7 +289,7 @@ DoorCounter/
| 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. |
| ~1 Hz LED blink after boot (FW 1.0), OR reboot loop with `FATAL: device_id/location_id/hmac_secret not provisioned``rst:0xc (SW_CPU_RESET)` (FW 1.1+) | NVS missing `device_id` / `location_id` / `hmac_secret`. Commonly triggered by a firmware upload wiping NVS. FW 1.1+ reboots on FATAL instead of hanging. | Re-run `flash_device.py` with the device's known credentials (see section 2 for dc-0002). |
| 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. |
@@ -228,6 +329,12 @@ flash any device.
cd firmware && pio run -e timercam -t upload
```
> **If the device reboot-loops after flashing** with `FATAL:
> device_id/location_id/hmac_secret not provisioned`, NVS was wiped. Re-run
> `flash_device.py` (see [section 2](#2-provision-device-identity)). FW 1.1
> turned the old FW 1.0 LED-blink hang into an explicit reboot loop; same
> root cause, same fix.
### Expected first boot
On the serial log (115200 baud), the device prints the boot banner, then