diff --git a/README.md b/README.md index 16a6c89..0298f4f 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,12 @@ python tools/flash_device.py \ 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 @@ -101,7 +107,9 @@ All requests are HMAC-SHA256 signed. See [design spec](docs/superpowers/specs/20 DoorCounter/ ├── firmware/ │ ├── platformio.ini -│ ├── lib/hmac/ — HMAC-SHA256 signing library +│ ├── 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 @@ -111,8 +119,24 @@ DoorCounter/ │ └── 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 +│ ├── 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 +``` diff --git a/tools/serial_monitor.py b/tools/serial_monitor.py new file mode 100755 index 0000000..28f6a37 --- /dev/null +++ b/tools/serial_monitor.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +# Serial monitor for ESP32. Optionally pulses RTS/DTR to reset the device +# so we capture boot output. Prefixes each line with elapsed seconds. +import serial, sys, time, argparse + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--port", default="/dev/ttyUSB0") + ap.add_argument("--baud", type=int, default=115200) + ap.add_argument("--seconds", type=int, default=20) + ap.add_argument("--reset", action="store_true", + help="Pulse RTS/DTR to reset the ESP32 before reading") + ap.add_argument("--timestamp", action="store_true", + help="Prefix each line with elapsed seconds since boot") + args = ap.parse_args() + + try: + s = serial.Serial(args.port, args.baud, timeout=0.2, + rtscts=False, dsrdtr=False) + except Exception as e: + print(f"[open-fail] {e}", flush=True) + sys.exit(2) + + if args.reset: + s.setDTR(False) + s.setRTS(True) + time.sleep(0.1) + s.setRTS(False) + s.reset_input_buffer() + + t0 = time.time() + end = t0 + args.seconds + buf = b"" + while time.time() < end: + chunk = s.read(512) + if chunk: + buf += chunk + while b"\n" in buf: + line, buf = buf.split(b"\n", 1) + text = line.decode("utf-8", errors="replace").rstrip("\r") + if args.timestamp: + sys.stdout.write(f"[{time.time()-t0:5.1f}s] {text}\n") + else: + sys.stdout.write(text + "\n") + sys.stdout.flush() + if buf: + text = buf.decode("utf-8", errors="replace") + if args.timestamp: + sys.stdout.write(f"[{time.time()-t0:5.1f}s] {text}\n") + else: + sys.stdout.write(text) + sys.stdout.flush() + s.close() + +if __name__ == "__main__": + main()