# I2C bus 0 SDA = GP4 SCL = GP5 # NeoPixel NP_PIN = 0 # GP0
The VL53L0X is initialised at module load (before run() is called) with up to 5 attempts and 500 ms between each. If all fail, the device logs the error and calls machine.reset() after a 3-second delay — the next boot will OTA-sync and retry.
If the sensor fails mid-run (20 consecutive bad reads), a reinitialisation is attempted in-loop via VL53L0X(i2c) without rebooting. Sensor status is reported as "offline" until a successful read recovers it.
Config is read from /config.json on the device filesystem at boot via platform.get_config(). All keys are optional — defaults are applied in code if a key is absent. Update at runtime via POST /config; hardware-level settings (pin assignments, LED count) require a reboot to take effect.
| Key | Default | Description |
|---|---|---|
| min_dist | 50 | Minimum valid distance in mm. Readings below this are clamped upward. Also the "full" end of the LED scale. |
| max_dist | 1500 | Maximum valid distance in mm. Readings above this are returned as max_dist — not counted as a sensor failure. Also the "empty" end of the LED scale. |
| alert_dist | 200 | Distance threshold in mm. Below this, LEDs switch to full alert brightness (255) regardless of the normal brightness setting. |
| led_count | 10 | Number of NeoPixel LEDs in the strip. Requires reboot to take effect. |
| brightness | 128 | Normal LED brightness (0–255). Alert brightness is always 255. |
| location | "unknown" | Human-readable location label. Written into META and reported to the IoT registry at startup. |
{
"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 main run() coroutine executes at approximately 10 Hz (asyncio.sleep_ms(100)). Each iteration reads the sensor, smooths the value, drives the LED bar, and pushes telemetry to the platform.
# 1. Raw read raw = tof.read() if raw <= 0: # hardware error → increment failures, return max raise Exception("invalid low reading") if raw > MAX_DIST_MM: # out-of-range — NOT a failure return MAX_DIST_MM dist = clamp(raw, MIN_DIST_MM, MAX_DIST_MM) # 2. Smooth (3-sample rolling average) _history.pop(0) _history.append(dist) smoothed = sum(_history) / len(_history) # 3. LED bar: more LEDs lit = object closer num_lit = int((1 - (smoothed - MIN_DIST) / (MAX_DIST - MIN_DIST)) * NUM_LEDS) num_lit = clamp(num_lit, 0, NUM_LEDS) # 4. Colour: green (far) → red (close) ratio = (MAX_DIST - smoothed) / (MAX_DIST - MIN_DIST) brightness = 255 if smoothed <= ALERT_DIST else BRIGHTNESS_NORMAL color = (int(ratio * brightness), int((1 - ratio) * brightness), 0)
The entire 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 an app crash — only on sensor init failure at startup or on OTA update.
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 file survives reboots and is overwritten on each new crash.
The app pushes these keys via platform.report() on every sensor loop iteration (~100 ms). They appear merged into GET /status alongside platform fields, and are included in each heartbeat to the API.
ok · warning · offline · unknown. See Sensor Loop section.app.run() started. Distinct from the platform's own uptime_ms (which counts from main.py start).POST /identify endpoint for the blue-flash identification blink.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 manually.
{ "status": "high pulse activated (60s)" }
GET /endpoints alongside the platform routes.
Log files are accessible via the platform's GET /logs and GET /logs/<filename> endpoints. The ?lines=N query param returns the last N lines of any file.
| File | Written by | Contents |
|---|---|---|
| boot_log.txt | boot.py | Current boot session. WiFi state, OTA decisions, file writes, time sync, device_id. |
| boot_log_1.txt … _5.txt | boot.py | Rotated boot logs from the previous five boots. |
| platform_log.txt | platform.py | Registry calls, heartbeat results, route registration. Auto-rotates at 8 KB. |
| crash.txt | app.py | Full Python traceback from the most recent unhandled exception in run(). Overwritten on each crash. Absent if no crash has occurred. |
# Tail platform log (last 30 lines) curl http://<device-ip>/logs/platform_log.txt?lines=30 # Check for a crash curl http://<device-ip>/logs/crash.txt # Most recent boot log curl http://<device-ip>/logs/boot_log.txt