Releases: morpheuslord/PICOTTY
Release list
v1.2.0 — 2026-09-06
v1.2.0 — 2026-09-06
Building on 1.1.0's dual-hub work: OTA gains firmware-only / settings-only
pushes plus a big speed-up, and the Telegram bot becomes button-driven.
Added — OTA firmware-only / settings-only + faster, more resilient pushes
- Scoped pushes. An OTA push now takes a scope:
all(default),
firmware(everything exceptsettings.toml— update the code, keep each
node's existing config) orsettings(onlysettings.toml— change config,
keep the code). Exposed in the dashboard's Update-firmware sheet, on
POST /api/nodes/{id}/otaand/api/bulk/ota(scope), and in the OTA manager.
Works with existing nodes — the node writes exactly the files the hub sends, so
no firmware change is needed. (Tip: asettings-only push is how you retune a
board'sHUB_FAILOVER_TRIES/timings without reflashing code.) - ~8× faster. The OTA chunk size went from 512 B to 4096 B (still well under
the node's 16 KB frame cap), cutting round-trips. - Resilient transfer. A transient link drop mid-transfer now retries the whole
transfer (safe —ota_beginre-wipes staging and nothing is swapped until
commit) instead of failing the push; commit is still one-shot, checksum-verified,
with.bak/watchdog auto-revert.
Added — Telegram button UI (picotty-telegram 1.3.0)
The bot is now tap-driven, not type-driven. /menu (and /start) opens an
inline-keyboard home screen — Status, Nodes, Telemetry, Events, Fleet, Alerts,
Source hub, Arm — and everything drills down by tapping:
- Nodes → node detail with action buttons: Ping, Read, Telemetry, Log, Mute,
and (when armed) Shell, Reboot (confirm step), SysRq (key picker), Move hub. - Fleet → Macros / Runbooks as buttons, then a "run on ALL online / this node"
target picker. - Source hub switch and per-node hub steering are button flows too.
- Arm by tapping 🔒 then sending your TOTP code (no
/armneeded). - Context memory: the bot remembers your selected node and the pending prompt,
so buttons act without re-typing ids; the home screen shows live hub/online/armed
status. Every typed command still works.
Changed — clearer hub-switch controls
The dashboard's dual-hub control is now a prominent ⤓ Take over button when a
board is live on a peer hub, alongside a clearly-labelled ⇄ Switch hub control
(it was previously just an unlabelled "Hub" button).
Fixed — Magic SysRq chord
The Alt+SysRq+B quick-chord (and any custom chord naming SysRq) was sent as a
plain HID chord, which failed on the node with unknown keycode: 'SYSRQ' (SysRq is
the PrintScreen key, and it must be held while the command key is tapped). Such
chords now route to the dedicated /sysrq command, which does the correct timing.
Also fixed HubClient.sysrq() sending the key under the wrong field (command
instead of key), so a Telegram /sysrq <node> <key> no longer silently defaulted
to b (reboot).
Verified
Hub 7/7 db + 61/61 integration (incl. OTA firmware/settings scope + empty→422);
sidecar 28/28 unit (incl. the button-menu callback scheme) + wiring smoke; no
regressions.
v1.1.0 - 2026-08-22
Two features: a board can now be served by two hubs with automatic failover
and runtime source-selection, and the Telegram bot gains a much larger command
surface.
Added — dual-hub failover
Give a node a primary and a backup hub. It prefers the primary, fails over
to the backup when the primary is unreachable, and either hub can drive the
board — so a dead main hub no longer means losing the fleet. See
docs/dual-hub.md.
- Independent peers. The two hubs run independently, each with its own SQLite
DB; they share only the node token. Failover moves the live control
connection, not the data — a board holds exactly one connection at a time, so no
split-brain and no cross-hub coordination. Per-hub data (queue, recordings, OTA
jobs, history) stays on the hub that recorded it. - Node config (settings.toml):
HUB_IP_BACKUP/HUB_PORT_BACKUP, display labelsHUB_LABEL/HUB_LABEL_BACKUP,
HUB_FAILBACK(stickydefault /preemptive),HUB_FAILOVER_TRIES. Absent
backup = today's single-hub behavior, fully backward compatible. The failover
state machine lives in a new, host-testable
hubselect.py. - Runtime steering. Each hub advertises its identity to a board on connect (a
welcomeframe carrying the newHUB_ID), so the board knows which hub it's
talking to. An authenticated hub can then send a directive — switch (move
now), prefer (promote to home hub, persisted — "use my backup as my main"),
pin/unpin — restricted to the board's configured hubs. Drive it from the
dashboard's per-node Hub control,POST /api/nodes/{id}/hub, or Telegram. - Hub: new
HUB_IDenv (default hostname), shown on the dashboard and sent in
the welcome frame; a node's reported hub label is surfaced in the node API and on
the dashboard (a "via <label>" badge). Node firmware bumped to 1.3.0.
Added — richer Telegram bot (picotty-telegram 1.2.0)
Surfaced hub capabilities that were REST-ready but had no chat command:
- Read-only (allowlist tier):
/events [n](recent audit),/log <node> [lines](recent console output),/search <text>(search console history),
/ping <node>(active RTT). - Fleet (break-glass armed tier):
/bulk <text>(type a line into every
online node),/macros+/runmacro <id> [node ...],/runbooks+/runbook <id> <node ...|all|group:NAME>. - Dual-hub:
/hubs(which hub each board is on) and/hub <node> <switch|prefer|pin|unpin> [target](armed). - New
picotty.clientSDK methods back these (node_output,output_search,
ping,bulk_cmd,macros/run_macro,runbooks/run_runbook,
hub_directive), so any client — not just the bot — can use them.
Added — cross-hub takeover & peer visibility
A board holds one hub connection at a time, so only the hub that holds it can
steer it. New HUB_PEERS (comma-separated peer hub URLs) closes the gap:
- Peer visibility. Each hub polls its peers' rosters and shows a board that's
live on the other hub as "active on<peer>" (a badge) instead of offline —
even a board it has never seen. - On-demand takeover from either hub. Steering a board the hub doesn't hold
relays the directive to the peer that does, which delivers it — so you can
pull a board to the backup from the backup's own dashboard while it's still on
the primary. Relayed calls are never relayed again (loop-safe). Peer calls
assume a trusted management VLAN (no auth), matching the node-token posture.
Added — Telegram sidecar dual-hub
- The bot fails over between hubs. Set
HUB_BASE_URL_BACKUPand the sidecar
prefers the primary hub, failing over to the backup (REST + the live event
stream) so the phone control plane survives a hub outage. /source [primary|backup]picks which hub the bot acts on (distinct from
/hub, which steers an individual board). Telegram permits only one active
receiver per token, so the intended HA is: run the same bot on both hosts with
one enabled and the other a hot spare — see docs/dual-hub.md.
Fixed — responsive dashboard
The dashboard (a fixed-width desktop layout) now adapts to phones, tablets, and
any window aspect ratio: the side rails are fluid on desktop and the three columns
stack with the console kept usably tall on narrow screens, with the page scrolling
instead of clipping. CSS-only; no behavior change.
Verified
Firmware selector 12/12 host unit; hub 7/7 db + 58/58 integration (welcome
frame, hub-label, directive endpoint) + 5/5 two-hub relay (cross-hub takeover,
peer visibility, loop guard); sidecar 27/27 unit (incl. hub-failover + source
select) + wiring smoke + 18/18 end-to-end through picotty.client. No
regressions.
v1.0.3 - 2026-08-12
What's Changed
- Active issue by @morpheuslord in #5
v1.0.3 — 2026-08-12
A reliability pass on node presence: nodes no longer get stuck showing offline
while alive, both ends recover from a dropped link on their own, and the
dashboard now surfaces link quality and node activity so a degrading node is
visible before it drops.
Added — link telemetry & activity monitoring
Precautionary observability so a degrading node is visible before it drops:
- Network telemetry. The hub's per-node ping now feeds a rolling window that
yields RTT avg/min/max, jitter (mean variation between consecutive pings),
and packet loss %. Surfaced per node in the API and dashboard, with a
livenode_netevent and a colour-coded link quality badge (good/fair/poor). - Activity / uptime. Nodes report firmware uptime in the heartbeat; the
hub shows it and raises anode_downevent when it jumps backwards (an
unannounced reboot). The hub also counts reconnects per node this run, so a
flapping link is obvious even while the node reads "online".
Fixed — nodes falsely shown offline (stuck-offline / reconnection)
A node that was alive and networked could show offline in the dashboard
indefinitely, only recovering when the hub was restarted (no node restart
needed). Root cause: when the hub's liveness sweep flipped a stale node offline
it removed it from the registry without closing the socket, so the still-open
connection kept delivering the node's frames onto a detached state — the node
never re-registered. Hardened both sides of the link:
- Hub — sweep now tears down the socket.
mark_offlinecloses the node's
writer, forcing a reconnect instead of a lingering half-open connection. - Hub — TCP keepalive on accepted node sockets, a kernel-level backstop for a
peer that vanished without a FIN. - Hub — node keepalive pinger (
HUB_NODE_PING_INTERVAL_MS, default 5s): the
hub→node half of the heartbeat, so an idle node keeps receiving traffic and a
silent node is swept promptly. - Node — dead-hub detection (
HUB_TIMEOUT_MS, default 20s): reconnect if no
frame arrives from the hub within the window, catching a half-open socket where
the node's own sends still buffer locally. - Node — bounded send (
SEND_MAX_WAIT_MS, default 2s): a wedged TX buffer now
reconnects instead of spinning until the watchdog resets the whole node. - Node — DHCP re-acquire (
REBIND_AFTER_FAILURES, default 5): after repeated
connects that never reached the hub, re-init the interface to pull a fresh
lease — recovers a node that powered up before its DHCP server (site power cut)
or whose lease went bad.
Node firmware bumped to 1.2.0. The node pinger and dead-hub timeout are a
matched pair: run the updated hub alongside 1.2.0 firmware (or set
HUB_TIMEOUT_MS = 0 to pair 1.2.0 nodes with an older, non-pinging hub).
Telegram sidecar — /telemetry (picotty-telegram 1.1.0)
/telemetrysurfaces the new link telemetry over chat: bare, it prints a
per-node roster (rtt avg, jitter, loss %, a good/fair/poor quality rating, and
reconnect count);/telemetry <node>gives that node's detail plus firmware
uptime. Requires a hub on 1.0.3+ (the fields it reads); the sidecar's
picottyfloor is bumped accordingly.
Full Changelog: 1.0.2...v1.0.3
v1.0.2 - 2026-08-05
v1.0.2 - 2026-08-05
First packaged release. (Versions 1.0.0 and 1.0.1 were burned on PyPI by the
file-name-reuse policy after deleted test uploads; 1.0.2 is the first published
version.) PICOTTY is a star-topology networked serial console with
USB-HID keyboard injection for a fleet of headless machines: each Raspberry Pi
Pico node plugs into a target's USB port and becomes a keyboard and a serial
console reader, and one Pi Zero 2 W hub coordinates the swarm through a single
browser dashboard over your management network. Built for homelab mini-PCs that
ship with no BMC/IPMI and no accessible serial port — this is the lights-out
management they never came with.
This release turns the repo-clone deployment into an installable, versioned
distribution and adds a phone control plane.
Highlights
picottyPython package (uv) —uv tool install picottybrings up the hub;
three import surfaces (picotty.hub,picotty.client,picotty.protocol).- Interactive serial write — a real terminal (xterm.js) in the browser, typing
straight into the target's serial getty, alongside HID keyboard injection. - Telegram bot sidecar — stats, push alerts, and a break-glass terminal on your
phone, configured entirely from the dashboard. - OTA firmware updates — chunked, checksummed,
.zip-upload bundles with canary
rollout and watchdog-revert. - Automation — a wait-for-output expect engine, YAML runbooks, and an offline
command queue.
The hub — the picotty package
- Repackaged as the
picottydistribution built with theuv_buildbackend
from asrc/layout, with a committeduv.lockfor reproducible installs. - Three import surfaces:
picotty.hub— the server (registry + SQLite +:9000TCP + FastAPI dashboard); needs the[hub]extra.picotty.client— the SDK:HubClient(async REST) +HubEvents(WebSocket async-iterator); lean base install (httpx + websockets).picotty.protocol— the wire protocol: framing, validation,PROTOCOL_VERSION.
- Console entry points:
picotty-hub(the server) andpicotty-sim(the node
simulator — a fake node for demos/tests, no hardware). - Lean-by-default install with extras:
[hub]pulls FastAPI/uvicorn/aiosqlite/
pydantic/pyyaml;[telegram]pulls the sidecar deps. A Pi running only the
sidecar never installs FastAPI. - Runtime state out of the tree — SQLite defaults to
~/.local/share/picotty/
(a systemdStateDirectorygets/var/lib/picotty); the dashboard's static
assets ship inside the wheel (importlib.resources).
Node firmware (CircuitPython, RP2040)
- One node per target: reads the target's serial console back over the network and
types keystrokes (BIOS, GRUB, initramfs, the OS) as a USB HID keyboard. - LED status codes, opt-in
/error.txt, and a REPL debug path keep a monitorless
node diagnosable; a hardware watchdog recovers a hung loop. - Per-node keyboard layouts, target-machine liveness reporting, and a
serial_tx
capability that gates the interactive write path.
Dashboard (Swarm Control)
- Rebuilt around a real terminal renderer with an HID ⇄ Serial input toggle,
per-node prompt-state badges, and a machine up/dead liveness badge (is the
target alive, not just the node). - Reboot-machine menu with three methods (serial
reboot, Ctrl+Alt+Del, Magic
SysRqAlt+SysRq+B), custom quick chords, macros, and bulk/fleet actions. - Author, view, and edit YAML runbooks in the browser; event history and audit;
hover hints on every control with a docs "?" deep-link, plus an in-app help page.
Automation
- Expect engine — wait-for-output flows with per-step regex + timeouts.
- YAML runbooks — expect flows run across a node group.
- Offline command queue — commands queued for an offline node deliver on
reconnect (once, guarded against double-delivery).
OTA firmware updates
- Push firmware over the wire: chunked transfer, SHA-256 verify, staged writes
with a.bakbackup and a/ota_pending.jsonmarker, watchdog-revert in
boot.py, and finalize-when-healthy. - Upload a firmware
.zipthe hub decompresses into a bundle; canary rollout
with per-node provenance (last_ota).
Operations
- Raw serial bridge — expose an assigned node's serial as a TCP port for
minicom/PuTTY. - Alerting — outbound webhook / ntfy on node down, watchdog recovery, and
command failures, with dedup. - Session recording — asciicast capture and in-browser replay.
Telegram bot sidecar (new)
- A separate process that reaches the hub only via REST +
/wsand bridges it to
Telegram over outbound-only long polling (no inbound port) — usable from an
isolated management VLAN. Depends onpicotty[telegram], importspicotty.client. - Three tiers: stats (
/status/nodes/uptime), push alerts (node down/up,
watchdog, failed, hub-restart;/mute), and a terminal bridge (/shell+ control
keys,/reboot,/sysrq) gated behind a chat-ID allowlist + break-glass TOTP
arming with auto-disarm and idle-close. - Dashboard setup — Settings → Telegram card validates the bot token via
TelegramgetMe, generates a TOTP secret, and one-click Install / start
sidecar. The hub and sidecar share one credentials file
(~/.config/picotty/telegram.env, chmod 600) that the sidecar hot-reloads.
Packaging & release
uv sync/uv rundev workflow;uv buildproduces the wheel + sdist.- GitHub-Release-triggered publish to PyPI via Trusted Publishing (OIDC, no
token) — publishing a release builds and uploads the distributions. - CI runs the hub (
test_db,test_integration) and sidecar (unit, smoke) suites;
workflows use least-privilege permissions.
Tooling
tools/package_tester.py— a live-hub smoke test to run on the hub host after
install: exercises REST + WebSocket + a node round-trip end to end.
Docs
New/updated: packaging.md, telegram.md,
plus architecture, hardware, deployment, firmware, operations, automation, and ota.
Install
uv tool install picotty # picotty-hub + picotty-sim on PATH
picotty-hub # dashboard at http://<hub-ip>:8080From a source checkout: bash hub/scripts/install.sh (uv) then
bash hub/scripts/install-service.sh.
Notes & known limitations
- Serial console, not a KVM — no video capture; reading output requires the
target to have a serial console configured. - Run the hub under one uvicorn worker (the single event loop is the design).
- The CircuitPython node library (installable
.mpybundles + circup) is
scaffolded and documented (node/README.md) but deferred;
the authoritative firmware today isfirmware/circuitpython/. - The management network is not an authenticator — the node token applies on
every connection; auth on the dashboard is optional and off by default (assumes an
isolated VLAN reached over VPN/tunnel).
Verified
Hub wheel builds clean; 7/7 db + 44/44 integration checks; sidecar 14/14 unit +
wiring smoke + 9/9 end-to-end against a real hub through picotty.client; live
tools/package_tester.py green on a Pi Zero 2 W.