"Use this path" was respelling the composed spec as raw hex, two rows under the live preview that *is* the path — the same thing said worse, growing by a hop every time the route did. The row now says what Enter does and lets the picture above it say what to. Only the unarmed case still needs words. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MeshTerm
A full-screen terminal companion for MeshCore LoRa mesh devices — tune your radio, chat across the mesh, watch the network live, and keep a longitudinal record of everything you overhear, all from a connected serial, Bluetooth, or network (TCP) companion.
MeshTerm is interactive by default: a modern, keyboard-driven TUI built on
Rich and
prompt_toolkit. It is also
fully scriptable — every screen has a mirror meshterm subcommand — so the same
capabilities drive both an evening of exploring the mesh and a cron job. Every packet the
radio overhears is recorded to a local SQLite database, so the longer you run it, the more
your mesh's history is worth.
Why MeshTerm
- One tool, two faces. Launch
meshtermfor a full-screen menu; pass a subcommand for a scripted one-shot. The registry that builds the menu builds the CLI, so they never drift apart. - It remembers. A passive monitor logs every overheard advert and telemetry frame — SNR, RSSI, shared location — to SQLite from the moment the radio opens. Maps, contact lists, charts, and the Time Machine all read back that history.
- It measures. Live trace with per-hop reliability, a coarse→refine→verify TX-power sweep, and a walkable graph of how the mesh actually hangs together.
- It talks. Live channel and direct messaging, store-and-forward courier delivery for
contacts that aren't there yet, and shareable channels as QR codes and
meshcore://links. - It connects however your companion does. Serial (USB), Bluetooth LE, or TCP (a WiFi board or a network proxy) — the discoverable transports are auto-found and picked interactively, a network device is named by hand, and every kind reconnects live when a link drops. No radio? A built-in simulator covers development.
- It works offline. The street map caches OpenStreetMap tiles to disk and falls back to a blank grid when there's no network — it never needs the internet to run.
Install
git clone <your-fork-url> meshterm
cd meshterm
pip install -e ".[dev]"
Python 3.10+ is required (matching the meshcore library). The [dev] extra adds
pytest and ruff; drop it for a runtime-only install (pip install -e .).
python -m pytest -q # run the test suite
Quick start
# Interactive full-screen menu (auto-discovers serial + Bluetooth companions)
meshterm
# Connect over Bluetooth (address from `meshterm devices`)
meshterm --ble AA:BB:CC:DD:EE:FF info
# Connect over the network to a TCP companion (host[:port], port defaults to 5000)
meshterm --tcp 192.168.1.50 info
# No radio attached? Use the built-in simulator for development.
meshterm --mock
# On a ClockworkPi PicoCalc (Luckfox Lyra / Calculinux) the handheld flavour is
# auto-detected: a 53-column layout, a 16-slot palette, a compact glyph language on a
# custom console font, and an F-key hint lane. Preview it anywhere, or inspect it:
meshterm --mock --platform picocalc
meshterm specimen
Handheld bring-up (fonts, palette, boot services) lives in
scripts/calculinux-setup.sh + scripts/calculinux-console-font.sh.
Features
MeshTerm's menu is organised into four sections. Every interactive screen is listed here with its scripted equivalent, where one exists.
🛰 Mesh — what's going on out there
| Feature | What it does | Scripted |
|---|---|---|
| 📊 Dashboard | The live mesh overview: a two-hour all-packet activity chart with a pulse line, session traffic tallies by packet class, and window RF health beside the radio's own live numbers. Repaints every second. | — |
| 📡 Live feed | Every packet as it arrives, newest first: time, what the frame is (its parsed payload class), and what it is about — the node for an advert, the channel for a channel text, sender → recipient for a direct message or a request, the tag for a trace — with reception quality beside it. Enter opens any row in the packet viewer, route graph and all. | — |
| 🌍 Map | Located nodes plotted over a real OpenStreetMap street basemap rendered as Unicode braille (streets, rivers, place names). Pannable and zoomable; repeaters highlighted and drawn on top. Falls back to a blank grid offline. | meshterm map (one-shot render) |
| 🌐 Mesh walk | The mesh's observed shape, walked one node at a time: an evidence graph built from trace walks, firmware routes, overheard relay chains, and repeater neighbour tables. SNR-coloured braille edges, quality bars, Enter to walk, ⌫ to backtrack, type to find any node. The map answers where; the walk answers how it hangs together. | — |
| 🚨 Watchtower | A passive sentinel over the nodes you star: silence alarms when a watched node goes quiet, SNR-sag warnings when reception degrades, recovery notes when it returns, and a heads-up when a never-before-seen node appears. Runs in the background all session; unacked alerts show as a header badge. | — |
| 📡 Send advert | Announce this node to the mesh — a zero-hop or flood advertisement, or share this node's contact card as a QR code. | meshterm config advert / config share |
💬 Messaging
| Feature | What it does | Scripted |
|---|---|---|
| 💬 Chat | Live full-screen channel and direct messaging — a scrolling transcript with a pinned input line where sent and received messages stream together. Every message is logged; unread counts show in the menu header. | meshterm chat send / history / list |
| # Channels | Create, join, reorder, mute, and share mesh channels — with QR codes and meshcore:// share links. |
meshterm channels list / add / join / import / share / clear |
| 📨 Courier | Store-and-forward outbox for contacts that aren't reachable yet. Queue a message; it goes out (with ack tracking and polite exponential backoff) the moment the contact is next heard, or at a scheduled time. | meshterm courier queue / list / send / cancel / clear |
⚙ Device management
| Feature | What it does | Scripted |
|---|---|---|
| 📋 Device info | The connected companion's identity and full radio configuration at a glance. | meshterm info |
| 🔨 Device actions | Immediate operations on the box itself: clock sync, TOML backup/restore, the identity key, reboot, and factory reset — each acting the moment it's confirmed, with destructive ops gated. | config backup / restore / reboot / … |
| 🔧 Config | View and stage changes to every setting (name, radio, behaviour, experimental) for review before applying, with TOML backup/restore. | meshterm config / config set <key> <value> |
| 🗼 Repeater admin | Set up remote repeaters and room servers over the mesh: log in (remembered or prompted password), then a config-style editor speaking the node's text CLI — including repeater-only knobs (TX delay, airtime factor, advert intervals) — plus one-shot actions and a readline remote command line. | meshterm repeater-admin <node> <command…> |
🧰 Tools — measure and explore
| Feature | What it does | Scripted |
|---|---|---|
| 👥 Contacts | This node and its known contacts — a recency heat-map, overheard packet counts, and full public keys with the path-hash prefix highlighted. | meshterm contacts |
| 🧭 Trace | A live trace screen: pick a target and watch each trace stream in hop by hop, with running per-hop medians and reliability. Compose or force a route through specific repeaters. A trace transmits exactly once (repeaters can blacklist nodes that burst) — sample more by running it again. | meshterm trace --target … |
| ⚡ TX-power optimization | Sweep a remote node's transmit power live — coarse, then refine, then verify — watch each level land, and decide whether to apply the winner. | meshterm tx-optimize --path … [--apply] |
| ⏳ Time machine | Everything the recorder ever heard, as braille charts: reception volume, the median-SNR band, hour-of-day rhythm, packets and nodes per day, and first-ever arrivals — over a switchable 24h / 7d / 30d / all-time window, per node or mesh-wide. | — |
| 🎧 Monitor | Passive recording is always on in the app. On the CLI, capture a bounded foreground window, tailing each overheard packet and summarising when it ends. It transmits nothing — it only listens. | meshterm monitor --seconds 60 |
| 🔌 Device discovery | Enumerate serial and Bluetooth LE companions, pick one interactively (or auto-select the only one present), add a network (TCP) companion by host:port, and remember the last good default. A dropped link is detected live and offers to reconnect. | meshterm devices |
CLI cookbook
Every menu option is also a Typer subcommand — ideal for scripting, cron, and bots.
# Trace — a trace transmits exactly once; run it again to sample more.
meshterm trace --target Alice --profile yagi
# Force a route through specific repeaters (MeshCore-app style): comma-separated
# contact names and/or hex key prefixes, mixed freely. Blank lets the device route.
meshterm trace --target Alice --path "3d,f2,3d"
meshterm trace --target Alice --path "3d,Bravo-Repeater,f2"
# Sweep and apply a remote node's TX power
meshterm tx-optimize --path "Bravo-Repeater,Alice" --samples 6 --step 3 --apply
# Messaging: live chat in the menu, or scripted from the CLI
meshterm chat # interactive: pick a conversation, chat live
meshterm chat send --to Alice "on my way" # direct message
meshterm chat send --channel 0 "net in 5" # channel broadcast
meshterm chat history --to Alice
meshterm chat list
# Store-and-forward: queue for a contact that's offline right now
meshterm courier queue Alice "ping me when you're back" --at 18:30
meshterm courier list # the outbox — waiting and finished
meshterm courier send 3 # force one delivery attempt now
# Remote repeater admin (one transmission per invocation)
meshterm repeater-admin Bravo-Repeater "get name"
# Device configuration: view, set, back up, restore
meshterm config # show all current settings
meshterm config set radio_sf 9 # change one setting
meshterm config backup node.toml # archive every setting to TOML
meshterm config restore node.toml --dry-run
meshterm config advert-cadence 2 # auto-advert to neighbours every 2 h (0 = off)
meshterm config advert-cadence 24 --flood # flood the wider mesh daily
# Passive capture window (records to history; transmits nothing)
meshterm monitor --seconds 60
Global options (before the subcommand): --profile/-p, --port, --ble, --ble-pin,
--tcp, --mock, --db, --json, --quiet/-q.
Configuration
MeshTerm runs with zero configuration — it discovers attached serial devices and nearby
Bluetooth companions, lets you pick one, and remembers the last good default. A network (TCP)
companion isn't discoverable, so reach it with --tcp host:port, a TCP profile, or the
picker's "add a network device" prompt. Define a config file only to give your hardware stable
aliases or tune behaviour.
Copy config.example.toml to ~/.meshterm/config.toml:
default_profile = "s3"
trace_cooldown_s = 1.0
direct_message_soft_retries = 2 # auto-resend an unacked DM up to N extra times (0–2)
history_days = 365 # days of overheard-packet history to keep (0 = forever)
[profiles.s3]
port = "COM5"
baudrate = 115200
default_tx_power = 20
description = "XIAO ESP32-S3 + Wio SX1262 serial companion"
# A Bluetooth LE companion: give it an `address` instead of a `port`.
[profiles.handheld]
address = "AA:BB:CC:DD:EE:FF"
# ble_pin = "123456" # only if your device requires a pairing PIN
description = "Pocket handheld over Bluetooth"
# A network (TCP) companion: give it a `host` (and optional `tcp_port`, default 5000).
[profiles.wifi]
host = "192.168.1.50"
description = "Basestation over WiFi"
Timestamps are stored as UTC and rendered in your local time. History older than
history_days is pruned once at session start.
Architecture
MeshTerm is layered so a new feature is one file in meshterm/tools/:
cli.py Typer app; no subcommand -> interactive menu
context.py AppContext (console, config, repository, device) dependency container
core/ Domain: models, config + device profiles, connection abstraction (+ mock)
tools/ Pluggable "menu options"; each self-registers and gets logging for free
services/ Background algorithms (monitor, trace, tx search, courier, watchtower) — no UI
persistence/ SQLite schema, repository, structured logging
ui/ Rich theme/widgets + the full-screen TUI (screens, dialogs, map, chat)
Adding a feature means subclassing Tool, decorating it with @register, and
implementing run(). The same registry builds both the CLI and the menu, and the base
class wraps every execution in a logged runs row automatically.
Development
pip install -e ".[dev]"
python -m pytest -q # tests
ruff check . # lint
ruff format . # format
Screens are designed to stay readable at 72 columns.
Acknowledgements & attribution
MeshTerm stands on the MeshCore project — its firmware and the
meshcore Python companion library are what make talking to the radio possible.
Map data
The street map is rendered from © OpenStreetMap contributors data, served as vector tiles by OpenFreeMap. OpenStreetMap data is available under the Open Database License (ODbL). Tiles are decoded with a small built-in reader for the Mapbox Vector Tile format.
Open-source dependencies
MeshTerm is built with these libraries; each is used under its own license (see the respective project for the authoritative terms):
| Library | Role | License |
|---|---|---|
| meshcore | Companion-device protocol (serial / BLE) | MIT |
| pycryptodome | AES/HMAC for decrypting overheard channel packets | BSD-2-Clause / Public Domain |
| pyserial | Serial-port enumeration and I/O | BSD-3-Clause |
| bleak | Bluetooth LE scanning + connection | MIT |
| Typer | Scripted CLI (subcommands mirror the menu) | MIT |
| Rich | Console rendering | MIT |
| prompt_toolkit | Full-screen interactive TUI | BSD-3-Clause |
| Segno | Pure-Python QR codes (channel share links) | BSD-3-Clause |
| tomli / tomli-w | TOML config read/write | MIT |
License
Copyright © 2026 Jean-Pierre Martineau.