Jean-Pierre MartineauandClaude Opus 5 9ec72dbf48 Skill: blank the NULs in ANSI art without touching its SAUCE record
An unpainted cell exports as 0x00, which no terminal draws as nothing — it
comes out as tofu, as a stray glyph, or swallowed with the rest of the row
shifted left. One byte wide to fix, and this makes that one substitution and
no other: same length out, every escape sequence where it was.

The trap it exists to avoid is the obvious version of the job. An ANSI file's
last 128 bytes are a SAUCE record whose NULs are structure — binary fields and
the padding on the font name — so a tr over the whole file corrupts the
metadata and fixes no art at all. It cuts at the 0x1A that opens the record and
counts what it left alone. Both splashes are in exactly that state today: 24
NULs each, all of them SAUCE.

A space is only black if the span it lands in left the background black, so
the ones that would come out coloured are named by row and column rather than
quietly repainted — that is an editing call, not a substitution.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 19:59:51 -04:00

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.

Python 3.10+ For MeshCore Interface: TUI + CLI


Why MeshTerm

  • One tool, two faces. Launch meshterm for 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 answers one question — what would you like to do? — so its five sections are named for the doing, not the subject. Every interactive screen is listed here with its scripted equivalent, where one exists.

💬 Message — the people on the other end

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
👥 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

📊 Watch — what the mesh is doing, and what it did

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. —
🚨 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. —
⏳ 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

🧭 Explore — where the nodes are and how they reach each other

Feature What it does Scripted
🌍 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. —
🎯 Trace target 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 …
👣 Trace path The other half of tracing: compose the whole circuit by hand — out and back whichever way you choose — and walk it. The path composer suggests each next hop from the links actually observed, strongest first, and can fetch a repeater's neighbour table over the mesh when you hold its admin password. meshterm trace --path …
🏆 Trophy case Every trace that comes home is scored, on seven boards: longest distance, farthest node, longest single leg, most nodes (with and without revisits), weakest surviving link, and biggest enclosed loop. Records are kept per hash width, and a record walk must be a trail — no link crossed twice the same way. meshterm records

🔧 This node — the radio in your hand

Feature What it does Scripted
📋 Device info The connected companion's identity and full radio configuration at a glance. meshterm info
🔧 Device 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>
🔨 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 / …
📡 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
🔌 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

🗼 Other nodes — someone else's radio, over the mesh

Feature What it does Scripted
🗼 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…>
📶 TX optimize 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]

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.

Want to send a pull request? See CONTRIBUTING.md first.

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.

MeshTerm is open source under the Apache License, Version 2.0 — free to use, modify, and redistribute, commercially or otherwise. The "MeshTerm" name and any associated logo are trademarks reserved by the author and aren't covered by the code license (see NOTICE); a redistributed fork should go by its own name. The donate link built into the app supports this project and its original author — forks that keep soliciting through it without redirecting it to themselves aren't affiliated with this project.

S
Description
No description provided
Readme Apache-2.0
19 MiB
0 Stars 1 Watchers 0 Forks
Languages
Python 99.1%
Shell 0.9%