MeshCore.create_ble builds its client, calls connect() on it, and hands
it back only on success. A connect that *raises* therefore left that
client — and the bleak link it had already opened — orphaned inside the
library with no reference we could close. Our _mc was never assigned, so
disconnect() was a no-op and the probe's _safe_disconnect closed nothing.
That is not a corner: a PIN-protected companion answers the unbonded
notify-subscribe with a GATT authentication error, and the picker's first
attempt is always PIN-less. So selecting an unbonded companion leaked the
link, Windows held it for the life of the process, and the peripheral —
still believing it had a peer — stopped advertising. The PIN dialog then
asked for a code it could no longer deliver: the retry paired at the OS
level (the bond really is created) but could not form a GATT session, and
the user was told their correct PIN belonged to something that "isn't a
MeshCore companion". Since Unpair & quit is what leaves you unbonded, the
whole thing read as unpairing having changed the PIN.
Assemble the two objects here instead. _connect_owned_ble keeps the
handle, so both failure shapes close it: a raise (closed, then re-raised)
and a None handshake (closed, then reported as "not a companion"). The
teardown is shielded — the other way in is a probe whose wait_for expired
and cancelled the handshake, where a plain await would be cancelled the
moment it suspended and abandon the very close it was called to perform.
The picker's accepted PIN now also rides on the context, so a mid-session
reconnect doesn't re-run the whole "needs a PIN" dance against a companion
we already authenticated to. Session-lifetime only; nothing reaches disk.
Tests stop naming a real companion: the addresses and pairing code are
stand-ins now, and the fakes model the class we construct rather than the
create_ble we no longer call.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Add TCP alongside serial and BLE as a first-class companion transport, end to
end: discovery/connection/selection learn a tcp: stable_id and host:port target,
--tcp and TCP config profiles reach a WiFi board or network proxy, and a network
companion -- which isn't discoverable -- is rebuilt into the picker from its
remembered endpoint.
Device picker gains the network-device affordances:
- "Add a network device" prompts for host:port, smoke-tests it, and remembers it.
The row is marked "- experimental" in the cautionary hue.
- Delete on a network row opens a chromeless Cancel/Remove confirm (Cancel left,
Remove right and default: Enter removes, Esc cancels), then DeviceStore.forget
prunes it and re-draws the list. Serial/BLE rows stay inert.
- The "Del remove" footer atom now follows the highlight, surfacing on the box's
bottom border only while the cursor sits on a deletable row; the box is sized
to the fullest footer so it never widens mid-navigation.
Folds in the accompanying courier, channels, and config refinements built along
the way. Screens stay readable at 72 cols; full suite green (745 tests).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two long-standing flakes, one root cause each:
Quit with a serial companion often froze until the exit watchdog killed
the process. The meshcore dispatcher's stop() awaits queue.join(), but
its processor task exits after draining at most one event once stopped —
so a routine advert/RX-log burst (two+ events queued at that instant)
deadlocks the join forever (repro confirmed against the installed
library). MeshCoreDevice.disconnect now gives the graceful teardown two
seconds, then abandons it: the dispatcher task is cancelled synchronously
and the raw transport closed directly, so the port is still released and
even the forced path finishes well under the watchdog.
BLE startup often failed until the user re-selected the device. Connecting
by bare address makes bleak re-discover the peripheral with a fresh
internal scan, which on Windows intermittently misses a slow-advertising
companion. The discovery scan's live BLEDevice now rides along
(DiscoveredDevice -> probe/context -> create_ble) so the connect opens the
peripheral directly, and a transport-level ConnectionError is retried once
before surfacing — the manual re-select was never anything but a retry.
PIN/bond rejections still surface on the first attempt, unlooped.
Verified against hardware: BLE info read off the Waymarker connects and
exits cleanly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Query MeshCore's device-info frame (send_device_query) on connect to learn the
board's own model string (e.g. "Seeed Tracker T1000-E") — the only reliable source,
since BLE companions advertise no manufacturer data, use randomized addresses with
no IEEE OUI, and expose no GATT Device Information Service.
The probe folds the model into the identity dict, so it's captured during the
connection that already happens, then remembered in the device store (preserved
across reconnects like the node name). The device list and splash picker rename
their "Vendor" column to "Hardware": it shows the firmware model for confirmed
devices and falls back to the USB vendor name for merely-attached serial ports.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reworks the startup device picker so it reads honestly at a glance:
- VENDOR is now just the hardware maker. A BLE advert rarely carries one
(confirmed against real hardware: the companion advertises no
manufacturer_data), so it stays blank instead of mislabelling the
transport "Bluetooth" as a vendor.
- A new TYPE column marks the transport with an icon: a plug emoji for
wired serial, and the Bjarkan rune the Bluetooth logo is drawn from,
rendered white on Bluetooth blue and flanked by half-block slivers so
it reads as a slim rounded badge.
- Every confirmed companion (not just the most-recent one) is shown by
the mesh node name learned at connect time, so a known COM11 reads as
its node name rather than the OS's generic "USB Serial Device".
- Confirmed companions sort to the top, most-recently-used first, and
wear their name in white so they stand out from merely-detected ports.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Companions can now be reached over Bluetooth as well as USB. Rather than
a parallel BLE path, this generalizes the existing serial abstractions so
a device is identified by a transport (serial | ble) plus a target (COM
port or Bluetooth address); every feature rides the same Device interface
and works over either link unchanged.
- discovery: DiscoveredDevice gains transport/address/name and
transport-aware stable_id/label/confidence/target; discover_ble_devices
scans for MeshCore* adverts via bleak, discover_all merges both.
- connection: MeshCoreDevice opens create_ble vs create_serial by
transport; new link_present() liveness (OS port enumeration for serial,
the BLE client's is_connected flag for Bluetooth); is_connection_lost
recognizes Bleak errors and BLE transport callbacks; probe_device is the
transport-aware smoke test.
- disconnect: the session watcher polls transport-neutral link_alive(), so
a dropped BLE link raises the same reconnect dialog and auto-resume as an
unplugged cable; reconnect rebuilds the same endpoint by address.
- selection/context/store/profiles: --ble address, remembered BLE default,
and BLE device profiles all resolve to the right transport.
- UI/CLI: startup picker scans both transports behind a spinner and tags
Bluetooth companions; `devices` lists them (--no-ble to skip); global
--ble/--ble-pin flags; README and example config updated.
Validated on hardware: discovery, connect, reconnect, and channel
messaging (send + receive) over BLE.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reserve the "MeshCore device" tag for hardware we have actually talked to,
rather than guessing from the USB vendor ID:
- DeviceStore now keeps a registry of every confirmed device (keyed by
stable serial id), remembered forever, migrating the old single-record
file transparently.
- Selecting a device on the splash runs an immediate smoke test
(smoke_test_meshcore): a genuine companion is remembered and confirmed;
anything else re-prompts with a message to choose another. The probe uses
a short handshake window so a non-MeshCore port is rejected cleanly in a
few seconds instead of blocking on meshcore's full default timeout.
- The probe renders in place on the chromeless splash via a new BusyScreen
(animated Braille spinner) driven by session.busy_startup, so the wordmark
and box stay put instead of a separate progress dialog.
- Silence the meshcore library's console logging (its import calls
logging.basicConfig, which printed "INFO:meshcore:..." over the TUI):
route it to the JSON-lines file log with a NullHandler guard on root.
- The devices tool now marks confirmed devices and stops asserting "LoRa"
before a device is proven.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rename the package directory, imports, brand strings, env-var prefixes,
console-script entry point, and the config/data dir (~/.config/meshterm).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Improve the device picker and inventory in three ways:
- Remember a device's mesh node name. On a successful connect, learn the
radio's own name (adv_name/name, best-effort) and persist it with the
remembered device; the picker leads its row with it and the devices table
shows it. A failed probe never blocks the connection and preserves any
name already on file.
- Qualify devices in tiers instead of one over-generous "likely LoRa" flag.
A native-USB dev board is a strong "LoRa device"; a bare USB-UART bridge
(CP210x/CH340/FTDI) is only a "serial adapter" / "maybe", since the same
chip shows up on plenty of non-LoRa hardware. Sorting and the table follow
the board > bridge > unknown ranking; JSON gains a `confidence` field.
- Stop repeating the COM port. Windows descriptions often already end with
"(COM11)", so the label no longer renders "… (COM11) (COM11)".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MeshTools is a Rich+Questionary interactive / Typer-scriptable toolkit for
tuning a MeshCore mesh over a serial companion device, with SQLite-logged runs
and Plotly visualizations. Includes the skeleton (trace, info, history),
TX-power optimization, and a MockDevice simulator (--mock).
Device discovery & selection:
- core/discovery.py: enumerate serial ports (pyserial), flag/sort likely LoRa
hardware by USB vendor id, stable_id for replug-safe identity.
- core/device_store.py: remember the last successfully connected device in
<config_dir>/devices.json (independent of --db).
- core/selection.py: non-interactive resolver (--port > profile > remembered >
sole device > guidance error) for the CLI.
- ui/device_picker.py + menu startup: interactive picker preselecting the
remembered default; banner reflects the choice.
- tools/devices.py: `meshtools devices` lists/marks attached devices.
- AppContext resolves the port and records last-known-good on connect success.
Fixes:
- theme.py: force UTF-8 on stdout/stderr so glyphs don't crash legacy consoles.
- connection.py: friendly message when meshcore is missing; pin meshcore>=2.3.
- trace/tx-optimize: fall back to free-text target entry when the device
reports no contacts (autocomplete requires non-empty choices).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>