KC8JCMesh

What Is This Thing?

KC8JCMesh is a terminal monitor and messenger for a Meshtastic node. It connects to your node over USB, WiFi or Bluetooth, shows you the mesh it can hear, and lets you send and read messages without leaving the terminal or picking up your phone. This is largely because phones suck and TUI apps are cool.

You know this, but Meshtastic is long-range, low-power mesh networking over LoRa radios. The phone apps are good and useful but this is for the times you are already sitting at a keyboard and would rather not leave the terminal to read a message that arrived on a radio three feet away. And no one really wants to look at that little OLED screen.

Written in Python with Textual, same as the other two, because I've got it figured out and know how it works. Textual is my hammer, etc., etc.

Where's The Code?

Right here: kc8jcmesh-0.1.1-py3-none-any.whl
With a hash here: SHA256SUMS
and install instructions here: INSTALL.txt

The short version, if you have pipx:

pipx install https://www.kc8jc.com/kc8jcmesh/kc8jcmesh-0.1.1-py3-none-any.whl

Note the command is kc8jc-mesh, with a hyphen. The other two programs here do not have one. Sorry about that; it has been spelled this way since the first commit and renaming it would only confuse the person who already types it (me).

It needs Python 3.13 or newer. Everything else it depends on - the Meshtastic library, Textual, and the Bluetooth stack - comes along with it automatically. You do not need to install anything else by hand, which makes a change from the other two. And yeah, I should probably go back and do something with those, but I'm not made of time here people!

Three Ways In

You need a Meshtastic node, and there are three ways to reach one:

kc8jc-mesh                          # USB serial, port auto-detected
kc8jc-mesh --port /dev/ttyUSB0      # USB serial, named port
kc8jc-mesh --host 192.168.1.50      # TCP/IP, for WiFi-enabled nodes
kc8jc-mesh --ble                    # Bluetooth LE, scan and pick
kc8jc-mesh --ble AA:BB:CC:DD:EE:FF  # Bluetooth LE, straight to one device

If you know your node's Bluetooth address (nerd!), give it. Scanning takes about ten seconds and then makes you pick from a list; handing over the address skips all of that and goes straight to the device. It is faster and it is more reliable.

On Linux you need to be in the dialout group to open a USB node. If you are not, it fails in a way that looks like a broken cable rather than a permissions problem:

sudo usermod -aG dialout $USER

...then log out and back in, because a group change does not apply to the session you typed it in. Ask me how I know.

What It Actually Does

The default is a full-screen UI: tables of the nodes your radio has heard, a live telemetry feed, and messaging.

  • m sends a direct message to a node
  • c sends a message to a channel
  • s opens settings
  • t filters your own node's telemetry in or out of the feed
  • esc clears the filter, q quits

There are dialogs for adding a channel, renaming a node, setting the node's location, and sharing a channel as a meshtastic.org URL you can hand to somebody else. That last one also shows the raw base64 encryption key, because the URL and its QR code do not always import cleanly on iOS and when that happens you want the key itself rather than a shrug. Again, ask me how I know.

Or No UI At All

--cli skips the full-screen interface entirely and just prints: your node's identity, its GPS position, its statistics, and a table of every peer it has heard. Then it listens and keeps printing as packets arrive.

kc8jc-mesh --cli                # snapshot, then listen
kc8jc-mesh --cli --no-listen    # snapshot and exit
kc8jc-mesh --cli --local-only   # just the attached node, no peers

That is the mode for a pipe, a cron job, or a machine where something redrawing the whole screen is in the way.

The Cool Part?

Setting the node's clock with --set-time looks like a one-line job and is not.

Meshtastic firmware 2.5 and later silently drops admin writes that arrive without a valid session passkey. The library call that establishes one fires the request and returns immediately. Ya know, before the node has answered. So the obvious code asks for a passkey, does not wait, sets the time, and the node throws the command away without telling you that id did nothing. Everything looks like it worked. And yet, the clock is not set. Sigh.

So this asks, then waits for the passkey to actually turn up, and only then sets the time. It is five lines and a deadline, and it is the difference between a something that works and something that appears to work. Not. Subtle.

Where It Writes Things

Message history goes to <nodeid>-MessageLog.txt, and if the UI ever comes down with an exception the traceback goes to kc8jc-mesh-crash.log. Both land in the same directory, picked once when the program starts, in this order:

  • Wherever you told it, with --data-dir or the KC8JCMESH_DATA_DIR environment variable.
  • The directory you are standing in, if a message log is already sitting there (which is likely is not on your first run).
  • Otherwise your application data directory - ~/Library/Application Support/KC8JCMesh on a Mac, ~/.local/share/KC8JCMesh on Linux.

That middle rule is deliberate. If you have been running this out of a directory that already has your message history in it, it keeps writing there and nothing moves. An upgrade should never orphan a month of messages. Bit myself with that a time or two and it bummed me out.

How Did I Get Here?

I have a Meshtastic node. I spend a lot of time in a terminal. Reaching for a phone to read a message that came in over a radio on the desk next to me started to feel silly, and the Meshtastic library is perfectly pleasant to talk to from Python. I already had a habit/problem of building terminal UIs for radio things. Why not?

Support

Same as everything else here: a Hobby Project, not a product. I wrote it because I wanted it and I will probably only touch it when I want it to do something new. Send me an email if it works, or if it does not, but know that my support availability is limited - even for myself.