Skip to content

Repository files navigation

Fleet Manager (MicroTelemetry)

A lightweight IoT observability and firmware management platform for ESP32 (and ESP8266 via Arduino) fleets. It ingests crash dumps, streams device telemetry over CBOR, and delivers OTA updates via cohort-based rollouts.

Built with Django 5, Celery, PostgreSQL, Redis, and S3-compatible storage (MinIO locally). A React dashboard visualizes fleet health, crashes, and firmware releases.

Features

Capability Agent endpoint Description
Crash reports POST /api/v1/agent/crash-report/ Binary core dump upload → S3 → async xtensa-esp32-elf-gdb symbolication
Heartbeats POST /api/v1/agent/heartbeat/ CBOR metrics (heap, RSSI, battery) buffered in Redis, flushed to Postgres every 60s; response may include a one-shot remote command (e.g. reboot)
OTA pull GET /api/v1/agent/ota-check/ 302 redirect to a signed firmware URL when a newer build exists for the device cohort
Remote reboot (via heartbeat response) Dashboard queues reboot; device applies on next heartbeat (~60s) with command_id dedup
Device registration Dashboard UI + API Register by MAC; per-device X-Device-Token required on all agent calls

Devices are identified by their 6-byte factory MAC as a 12-character lowercase hex string (e.g. 240ac4a1b2c3). They must be registered in the dashboard before the agent API accepts traffic.

Architecture

System overview

Fleet Manager splits traffic into two planes: an agent plane (ESP32 devices, low bandwidth, CBOR/binary) and an operator plane (browser dashboard and Django admin).

flowchart TB
  subgraph devices [ESP32 / ESP8266 Fleet]
    ESP[Device SDK]
  end

  subgraph edge [Operator]
    UI[React Dashboard]
    ADM[Django Admin]
  end

  subgraph docker [Docker Compose Stack]
    NGX[frontend nginx :61294]
    WEB[web Gunicorn :52841]
    CW[celery-worker]
    CB[celery-beat]
    PG[(PostgreSQL)]
    RD[(Redis)]
    S3[(MinIO S3)]
  end

  ESP -->|CBOR heartbeat crash OTA| WEB
  UI -->|/api proxy| NGX
  NGX -->|REST| WEB
  ADM --> WEB
  WEB --> PG
  WEB --> RD
  WEB --> S3
  CW --> PG
  CW --> RD
  CW --> S3
  CB -->|schedule flush| CW
Loading

Components

Layer Technology Role
Device SDK C / ESP-IDF Heartbeats (CBOR), crash dump upload, OTA poll via esp_http_client
Agent API Django (agents/) Device-facing endpoints; returns 503 on DB/storage failure so devices back off
Dashboard API DRF (dashboard/) JSON for the React UI — devices, metrics, crashes, firmware, cohorts
Domain Django (fleet/) Models, business logic in fleet/services/, Celery tasks
Web server Gunicorn + WhiteNoise WSGI app; serves collected static files (admin CSS) when DEBUG=false
Task queue Celery + Redis Async symbolication; periodic heartbeat flush from Redis stream → Postgres
Metadata DB PostgreSQL Devices, cohorts, firmware releases, heartbeat rows, crash report metadata
Buffer Redis Stream High-frequency heartbeats land here before bulk insert (default: 60s flush)
Object storage MinIO (S3 API) Core dumps, .elf symbols, firmware .bin files; presigned URLs for OTA
Frontend React + Vite build → nginx SPA on port 61294; proxies /api and /admin to Gunicorn

Data flows

Heartbeat (telemetry)

sequenceDiagram
  participant D as ESP32
  participant W as Agent API
  participant R as Redis Stream
  participant C as Celery Beat
  participant P as PostgreSQL

  D->>W: POST /agent/heartbeat/ (CBOR)
  W->>R: XADD fleet:heartbeats:stream
  alt pending remote command
    W-->>D: 200 {"status":"ok","command":"reboot","command_id":"42"}
  else no command
    W-->>D: 200 {"status":"ok"}
  end
  C->>C: flush every 60s
  C->>R: XRANGE + XDEL batch
  C->>P: bulk_create HeartbeatMetric
Loading

Views stay thin: decode CBOR, validate fields, enqueue. No per-packet Postgres write.

Crash report (symbolication)

sequenceDiagram
  participant D as ESP32
  participant W as Agent API
  participant S as MinIO
  participant Q as Celery Worker
  participant G as xtensa-esp32-elf-gdb
  participant P as PostgreSQL

  D->>W: POST /agent/crash-report/ (binary)
  W->>S: PUT core dump
  W->>P: CrashReport pending
  W->>Q: symbolicate_crash_report.delay
  W-->>D: 202 Accepted
  Q->>S: GET dump + optional ELF
  Q->>G: batch backtrace
  Q->>P: symbolicated_trace
Loading

Panic-path firmware must not use malloc() or printf() — capture registers/stack in RTC/SPIFFS, upload after reboot.

OTA pull (cohort rollout)

sequenceDiagram
  participant D as ESP32
  participant W as Agent API
  participant P as PostgreSQL
  participant S as MinIO

  D->>W: GET /agent/ota-check/?hw_version&fw_version
  W->>P: Device + cohort + FirmwareRelease
  alt newer firmware for cohort + hw
    W->>S: presigned GET URL
    W-->>D: 302 Location + X-Firmware-Version
  else up to date
    W-->>D: 204 No Content
  end
Loading

OTA matching uses semantic versioning per hw_version and cohort. Devices without a cohort never receive an update.

Domain model

Model Purpose
Cohort Named rollout group (e.g. stable, canary)
Device Primary key = 12-char MAC hex; token_hash for agent auth; tracks hw_version, fw_version, last_seen_at
HeartbeatMetric Time-series rows: heap, min heap, RSSI, battery (indexed columns, not JSON)
CrashReport S3 keys, panic reason, symbolication status/trace
FirmwareRelease Version + hw_version + cohort + s3_key for the binary in object storage
TelemetryThresholdConfig Per-HW version alert thresholds (heap, RSSI, battery, CPU temp) for charts and breach events
DeviceCommand Queued remote actions (reboot) delivered once per heartbeat
FleetEvent Connectivity, threshold breaches, OTA, crash, and reboot events for the Events tab

Docker topology (production compose)

Container Image / build Notes
postgres postgres:16-alpine Persistent volume postgres_data
redis redis:7-alpine Celery broker + heartbeat stream
minio minio/minio API + console ports exposed on host
minio-init minio/mc One-shot: creates fleet-manager bucket, then exits
web Dockerfile → Gunicorn migrate + collectstatic on start; WhiteNoise for /static/
celery-worker same image Runs symbolicate_crash_report and flush tasks
celery-beat same image Schedules heartbeat stream flush (60s default)
frontend frontend/Dockerfile Multi-stage: npm run build → nginx serves SPA + API proxy

Internal DNS names (postgres, redis, minio, web) are used inside the compose network. Host ports are configurable via .env (WEB_PORT, FRONTEND_PORT, etc.).

Design principles

  • Thin views, fat services — request parsing in views; OTA rules, storage, and symbolication in fleet/services/.
  • Structured telemetry — explicit columns on HeartbeatMetric for PostgreSQL indexing and partitioning-friendly schemas.
  • Write buffering — Redis absorbs heartbeat spikes; Celery bulk-writes to Postgres.
  • Fail-safe agents — storage or DB errors surface as 503, not opaque 500s, to protect device batteries and flash wear.

Repository layout

fleet_manager/
├── core/                 # Django project (settings, Celery, URLs)
├── fleet/                # Models, services, Celery tasks, migrations
├── agents/               # Device-facing HTTP API
├── dashboard/            # REST API for the web UI
├── frontend/             # React dashboard (Vite dev / Docker nginx prod)
├── firmware/
│   ├── sdk/              # Shared C headers (reference)
│   └── examples/         # Arduino + ESP-IDF ready-to-flash agents
├── images/               # README screenshots (hardware setup, dashboard, OTA)
├── scripts/              # stop-local.sh, simulate_heartbeat.py
├── docker/               # Gunicorn entrypoint, nginx config for frontend image
├── docker-compose.yml    # Full production stack (local Docker)
├── manage.py
├── requirements.txt
└── README.md

Production stack (Docker)

Requires Docker Desktop running.

# Stop any local runserver / Vite on the same ports
./scripts/stop-local.sh

cp -n .env.example .env   # skip if .env exists
# Ensure .env has DEV_USE_SQLITE=false for Docker

docker compose up --build -d
docker compose exec web python manage.py seed_demo
# If seed_demo issues a device token, copy it for simulate_heartbeat.py / firmware secrets.h

On first stack start, ensure_dashboard_admin creates the dashboard user from DASHBOARD_ADMIN_USERNAME / DASHBOARD_ADMIN_PASSWORD in .env.

Service URL (default host ports)
Dashboard (nginx + React) http://localhost:61294
API (Gunicorn) http://localhost:52841
Django admin http://localhost:52841/admin/
MinIO API (S3) http://localhost:38472
MinIO console http://localhost:41908
Postgres localhost:47291
Redis localhost:58163

MinIO login (console at port 41908 — not admin):

Field Value
Username minioadmin
Password minioadmin

Django and Celery use the same keys via .env (AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY). The minio-init container creates the fleet-manager bucket on first stack start.

Stack: Gunicorn (not runserver), Postgres, Redis, MinIO, Celery worker + beat, built frontend behind nginx. Host ports are configurable in .env to avoid local clashes.

docker compose logs -f web
docker compose exec web python manage.py createsuperuser
docker compose down

Local development (Django on your machine)

You do not need the full docker compose up stack, but Django expects backing services unless you use the SQLite shortcut below.

Option A — Infra only in Docker (recommended)

Run Postgres, Redis, and MinIO in Docker; run Django locally:

cp .env.example .env
docker compose up postgres redis minio minio-init -d

source .venv/bin/activate   # or: python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
python manage.py migrate
python manage.py seed_demo
python manage.py runserver 52841

Connection refused on port 47291 means Postgres is not running — start it with the docker compose up postgres ... line above.

Option B — No Docker (SQLite only)

Good for admin, dashboard API, and migrations. Agent heartbeats still need Redis; crash uploads need MinIO.

cp .env.example .env
echo 'DEV_USE_SQLITE=true' >> .env

python manage.py migrate
python manage.py runserver

Default dev server URL: http://127.0.0.1:8000/ (not 52841 unless you pass that port).

Backend (full local stack)

python -m venv venv
source venv/bin/activate   # Windows: venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env

# Requires Option A infra, or your own Postgres on the port in DATABASE_URL

python manage.py migrate
python manage.py seed_demo
python manage.py runserver 52841

Celery (separate terminals):

celery -A core worker --loglevel=info
celery -A core beat --loglevel=info

Frontend

cd frontend
npm install
npm run dev

Open http://localhost:61294 — Vite proxies /api to Django (WEB_PORT, default 52841).

Tests

pytest

Test with a real ESP32

Full guides: firmware/examples/README.md

Arduino (fastest)

cp firmware/examples/arduino/FleetManagerAgent/secrets.example.h \
   firmware/examples/arduino/FleetManagerAgent/secrets.h
# Edit secrets.h: Wi-Fi, FLEET_API_HOST=<your LAN IP>, FLEET_DEVICE_TOKEN=<from dashboard>
  1. Connect the ESP32 to your computer with USB (if there is a power switch, make sure it is set to OFF). A WROVER module is shown below; any ESP32 dev board works.

    ESP32-WROVER-B connected over USB

  2. In Arduino IDE 2.x, open firmware/examples/arduino/FleetManagerAgent/FleetManagerAgent.ino, select ESP32 Wrover Module (or your board), choose the serial port, and click Upload.

    Arduino IDE compiling and flashing the Fleet Manager agent

  3. Open Serial Monitor @ 115200 baud and note Device ID (Wi‑Fi MAC). Register it in the dashboard (+ Register), copy the one-time token into FLEET_DEVICE_TOKEN in secrets.h, and upload again.

  4. On success you should see heartbeats returning HTTP 200 (not 401).

    Serial Monitor showing heartbeats accepted by the platform

Example output:

=== Fleet Manager Arduino Agent ===
Chip: ESP32-D0WD
Device ID: 30aea4c2cdc4
Device Name: esp32-device
HW 1.0 FW 1.1.3
API: http://192.168.68.108:52841
OTA poll every 300 s
WiFi connecting to Argo_IoT....
WiFi OK
IP=192.168.68.110 RSSI=-51
[crash-report] accepted
[heartbeat] device_id=30aea4c2cdc4 sending...
[heartbeat] OK device_id=30aea4c2cdc4 heap=226920 min_heap=222712 rssi=-51 batt=21 mV cpu=41C
[ota] checking for update (fw 1.1.3)...
[ota] no update

More detail: firmware/examples/arduino/FleetManagerAgent/README.md

Arduino ESP8266 (NodeMCU / Wemos D1)

Same agent API as ESP32, but a separate sketch and .bin. Use HW version 8266 in the dashboard so OTA never mixes chip families.

cp firmware/examples/arduino/FleetManagerAgent8266/secrets.example.h \
   firmware/examples/arduino/FleetManagerAgent8266/secrets.h
# Edit secrets.h: Wi-Fi, FLEET_API_HOST, FLEET_DEVICE_TOKEN, keep FLEET_HW_VERSION "8266"
  1. Install board package esp8266 by ESP8266 Community (Boards Manager).
  2. Open firmware/examples/arduino/FleetManagerAgent8266/FleetManagerAgent8266.ino.
  3. Select your board (e.g. LOLIN(WEMOS) D1 R2 & mini) and an OTA-capable flash size.
  4. Upload; note Device ID in Serial Monitor, register in the dashboard, set FLEET_DEVICE_TOKEN in secrets.h, and upload again.
  5. Expect HW 8266 and heartbeats with HTTP 200.
  6. For OTA: export FleetManagerAgent8266.ino.bin, deploy with HW version 8266 in the Firmware tab.

Example output:

=== Fleet Manager Arduino Agent (ESP8266) ===
Chip ID: 0x755901
Device ID: 600194755901
Device Name: esp8266-device
HW 8266 FW 1.0.1
API: http://192.168.68.108:52841
OTA poll every 300 s
WiFi connecting to Argo_IoT........
WiFi OK
IP=192.168.68.123 RSSI=-60
[crash-report] accepted
[heartbeat] device_id=600194755901 sending...
[heartbeat] OK device_id=600194755901 heap=49248 min_heap=49248 rssi=-59 batt=0 mV
[ota] checking for update (fw 1.0.1)...
[ota] no update

Full guide: firmware/examples/arduino/FleetManagerAgent8266/README.md

Generate OTA binary locally (Arduino IDE)

Use this when you want to push a new build from the dashboard Firmware tab (Deploy OTA Update) instead of flashing over USB.

Before exporting, set Tools → Partition Scheme to an OTA-capable layout (for example Default 4MB with spiffs or Minimal SPIFFS). OTA needs enough flash to hold the running app and the incoming image at the same time.

Bump FLEET_FW_VERSION in secrets.h so the platform treats the build as newer than what devices report today.

Arduino IDE 2.0+ (recommended)

  1. Open firmware/examples/arduino/FleetManagerAgent/FleetManagerAgent.ino.
  2. Tools → Board — select your ESP32 board (e.g. ESP32 Wrover Module).
  3. Sketch → Export Compiled Binary (compiles without uploading).
  4. Sketch → Show Sketch Folder (Ctrl+K / Cmd+K).
  5. Open the new build/ folder, then the board subfolder (e.g. build/esp32.esp32.esp32wrover/).
  6. Use FleetManagerAgent.ino.bin for OTA upload in the dashboard.

Arduino IDE 1.8.x

  1. Open the sketch and select the board under Tools → Board.
  2. Sketch → Export Compiled Binary (Ctrl+Alt+S / Cmd+Option+S).
  3. When compilation finishes, Sketch → Show Sketch Folder.
  4. On 1.8.x the .bin is often next to the .ino file; on newer toolchains it is under build/... as above.

Which .bin to upload?

Arduino exports several binaries. For Fleet Manager OTA, upload only:

File Use for dashboard OTA?
FleetManagerAgent.ino.bin Yes — application firmware
FleetManagerAgent.ino.partitions.bin No — partition table (only when changing flash layout)
FleetManagerAgent.ino.bootloader.bin No — USB/full flash workflows
boot_app0.bin No
FleetManagerAgent.ino.merged.bin No — full-chip image, not for OTA pull

Upload one file per deployment: FleetManagerAgent.ino.bin.

Important tips

  • OTA code must be in the binary you ship. The sketch must include OTA check/apply logic (this repo’s agent does). If you flash a build without OTA support, the next update requires USB again.
  • Version numbers: set Version in the dashboard to match FLEET_FW_VERSION in secrets.h (e.g. 1.1.0). Set HW version to match FLEET_HW_VERSION (e.g. 1.0 for ESP32, 8266 for ESP8266).
  • First install vs OTA: use USB Upload once to get OTA-capable firmware on the device; after that, use the dashboard for subsequent updates.
  • Polling interval: the Arduino agent checks OTA about every 5 minutes (FLEET_OTA_MS) unless overridden in secrets.h. After Send OTA, wait for the next poll or reboot the device to trigger sooner.
  • Presigned URLs: if OTA download fails from the device, set AWS_S3_PUBLIC_ENDPOINT_URL in .env to your LAN MinIO API URL (see .env.example).
  • Deployment stuck at pending / offered: this usually means the device can reach /api/v1/agent/ota-check/ but cannot fetch the binary URL from Location. A common misconfiguration is signed URLs using http://minio:9000/... (Docker-internal host). Configure AWS_S3_PUBLIC_ENDPOINT_URL=http://<your-lan-ip>:<MINIO_API_HOST_PORT> (for example http://192.168.68.108:38472) and restart web.

OTA through the dashboard (end-to-end)

The flow below was verified on a real ESP32 over Wi‑Fi: export the app binary, deploy from the Firmware tab, device downloads from MinIO, reboots on the new version, and the dashboard marks the target updated.

  1. Firmware tab — open Deploy OTA Update, set version/HW, and pick targets (or use Sketch → Export Compiled Binary first).

    Fleet Manager firmware tab — deploy OTA form

  2. Choose the app binary — select FleetManagerAgent.ino.bin from the sketch build/esp32.esp32.*/ folder (not merged.bin or bootloader).

    Choosing FleetManagerAgent.ino.bin for OTA upload

  3. Queue deployment — enter the new version (e.g. 1.1.2), matching HW (e.g. 1.0), select device(s), click Send OTA.

    Selecting target devices and sending OTA

  4. Device applies update — Serial Monitor shows presigned URL download, update applied, reboot, and the new FW line after boot.

    ESP32 serial log — OTA detected, applied, and rebooted

    Example (abbreviated):

    [ota] checking for update (fw 1.0.0)...
    [ota] update available: http://192.168.68.108:38472/fleet-manager/firmware/manual/1.1.2/...
    [ota] version: 1.1.2
    [ota] downloading: http://192.168.68.108:38472/...
    [ota] update applied, rebooting to 1.1.2
    [ota-report] updated sent
    ...
    HW 1.0  FW 1.1.2
    
  5. Dashboard confirmsRecent Deployments shows completed; the device badge shows updated.

    Dashboard — deployment completed and device updated

ESP-IDF / FreeRTOS

cd firmware/examples/esp-idf/fleet_agent
idf.py set-target esp32
idf.py menuconfig   # Fleet Manager Agent → Wi-Fi + API URL
idf.py -p PORT flash monitor

No hardware (simulate from laptop)

docker compose up -d
# Register 240ac4dead01 in the dashboard (or use seed_demo token), then:
FLEET_DEVICE_TOKEN=<token> python3 scripts/simulate_heartbeat.py --device-id 240ac4dead01

Open http://localhost:61294 — log in with DASHBOARD_ADMIN_* credentials. Allow ~60s for Redis flush, or check curl http://localhost:52841/api/v1/dashboard/devices/ (requires session auth).

Important: ESP32 must use your PC's LAN IP (e.g. 192.168.1.42:52841), not 127.0.0.1.

Platform screenshots

Dashboard views from a local run:

View Screenshot
Devices overview Fleet Manager dashboard overview
Events and thresholds Fleet Manager events and threshold settings
Firmware / OTA tab Fleet Manager OTA deployment workflow

For a full USB → export → deploy → device reboot → dashboard completed walkthrough with Serial Monitor proof, see OTA through the dashboard (end-to-end) above.

Dashboard (React UI)

The SPA at http://localhost:61294 (or FRONTEND_PORT) requires login (session cookie). Credentials come from DASHBOARD_ADMIN_USERNAME / DASHBOARD_ADMIN_PASSWORD in .env, created on stack start via ensure_dashboard_admin.

Tab What you can do
Devices + Register new devices (one-time agent token), edit label, rotate token, delete device, per-device telemetry charts, zoom in/out (up to ~1 week of history), Older / Newer / Latest navigation, Restart device (online devices only)
Events Paginated event log (50 per page) with filters: device, time range, severity, metric (threshold breaches)
Firmware Upload .bin, deploy OTA to selected devices (match HW version1.0 for ESP32, 8266 for ESP8266)
Settings Per device-type thresholds (ESP32 vs ESP8266) that drive chart red lines and threshold_breach alerts

Device registration: Click + Register, enter the 12-char MAC (from Serial or board), optional label and HW version. Copy the one-time token into FLEET_DEVICE_TOKEN in firmware secrets.h and reflash via USB. Use Rotate agent token in the edit (✎) modal if the token is lost. Delete device removes the fleet record and telemetry; the same MAC can be registered again later.

Remote restart: On the Devices tab, select an online device and click Restart device. The platform queues a reboot; the agent receives it on the next heartbeat (~60s), stores the command_id in EEPROM/NVS so the same command cannot reboot the device twice, waits 1s, then calls ESP.restart() / esp_restart() outside the HTTP handler.

Telemetry charts: Default window is ~1 hour of points; use Zoom out to see up to ~7 days (requires that much history in Postgres). Use ← Older to pan backward through retained data.

Events filters: Metric applies to threshold_breach events (heap_free_bytes, wifi_rssi_dbm, battery_voltage_mv, cpu_temperature_c). Combine with severity to narrow warnings vs critical issues.

ESP32 gets HTTP 400 on heartbeat or crash-report

If Serial shows [heartbeat] HTTP 400 or [crash-report] HTTP 400 while Wi‑Fi is connected, Django is usually rejecting the Host header. The device calls http://<your-lan-ip>:52841, so that IP must be listed in ALLOWED_HOSTS.

  1. Find your machine's LAN IP (macOS: System Settings → Network, or ipconfig getifaddr en0).

  2. Add it to .env (comma-separated, no spaces):

    ALLOWED_HOSTS=localhost,127.0.0.1,web,192.168.68.108

    .env.example includes a placeholder IP for this reason.

  3. Recreate the web container so the env is picked up:

    docker compose up -d web
  4. Reset the ESP32. You should see [heartbeat] HTTP 200 and, if test crash is enabled, [crash-report] HTTP 202.

A 400 with a short HTML body is typical for DisallowedHost. A 400 with JSON like Missing X-Device-Id means the host is allowed but headers or CBOR are wrong — check X-Device-Id and that fleet_cbor.h uses extern "C" for Arduino builds.

401 / 403 on heartbeat usually means the device is not registered or X-Device-Token is missing or wrong — register in the dashboard and update secrets.h, then reflash via USB (OTA cannot fix a missing token header).

Agent API reference

Devices must be registered in the dashboard before they can call the agent API. Registration issues a per-device token (shown once). Send it on every agent request:

X-Device-Token: <token from dashboard Register device>
Code Meaning
401 Missing or invalid X-Device-Token
403 Device ID not registered (or no token provisioned — rotate token in dashboard)

Heartbeat (CBOR)

POST /api/v1/agent/heartbeat/
Content-Type: application/cbor
X-Device-Id: 240ac4a1b2c3
X-Device-Token: <device token>
X-Hw-Version: 1.0
X-Fw-Version: 1.0.0

CBOR map fields:

  • heap_free (uint) — free heap bytes
  • heap_min_free (uint) — minimum ever free heap
  • wifi_rssi (int) — RSSI in dBm
  • battery_mv (uint, optional) — battery voltage in millivolts
  • cpu_temp_c (int, optional) — CPU temperature in Celsius

Response (JSON):

{"status": "ok"}

When a remote command is pending (e.g. dashboard Restart device):

{
  "status": "ok",
  "command": "reboot",
  "command_id": "42"
}

The agent must persist command_id (EEPROM on ESP8266, NVS on ESP32 / ESP-IDF) and ignore duplicates. Reboot is deferred to the main loop with a ~1s delay after the HTTP transaction completes.

Crash report

POST /api/v1/agent/crash-report/
Content-Type: application/octet-stream
X-Device-Id: 240ac4a1b2c3
X-Device-Token: <device token>
X-Panic-Reason: Guru Meditation Error: LoadProhibited
X-Elf-S3-Key: firmware/1.0.0/build.elf   # optional, for symbolication

Body: raw binary dump. Response 202 with { "id", "status": "accepted" }.

OTA check

GET /api/v1/agent/ota-check/?device_id=240ac4a1b2c3&hw_version=1.0&fw_version=1.0.0
X-Device-Token: <device token>
  • 204 — no update
  • 302Location header is a presigned firmware URL; X-Firmware-Version header set

Database or storage failures return 503 so devices back off instead of retrying indefinitely.

Dashboard API

Base path: /api/v1/dashboard/ — all endpoints except GET /health/ and auth routes require a logged-in session.

Endpoint Description
POST /auth/login/ Dashboard login (username, password)
POST /auth/logout/ End session
GET /auth/session/ Current user
GET /stats/ Fleet summary and default threshold snapshot
GET /devices/ Device list
POST /devices/register/ Register device by MAC; returns one-time token
DELETE /devices/{device_id}/ Remove device and cascade telemetry/events
POST /devices/{device_id}/token/ Rotate agent token; returns new one-time token
GET /devices/{device_id}/metrics/ Heartbeat history (limit, optional end cursor for paging backward in time)
POST /devices/{device_id}/commands/ Queue remote command, e.g. {"command":"reboot"}
PATCH /devices/{device_id}/label/ Update device label
GET /events/ Paginated events (page, device_id, hours, severity, metric)
GET /crashes/ Crash reports
GET /firmware/ Firmware releases
GET /cohorts/ Rollout cohorts
GET /ota/deployments/ OTA deployment history
POST /ota/deployments/ Upload .bin, version, hw version, and target device_ids (multipart form)
GET /thresholds/ List per-HW threshold profiles (?hw_version=8266 for one)
POST /thresholds/ Save thresholds for a given hw_version

Events query parameters:

Parameter Example Purpose
page 2 Page number (50 events per page)
device_id 240ac4a1b2c3 Filter to one device
hours 24 Last N hours (max 30 days)
severity warning info, warning, or critical
metric heap_free_bytes Threshold breach field in details.metric

Metrics query parameters:

Parameter Example Purpose
limit 48 Number of heartbeat points (up to 10080 ≈ 1 week at 1/min)
end ISO timestamp Return points strictly before this time (chart Older navigation)

From the dashboard Firmware tab you can upload FleetManagerAgent.ino.bin or FleetManagerAgent8266.ino.bin, select devices, and queue an update without using Django admin. Cohort-based rollouts via admin still work for devices assigned to a cohort.

Firmware SDK

Reference library: firmware/sdk/README.md.
Flashable examples: firmware/examples/.

Environment variables

See .env.example for DATABASE_URL, REDIS_URL, S3/MinIO credentials, heartbeat flush interval, and online/offline tuning:

  • HEARTBEAT_EXPECTED_INTERVAL_SECONDS (default 60)
  • HEARTBEAT_MISSED_ITERATIONS (default 3)

Devices are considered offline when no heartbeat arrives for EXPECTED_INTERVAL * MISSED_ITERATIONS seconds.

Telemetry charts and threshold_breach events use per-HW profiles (editable in Settings). Env defaults when no DB row exists:

Variable Default Notes
THRESHOLD_HEAP_FREE_BYTES_MIN 50000 ESP32-class devices (hw_version 1.0)
THRESHOLD_HEAP_FREE_BYTES_MIN_8266 35000 ESP8266 — much less RAM than ESP32
THRESHOLD_WIFI_RSSI_DBM_MIN -75 Both families
THRESHOLD_BATTERY_VOLTAGE_MV_MIN 3600 Both families
THRESHOLD_CPU_TEMPERATURE_C_MAX 75 Both families

Use Settings → Device type to tune ESP32 (1.0) and ESP8266 (8266) separately so 8266 devices are not flagged for low heap at ESP32 limits.

License

Free for personal, educational, and internal evaluation use.

Commercial use is not permitted without explicit permission from the project owner.

Recommended license text for this policy: PolyForm Noncommercial 1.0.0
(https://polyformproject.org/licenses/noncommercial/1.0.0/)

Add a LICENSE file in the repository root with the full license text before distribution.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages