# I2C bus 0 SDA = GP4 SCL = GP5 # NeoPixel NP_PIN = 0 # GP0
Config is read from /config.json on the device filesystem at boot via platform.get_config(). Update at runtime via POST /config — device must restart to apply changes to hardware-level settings.
| Key | Default | Description |
|---|---|---|
| min_dist | 50 | Minimum valid distance in mm. Readings below this are clamped. Also the "full" end of the LED scale. |
| max_dist | 1500 | Maximum valid distance in mm. Readings above this are treated as out-of-range (not a sensor failure). Also the "empty" end of the LED scale. |
| alert_dist | 200 | Distance threshold in mm below which LEDs switch to alert brightness (255 vs normal 128). |
| led_count | 10 | Number of NeoPixel LEDs in the strip. |
| brightness | 128 | Normal LED brightness (0–255). Alert brightness is always 255. |
| location | "unknown" | Human-readable location string. Reported in META to the IoT registry. |
{
"min_dist": 50,
"max_dist": 1200,
"alert_dist": 150,
"led_count": 10,
"brightness": 100,
"location": "garage"
}
curl -X POST http://<device-ip>/config \ -H "Content-Type: application/json" \ -d '{"alert_dist": 150, "location": "garage"}'
The app pushes these keys via platform.report() on every sensor loop iteration (~100ms). They appear merged into GET /status alongside platform fields, and are included in each heartbeat to the API.
ok · warning · offline · unknown. See Sensor Logic section.POST /identify endpoint.POST /identify.These are the device-specific endpoints registered by app.py via platform.register_routes(). Platform endpoints (/status, /logs, /restart, etc.) are documented in the platform reference.
{
"distance_mm": 342,
"distance_cm": 34.2
}
{ "status": "high pulse activated (60s)" }
GET /endpoints alongside platform routes.
The sensor loop runs every 100ms via asyncio.sleep_ms(100). Each iteration: read raw value → clamp → update 3-sample rolling average → drive LEDs → push telemetry.
# Reading pipeline raw = tof.read() if raw <= 0: # hardware error → increment failures raise Exception if raw > MAX_DIST_MM: # out of range → NOT a failure, return max return MAX_DIST_MM dist = clamp(raw, MIN_DIST_MM, MAX_DIST_MM) # rolling average over 3 samples → smoothed # num_lit LEDs proportional to (1 - normalised distance)
LED colour is a green→red gradient based on normalised distance. Number of lit LEDs scales inversely with distance — more LEDs lit = object closer.
# Colour: green (far) → red (close) ratio = (MAX_DIST - dist) / (MAX_DIST - MIN_DIST) r = int(ratio * brightness) g = int((1 - ratio) * brightness) color = (r, g, 0) # Alert mode: dist ≤ alert_dist → brightness = 255 brightness = 255 if dist <= ALERT_DIST else BRIGHTNESS_NORMAL
The main run() loop is wrapped in a try/except. On any unhandled exception, the traceback is written to /crash.txt and the loop sleeps 1 second before continuing. The device does not reboot automatically on app crash — only on sensor init failure at startup.
except Exception as e: with open("/crash.txt", "w") as f: sys.print_exception(e, f) platform.log("run() crashed — see crash.txt") await asyncio.sleep_ms(1000)
GET /logs/crash.txt if the device is online but not reporting distance. The crash log survives reboots.
Log files are accessible via the platform's GET /logs and GET /logs/<filename> endpoints.
| File | Written by | Contents |
|---|---|---|
| boot_log.txt | boot.py | Current boot log. WiFi, OTA decisions, file writes. |
| boot_log_1.txt … _5.txt | boot.py | Rotated boot logs from previous boots. Up to 5 kept. |
| platform_log.txt | platform.py | Registry calls, heartbeat errors, route registration. Rotates at 8KB. |
| main_log_1.txt | app.py (legacy) | Heartbeat lines from previous firmware. May be absent after full OTA. |
| crash.txt | app.py | Full Python traceback from last unhandled exception in run(). Overwritten on each crash. |
curl http://<device-ip>/logs/platform_log.txt?lines=30
curl http://<device-ip>/logs/crash.txt