Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CircuitPython Board Inventory

A lightweight way to keep tabs on a fleet of CircuitPython boards. Each board checks in periodically with a small central server, reporting who it is, what firmware it's running, and a few health metrics. A single dashboard then shows the whole fleet at a glance — which boards are healthy, which have gone quiet, what CircuitPython and codebase versions are deployed where, and how much memory and battery each one has left.

Once you're past a handful of boards — sensors scattered around the house, a mix of mains-powered and battery deep-sleep nodes, boards on different codebases — it gets hard to answer simple questions like is everything still reporting? or which boards are still on the old firmware? This tracks that for you: one page, colour-coded by freshness, with per-board history of every version and codebase change.

Board Inventory dashboard

How it works

The server is FastAPI over a single SQLite file — no external database, no message broker. It exposes a small JSON API and serves the dashboard as one self-contained HTML page.

The client is a single file, board_inventory.py, that lives in lib/ on each board. In your code.py you construct a BoardInventory, call checkin() once at startup, and then call checkin_if_due() in your main loop. Checkins are best-effort: any network error is caught and printed, never raised, so the inventory can never take down your actual application.

Two extras ride along on the checkin channel:

  • History is recorded only when something meaningful changes — a CircuitPython upgrade or a codebase deploy — not on every checkin, so the log stays signal and doesn't fill with routine memory/uptime noise.
  • Commands can be queued for a board from the dashboard; the board picks them up on its next checkin. Actions that can take a board offline (reboot, enter_uf2) are opt-in on both ends (see Commands below).

Dashboard status model

Each board shows a coloured dot based on how long it's been since its last checkin:

  • green — seen within ~15.5 minutes (online)
  • yellow — seen within ~1 hour (stale)
  • red — not seen in over an hour (offline)

These thresholds are the ONLINE_SECS and STALE_SECS constants near the top of the <script> block in dashboard.html. Tune them to your fleet's checkin interval plus a little slack. Battery boards that deep-sleep for long stretches will naturally sit at yellow between wakes — that's expected, and a quick way to spot a sleeper that has stopped waking at all (it'll drift to red).

Quick start — Docker

The server ships with a Dockerfile and docker-compose.yml. From the server/ directory:

docker compose up -d --build

The dashboard is then at http://<host>:5000/. The SQLite database lives on a named volume (inventory-data), so it survives rebuilds and restarts. To stop:

docker compose down

(Add -v to also delete the data volume.)

Quick start — FreeBSD (or any Python host)

Docker is the easy path, but the server is plain FastAPI + SQLite and runs fine directly under any Python 3.10+. Brief FreeBSD-jail notes — packages, the Rust build-time dependency, and an rc.d service sketch — are in server/FREEBSD.md. The short version:

pip install -r requirements.txt
uvicorn app:app --host 0.0.0.0 --port 5000

Run it from the directory containing app.py and dashboard.html (the server reads the dashboard from beside itself). The database is created next to app.py unless you set BOARD_INVENTORY_DB.

Upgrading an existing install is just replacing the two files and restarting: on startup the server adds any missing columns (battery, rssi) to your existing database in place, so older data is preserved.

CircuitPython client setup

  1. Copy circuitpython/lib/board_inventory.py into the lib/ folder on the board's CIRCUITPY drive.
  2. Copy circuitpython/settings.toml.example to settings.toml and fill in your Wi-Fi credentials (and, optionally, a friendly hostname) assuming you don't already have a settings.toml.
  3. Set DEFAULT_SERVER_URL near the top of board_inventory.py to your server's address — or override it per board with BOARD_INVENTORY_URL in settings.toml.
  4. Use circuitpython/code.py as a starting point.

Minimal code.py:

import time
from board_inventory import BoardInventory

CODEBASE = "common_sensor"
VERSION = "2.1.0"

inventory = BoardInventory(
    codebase_name=CODEBASE,
    codebase_version=VERSION,
    description="HDC302x temp/humidity, garage west wall",
    aio_group_key="garage-env",
)
inventory.checkin()

while True:
    # ... your application work ...
    inventory.checkin_if_due()
    time.sleep(60)

Reported fields

Field Source
chip_id microcontroller.cpu.uid (the primary key; always sent)
board_type os.uname().machine, shortened for display
hostname explicit, else a settings.toml key, else chip_id
cp_version os.uname().version
ip_address wifi.radio.ipv4_address
codebase_name / codebase_version your CODEBASE / VERSION constants
description free text you set at construction
aio_group_key Adafruit IO group, if you use one
uptime_seconds time.monotonic() — time since power-on
free_memory gc.mem_free() after a collect
rssi running-average Wi-Fi RSSI in dBm
battery free text you set (see below)

Battery / status reporting

battery is deliberately free-form text, so report whatever suits the board — "87%", "4.02V", or "87% / 4.02V". It's read fresh at each checkin, so set it in your loop before the checkin fires:

inventory.battery = f"{read_battery_pct()}%"
inventory.checkin_if_due()

How you read the battery is board-specific (a fuel-gauge breakout, a raw ADC voltage divider, or nothing at all), so it stays in your code.py. Boards without a battery simply never set the attribute and show - on the dashboard.

Battery percentage is color coded by dashboard.html see the constants at the beginning of the script section:

  • green — 30-100%
  • yellow — 15-29%
  • red — below 15%

To report something other than battery, pass any extra keyword to the report and it's stored server-side and shown on the board's detail view — no schema or library change needed. Anything the server doesn't recognise is kept in a JSON extra field.

Commands

The dashboard can queue a command for a board; the board receives it on its next checkin and passes it to a handler you supply:

def handle_command(cmd):
    if cmd.get("action") == "set_debug":
        set_debug(cmd.get("value"))

inventory = BoardInventory(..., command_handler=handle_command)

reboot and enter_uf2 can take a board offline, so they're filtered out before they ever reach your handler unless you opt in with allow_unsafe_commands=True. The dashboard mirrors this: those two actions are hidden from the command menu unless you flip ALLOW_UNSAFE_COMMANDS to true near the top of dashboard.html. This opt-in also means a stray or repeated reboot command can't cycle a board you didn't mean to touch.

API reference

Method Path Purpose
POST /api/checkin Board reports status; response carries any queued commands
GET /api/boards JSON list of all boards
GET /api/boards/{chip_id} One board's detail plus recent history
DELETE /api/boards/{chip_id} Remove a board and all its history
GET /api/history/{chip_id} Version/codebase change history
POST /api/command/{chip_id} Queue a command, e.g. {"action": "set_debug", "value": true}
GET /api/commands/{chip_id} Recent commands for a board
GET / Dashboard UI

Configuration

ServerBOARD_INVENTORY_DB sets the SQLite file path (the Docker image points it at the /data volume; otherwise it defaults beside app.py).

Client settings.toml keys, all optional:

  • BOARD_INVENTORY_URL — override the server URL for this board
  • BOARD_INVENTORY_HOSTNAME — friendly name on the dashboard
  • CIRCUITPY_WIFI_HOSTNAME / CIRCUITPY_WEB_INSTANCE_NAME — used as hostname fallbacks before chip_id

Client constructor options include checkin_interval (default 300s), request_timeout, allow_unsafe_commands, and socket_pool / ssl_context / session for sharing one connection manager with other network code on the board. See the docstring at the top of board_inventory.py for the full list.

A note on security

There's no authentication on the API or dashboard — anyone who can reach the port can view the fleet, queue commands, and delete boards. It's built for a trusted LAN. Don't expose it directly to the internet; put it behind a VPN, a reverse proxy with auth, or a firewall rule if you need remote access.

Repository layout

GRGrant_CircuitPython_board_inventory/
├── server/
│   ├── app.py               FastAPI + SQLite server
│   ├── dashboard.html       single-page dashboard
│   ├── requirements.txt
│   ├── Dockerfile
│   ├── docker-compose.yml
│   └── FREEBSD.md           native FreeBSD-jail notes
├── circuitpython/
│   ├── lib/
│   │   └── board_inventory.py   the client library (copy to lib/)
│   ├── code.py              example usage
│   └── settings.toml.example
├── LICENSE
└── README.md

License

MIT — see LICENSE.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages