Networked serial console + USB HID keyboard injection for a fleet of headless machines — hub server, client SDK, and wire protocol.
Project description
Hub — the picotty package
The management side, packaged as the picotty Python distribution (uv). One
process on one asyncio event loop runs two faces at once — a raw TCP server for
the node swarm and a FastAPI app for the browser — over a shared in-memory
registry and one SQLite database.
One distribution, three import surfaces:
| Import | What | Needs |
|---|---|---|
picotty.hub |
the server (registry + SQLite + :9000 TCP + FastAPI dashboard) | [hub] extra |
picotty.client |
the SDK: HubClient (REST) + HubEvents (WebSocket) |
base install |
picotty.protocol |
wire framing, validation, PROTOCOL_VERSION |
base install |
The lean base (httpx + websockets) is all a client needs; the server stack
(FastAPI, uvicorn, aiosqlite, pydantic, pyyaml) comes from the [hub] extra, so a
Pi Zero 2 W running only the Telegram sidecar stays small.
Layout
hub/
pyproject.toml # the picotty distribution (uv_build backend)
uv.lock # pinned, reproducible installs
.python-version # 3.11 floor
src/picotty/
__init__.py # version, single-sourced from distribution metadata
protocol.py # public wire-protocol surface (re-exports hub/protocol.py)
client/ # the SDK: HubClient (REST) + HubEvents (WS async iterator)
sim.py # the node simulator (picotty-sim console script)
static/ # the dashboard, shipped inside the wheel (served at /)
hub/ # the server
main.py # wires TCP + FastAPI + background tasks on one loop
config.py # process config (env) + operator-tunable defaults
protocol.py # length-prefixed JSON framing (authoritative)
registry.py # in-memory NodeState registry
db.py # aiosqlite: schema, queries, batched output, retention
eventbus.py # WebSocket fan-out with subscription filtering
core.py # the shared Hub: dispatch, ping/pong, audit, view-merge
tcp_server.py # swarm face on :9000
telegram_setup.py # writes the sidecar .env for the dashboard's Telegram page
tasks.py # sweep, output flush, retention, stats, loop-lag
api/{rest,ws,models}.py
tests/ # test_db.py, test_integration.py, driver.py
scripts/ # install / run / systemd unit
Run it (from source, with uv)
bash scripts/install.sh # uv sync --extra hub (+ fetches terminal libs)
bash scripts/run.sh # foreground (dev), loads private/hub-token.txt
# or install as a service that starts on boot:
bash scripts/install-service.sh # renders + enables the systemd unit
journalctl -u swarm-hub -f # follow logs
Manual equivalent:
cd hub
uv sync --extra hub
uv run --extra hub picotty-hub
Or install it as a tool (no repo checkout): uv tool install picotty, which puts
picotty-hub and picotty-sim on PATH. See ../docs/packaging.md.
On first start the hub mints a shared node token and prints it once — put it
in each node's settings.toml as NODE_TOKEN. It listens on :9000 (swarm) and
:8080 (browser). Open http://localhost:8080 for the dashboard.
Configuration via environment (all optional): HUB_TCP_PORT, HUB_HTTP_PORT,
HUB_DB_PATH, HUB_STATIC_DIR, HUB_TCP_HOST, HUB_HTTP_HOST, TELEGRAM_ENV_PATH.
Runtime state (the SQLite DB) defaults to ~/.local/share/picotty/hub.db (honors
XDG_DATA_HOME); the systemd unit uses /var/lib/picotty. Static assets ship
inside the wheel. Operator-tunable settings (heartbeat, stale timeout, retention,
confirm-dangerous, alerts) live in the DB and change via PATCH /api/settings.
Run under one uvicorn worker only. The single event loop is the design: a second worker would get its own registry and node sockets and the two would disagree about who is online.
Test without hardware
Start the hub, grab the printed token, then run one or more fake nodes with the packaged simulator:
uv run picotty-sim --id node-01 --token <TOKEN>
uv run picotty-sim --id node-02 --token <TOKEN>
Each simulator connects, heartbeats, answers commands, and streams fake serial output — enough to exercise the full dashboard. The suites:
uv run python tests/test_db.py # offline db checks
uv run python tests/test_integration.py # end-to-end over real TCP + REST
Using the client SDK
from picotty.client import HubClient
async with HubClient("http://hub:8080") as hub:
print(await hub.health())
async with hub.events_stream() as stream: # the /ws feed
await stream.subscribe("node-01")
async for ev in stream:
...
This is what the Telegram sidecar imports; it needs only the base install.
Notes
- Registry is disposable. On restart it starts empty and refills as nodes reconnect; the SQLite record survives.
- Output is batched to SQLite (default every 500 ms) to protect SD-card write throughput; the live WebSocket stream is immediate and independent.
- Auth is optional and off by default — the design assumes an isolated management VLAN reached through a VPN/tunnel, not port exposure.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file picotty-1.0.2.tar.gz.
File metadata
- Download URL: picotty-1.0.2.tar.gz
- Upload date:
- Size: 280.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4bb597c2ce501c57d821d8168d0e9b60970832af360f628196e54f267ff0092b
|
|
| MD5 |
47cbd7bb655abdacbf4c81857004e64e
|
|
| BLAKE2b-256 |
c827079d840d187631b7a77e0c5046e8d3c1c885630b911cc5c0c616bc6a7c34
|
Provenance
The following attestation bundles were made for picotty-1.0.2.tar.gz:
Publisher:
publish.yml on morpheuslord/PICOTTY
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
picotty-1.0.2.tar.gz -
Subject digest:
4bb597c2ce501c57d821d8168d0e9b60970832af360f628196e54f267ff0092b - Sigstore transparency entry: 2343847363
- Sigstore integration time:
-
Permalink:
morpheuslord/PICOTTY@eebeac80b52766cf6a60fc36307478586efd4e71 -
Branch / Tag:
refs/tags/1.0.2 - Owner: https://github.com/morpheuslord
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@eebeac80b52766cf6a60fc36307478586efd4e71 -
Trigger Event:
release
-
Statement type:
File details
Details for the file picotty-1.0.2-py3-none-any.whl.
File metadata
- Download URL: picotty-1.0.2-py3-none-any.whl
- Upload date:
- Size: 296.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
64ba0d95c1355e2f258c2df73915e2b10984942691d3d25a85d35b6816e1e8d8
|
|
| MD5 |
20d1c3f043644c3ae1e01a4c61f7f5e6
|
|
| BLAKE2b-256 |
2a1457aa96bb728d7b53def2392b57c1f3406ab2eb42cfc7b567158021192907
|
Provenance
The following attestation bundles were made for picotty-1.0.2-py3-none-any.whl:
Publisher:
publish.yml on morpheuslord/PICOTTY
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
picotty-1.0.2-py3-none-any.whl -
Subject digest:
64ba0d95c1355e2f258c2df73915e2b10984942691d3d25a85d35b6816e1e8d8 - Sigstore transparency entry: 2343847368
- Sigstore integration time:
-
Permalink:
morpheuslord/PICOTTY@eebeac80b52766cf6a60fc36307478586efd4e71 -
Branch / Tag:
refs/tags/1.0.2 - Owner: https://github.com/morpheuslord
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@eebeac80b52766cf6a60fc36307478586efd4e71 -
Trigger Event:
release
-
Statement type: