Add curses dashboard and XIAO bridge examples; enhance stream parser for UTF-8 logging
This commit is contained in:
@@ -1,128 +1,462 @@
|
||||
# MicroMesh
|
||||
# MicroMesh 📻
|
||||
|
||||
MicroMesh is a small, dependency-free Meshtastic client for MicroPython. It talks to a
|
||||
Meshtastic device over its serial API and implements the protobuf wire format directly,
|
||||
so `google.protobuf`, threads, and `pyserial` are not required.
|
||||
**A tiny, dependency-free Meshtastic client for MicroPython.**
|
||||
|
||||
The initial release supports:
|
||||
MicroMesh lets a small MicroPython board talk to a Meshtastic radio over UART. Use it
|
||||
to receive messages, send text and position packets, inspect the node list, or power a
|
||||
full-screen terminal dashboard.
|
||||
|
||||
- serial `ToRadio` / `FromRadio` framing and resynchronization;
|
||||
- the configuration handshake and a small in-memory node list;
|
||||
- text, arbitrary data, and position packets;
|
||||
- routing acknowledgements, node/user data, waypoints, and device metrics;
|
||||
- familiar generated-protobuf methods: `SerializeToString`, `ParseFromString`,
|
||||
`CopyFrom`, `HasField`, `ClearField`, and `WhichOneof`;
|
||||
- preservation of unknown protobuf fields for forward compatibility.
|
||||
No protobuf compiler runs on the board. No threads. No `google.protobuf`. Just a UART,
|
||||
three wires, and regular Python.
|
||||
|
||||
This is intentionally not a complete replacement for the desktop
|
||||
[`meshtastic`](https://github.com/meshtastic/python) package. Configuration and admin
|
||||
messages are currently returned as raw encoded bytes.
|
||||
> [!NOTE]
|
||||
> MicroMesh is young and intentionally small. It supports the most useful Meshtastic
|
||||
> client operations, but it is not yet a complete replacement for the official desktop
|
||||
> [`meshtastic`](https://github.com/meshtastic/python) package.
|
||||
|
||||
## Install
|
||||
## What can it do?
|
||||
|
||||
From a checkout of this repository, install with a current `mpremote`:
|
||||
- Connect to a Meshtastic radio using its framed serial API
|
||||
- Download and track the radio's node database
|
||||
- Receive and send text messages
|
||||
- Send binary data and positions
|
||||
- Read user, position, signal, battery, and hop information
|
||||
- Preserve unknown protobuf fields for forward compatibility
|
||||
- Recover from serial noise and skip newer messages it cannot decode
|
||||
- Run a curses dashboard with live stats, nodes, and messages
|
||||
- Work on MicroPython without third-party runtime dependencies
|
||||
|
||||
```sh
|
||||
mpremote mip install package.json
|
||||
## Pick your setup
|
||||
|
||||
| I want to… | Use this |
|
||||
| --- | --- |
|
||||
| Run a simple listener on a XIAO RP2040 | [XIAO quick start](#xiao-rp2040-quick-start) |
|
||||
| View nodes and messages through my XIAO | [Curses dashboard](#curses-dashboard-through-the-xiao) |
|
||||
| Connect my computer directly to the Meshtastic radio | [Direct desktop dashboard](#direct-to-radio-dashboard) |
|
||||
| Use a Pico, Pico W, or ESP32 | [Other boards](#other-boards) |
|
||||
| Write my own program | [Python API](#python-api) |
|
||||
|
||||
## How the XIAO setup works
|
||||
|
||||
The XIAO is the computer running MicroMesh. The Meshtastic device is the LoRa radio.
|
||||
|
||||
```text
|
||||
Mac / PC XIAO RP2040 Meshtastic radio
|
||||
┌─────────┐ USB/REPL ┌────────────────┐ UART ┌─────────────────┐
|
||||
│ terminal│◀─────────────▶│ MicroPython + │◀────────▶│ Meshtastic │◀──▶ LoRa
|
||||
│ or UI │ │ MicroMesh │ 3 wires │ firmware │
|
||||
└─────────┘ └────────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
Alternatively, copy the `micromesh` directory to `/lib` on the board. After this
|
||||
repository is published on GitHub it can also be installed as
|
||||
`mpremote mip install github:ORG/micromesh`.
|
||||
The XIAO does not contain a LoRa radio. It controls a separate Meshtastic device over
|
||||
UART.
|
||||
|
||||
It can also be installed on CPython for development:
|
||||
## Before you begin
|
||||
|
||||
```sh
|
||||
python -m pip install .
|
||||
You need:
|
||||
|
||||
- A Seeed Studio XIAO RP2040 with MicroPython installed
|
||||
- A separate Meshtastic-compatible radio running Meshtastic firmware
|
||||
- Three jumper wires for TX, RX, and GND
|
||||
- A data-capable USB cable for the XIAO
|
||||
- Python 3 on your Mac, Linux computer, or Windows PC
|
||||
|
||||
Configure the Meshtastic radio's serial interface for:
|
||||
|
||||
```text
|
||||
Enabled: yes
|
||||
Mode: PROTO
|
||||
Baud: 115200
|
||||
```
|
||||
|
||||
After the first PyPI release, install it by name with:
|
||||
The radio's UART pin names depend on its model. Consult that board's pinout before
|
||||
connecting wires.
|
||||
|
||||
```sh
|
||||
python -m pip install micromesh
|
||||
> [!CAUTION]
|
||||
> Use **3.3 V UART logic** and always connect the grounds. Do not connect the boards'
|
||||
> power pins unless you have verified their voltage and current requirements.
|
||||
|
||||
## XIAO RP2040 quick start
|
||||
|
||||
### 1. Wire the boards
|
||||
|
||||
UART wires cross: TX goes to RX, and RX goes to TX.
|
||||
|
||||
```text
|
||||
XIAO RP2040 Meshtastic radio
|
||||
────────────────────────────────────────────────────
|
||||
D6 / GPIO0 / UART0 TX ──────────▶ UART RX
|
||||
D7 / GPIO1 / UART0 RX ◀────────── UART TX
|
||||
GND ─────────── GND
|
||||
```
|
||||
|
||||
## Quick start
|
||||
The XIAO D6/D7 mapping comes from the
|
||||
[`XIAO RP2040 pinout`](https://wiki.seeedstudio.com/XIAO-RP2040/).
|
||||
|
||||
### 2. Prepare a Python environment
|
||||
|
||||
Run these commands from the MicroMesh repository directory:
|
||||
|
||||
```sh
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
python -m pip install --upgrade mpremote
|
||||
```
|
||||
|
||||
Windows PowerShell uses this activation command instead:
|
||||
|
||||
```powershell
|
||||
.venv\Scripts\Activate.ps1
|
||||
```
|
||||
|
||||
### 3. Find the XIAO serial port
|
||||
|
||||
Connect the XIAO over USB, then run:
|
||||
|
||||
```sh
|
||||
mpremote connect list
|
||||
```
|
||||
|
||||
On macOS it will usually look similar to `/dev/cu.usbmodem2101`. Linux commonly uses
|
||||
`/dev/ttyACM0`; Windows uses a name such as `COM4`.
|
||||
|
||||
The commands below use `/dev/cu.usbmodem2101`. Replace it with your port.
|
||||
|
||||
### 4. Install MicroMesh on the XIAO
|
||||
|
||||
```sh
|
||||
mpremote connect /dev/cu.usbmodem2101 mip install package.json
|
||||
```
|
||||
|
||||
This installs the `micromesh` package under `/lib` on the board.
|
||||
|
||||
### 5. Run the example
|
||||
|
||||
The example broadcasts one `hello from XIAO RP2040` message after each successful
|
||||
configuration download. Comment out its `mesh.sendText(...)` line first if you want a
|
||||
receive-only test.
|
||||
|
||||
```sh
|
||||
mpremote connect /dev/cu.usbmodem2101 run examples/xiao_rp2040.py
|
||||
```
|
||||
|
||||
You should immediately see:
|
||||
|
||||
```text
|
||||
MicroMesh starting on XIAO RP2040
|
||||
UART0: D6/GPIO0 TX -> radio RX
|
||||
UART0: D7/GPIO1 RX <- radio TX
|
||||
UART0: 115200 baud; common GND required
|
||||
configuration requested; id=...
|
||||
```
|
||||
|
||||
When the radio finishes sending its configuration and node database:
|
||||
|
||||
```text
|
||||
radio configuration complete; sending greeting
|
||||
connected; 17 nodes known
|
||||
```
|
||||
|
||||
The number of nodes will be different for your radio. Press `Ctrl-C` to stop.
|
||||
|
||||
### 6. Start it automatically
|
||||
|
||||
Once the temporary run works, save it as the XIAO boot program:
|
||||
|
||||
```sh
|
||||
mpremote connect /dev/cu.usbmodem2101 fs cp examples/xiao_rp2040.py :main.py
|
||||
mpremote connect /dev/cu.usbmodem2101 reset
|
||||
```
|
||||
|
||||
To stop or replace an automatically running script, connect the XIAO, press `Ctrl-C`
|
||||
in `mpremote repl`, and upload a different `main.py`.
|
||||
|
||||
## Curses dashboard through the XIAO
|
||||
|
||||
The desktop curses dashboard provides keyboard message composition and a resizable UI.
|
||||
It displays live connection stats, known nodes, battery levels, SNR, hops, last-seen
|
||||
times, and incoming messages. The XIAO runs a small bridge between the radio UART and
|
||||
the dashboard over USB.
|
||||
|
||||
The dashboard needs a small bridge program on the XIAO. The regular
|
||||
`xiao_rp2040.py` example prints human-readable logs; it does not expose structured data
|
||||
to desktop applications.
|
||||
|
||||
### 1. Install the desktop dependencies
|
||||
|
||||
```sh
|
||||
source .venv/bin/activate
|
||||
python -m pip install -e . pyserial
|
||||
```
|
||||
|
||||
Windows users also need:
|
||||
|
||||
```powershell
|
||||
python -m pip install windows-curses
|
||||
```
|
||||
|
||||
### 2. Install the bridge on the XIAO
|
||||
|
||||
This replaces the current `main.py`:
|
||||
|
||||
```sh
|
||||
mpremote connect /dev/cu.usbmodem2101 fs cp examples/xiao_dashboard_bridge.py :main.py
|
||||
mpremote connect /dev/cu.usbmodem2101 reset
|
||||
```
|
||||
|
||||
### 3. Close `mpremote`, then launch the dashboard
|
||||
|
||||
```sh
|
||||
python examples/curses_dashboard.py --xiao-bridge /dev/cu.usbmodem2101
|
||||
```
|
||||
|
||||
Only one application can own a serial port. Close `mpremote`, Arduino Serial Monitor,
|
||||
screen, minicom, and other serial tools before launching the dashboard.
|
||||
|
||||
The message field is always active. Type a message and press Enter—there is no separate
|
||||
compose mode.
|
||||
|
||||
| Key | Action |
|
||||
| --- | --- |
|
||||
| Type + Enter | Broadcast the text in the message field |
|
||||
| Escape | Clear the message field |
|
||||
| `F5` (or `Ctrl-R`) | Request the configuration and node database again |
|
||||
| `F10` (or `Ctrl-Q`) | Quit cleanly |
|
||||
|
||||
To switch back to the regular logging example:
|
||||
|
||||
```sh
|
||||
mpremote connect /dev/cu.usbmodem2101 fs cp examples/xiao_rp2040.py :main.py
|
||||
mpremote connect /dev/cu.usbmodem2101 reset
|
||||
```
|
||||
|
||||
## Direct-to-radio dashboard
|
||||
|
||||
If the Meshtastic radio itself is connected to your computer over USB, the desktop can
|
||||
talk directly to it. Do not use `--xiao-bridge` in this mode.
|
||||
|
||||
```sh
|
||||
python -m pip install -e . pyserial
|
||||
python -m serial.tools.list_ports -v
|
||||
python examples/curses_dashboard.py /dev/cu.YOUR_RADIO_PORT
|
||||
```
|
||||
|
||||
The XIAO's USB port identifies as a MicroPython board. Direct mode will not work with
|
||||
that port; use `--xiao-bridge` when the radio is wired through the XIAO.
|
||||
|
||||
## Other boards
|
||||
|
||||
Ready-to-run examples are included for common MicroPython boards:
|
||||
|
||||
| Board | Example | UART | TX | RX |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Seeed Studio XIAO RP2040 | [`xiao_rp2040.py`](examples/xiao_rp2040.py) | UART0 | D6 / GPIO0 | D7 / GPIO1 |
|
||||
| Raspberry Pi Pico / Pico W | [`raspberry_pi_pico.py`](examples/raspberry_pi_pico.py) | UART0 | GP0 | GP1 |
|
||||
| Generic ESP32 | [`esp32.py`](examples/esp32.py) | UART2 | GPIO17 | GPIO16 |
|
||||
|
||||
Change the UART and pin numbers if your board uses a different mapping. See the
|
||||
complete [examples guide](examples/README.md) for wiring notes.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### `could not enter raw repl`
|
||||
|
||||
Use the exact port instead of `connect auto`:
|
||||
|
||||
```sh
|
||||
mpremote connect list
|
||||
mpremote connect /dev/cu.usbmodem2101 exec "print('MicroPython REPL OK')"
|
||||
```
|
||||
|
||||
If it still fails, close every other serial application, unplug and reconnect the
|
||||
XIAO, and try again.
|
||||
|
||||
### It repeatedly says `waiting for radio data`
|
||||
|
||||
The XIAO is running, but no complete Meshtastic frame has arrived. Check:
|
||||
|
||||
1. XIAO D6/TX goes to radio RX—not radio TX.
|
||||
2. XIAO D7/RX goes to radio TX.
|
||||
3. Both boards share GND.
|
||||
4. The radio UART is enabled in `PROTO` mode at 115200 baud.
|
||||
5. The configured radio UART pins match the pins you physically connected.
|
||||
|
||||
### The desktop curses dashboard stays at `configuration requested`
|
||||
|
||||
If the radio is wired through the XIAO, install `xiao_dashboard_bridge.py` as
|
||||
`main.py` and include `--xiao-bridge` in the dashboard command. Without the bridge,
|
||||
the desktop sends Meshtastic data to the MicroPython REPL instead of UART0.
|
||||
|
||||
Also confirm that `mpremote` or another serial monitor is not holding the port.
|
||||
|
||||
### The node count appears, but no messages appear
|
||||
|
||||
The node list is stored data downloaded from the radio. It is not a message history.
|
||||
Send a **new** text from another Meshtastic node while MicroMesh is running.
|
||||
|
||||
### A frame is skipped or reports a decode error
|
||||
|
||||
Meshtastic's protobuf schema evolves. MicroMesh reports and skips newer messages that
|
||||
its compact schema does not yet understand. Text and other supported packets continue
|
||||
processing. Update the installed files after pulling a newer MicroMesh version:
|
||||
|
||||
```sh
|
||||
mpremote connect /dev/cu.usbmodem2101 mip install package.json
|
||||
```
|
||||
|
||||
### The serial port is busy
|
||||
|
||||
One port can have only one owner. Quit the dashboard before using `mpremote`, and close
|
||||
`mpremote` before restarting the dashboard.
|
||||
|
||||
## Python API
|
||||
|
||||
Here is the smallest useful program:
|
||||
|
||||
```python
|
||||
from machine import UART, Pin
|
||||
from machine import Pin, UART
|
||||
from time import sleep_ms
|
||||
|
||||
from micromesh import PortNum, SerialInterface
|
||||
|
||||
uart = UART(1, baudrate=115200, tx=Pin(17), rx=Pin(16), timeout=0)
|
||||
|
||||
def received(packet):
|
||||
if packet.WhichOneof("payload_variant") != "decoded":
|
||||
return
|
||||
if packet.decoded.portnum == PortNum.TEXT_MESSAGE_APP:
|
||||
print("from !%08x:" % packet.from_, packet.decoded.payload.decode())
|
||||
print("from !%08x: %s" % (
|
||||
packet.from_,
|
||||
packet.decoded.payload.decode("utf-8"),
|
||||
))
|
||||
|
||||
|
||||
uart = UART(0, 115200, tx=Pin(0), rx=Pin(1), timeout=0, rxbuf=1024)
|
||||
mesh = SerialInterface(uart, on_packet=received)
|
||||
mesh.connect()
|
||||
mesh.sendText("hello mesh")
|
||||
|
||||
while True:
|
||||
mesh.poll()
|
||||
sleep_ms(10)
|
||||
```
|
||||
|
||||
The UART baud rate and pins depend on the attached Meshtastic device. `poll()` is
|
||||
non-blocking when the UART is configured with `timeout=0`.
|
||||
|
||||
Ready-to-run wiring and code for the Seeed Studio XIAO RP2040, Raspberry Pi Pico, and
|
||||
generic ESP32 boards are in [`examples/`](examples/README.md).
|
||||
|
||||
Direct messages accept either an integer or Meshtastic's hexadecimal node ID:
|
||||
### Send packets
|
||||
|
||||
```python
|
||||
# Broadcast text on the primary channel
|
||||
mesh.sendText("hello mesh")
|
||||
|
||||
# Direct message with an acknowledgement
|
||||
mesh.sendText("hello", destinationId="!a1b2c3d4", wantAck=True)
|
||||
|
||||
# Position packet
|
||||
mesh.sendPosition(45.5152, -122.6784, altitude=15)
|
||||
|
||||
# Custom application data
|
||||
mesh.sendData(b"custom", portNum=PortNum.PRIVATE_APP)
|
||||
```
|
||||
|
||||
## Protobufs
|
||||
Payloads may be at most 233 bytes. `poll()` is non-blocking when the UART uses
|
||||
`timeout=0`, so call it frequently from your main loop.
|
||||
|
||||
Messages can be used without a radio:
|
||||
### Inspect connection state
|
||||
|
||||
```python
|
||||
if mesh.config_complete:
|
||||
print("connected to", mesh.my_info.my_node_num)
|
||||
print("known nodes:", len(mesh.nodes))
|
||||
|
||||
for number, info in mesh.nodes.items():
|
||||
name = info.user.long_name if info.HasField("user") else "unknown"
|
||||
print("!%08x %s" % (number, name))
|
||||
```
|
||||
|
||||
### Use the lightweight protobuf codec
|
||||
|
||||
```python
|
||||
from micromesh import Data, PortNum
|
||||
|
||||
data = Data(portnum=PortNum.TEXT_MESSAGE_APP, payload=b"hello")
|
||||
encoded = data.SerializeToString()
|
||||
message = Data(portnum=PortNum.TEXT_MESSAGE_APP, payload=b"hello")
|
||||
encoded = message.SerializeToString()
|
||||
decoded = Data().ParseFromString(encoded)
|
||||
print(decoded.payload)
|
||||
```
|
||||
|
||||
Generated-module-style imports are available as `micromesh.mesh_pb2` and
|
||||
`micromesh.portnums_pb2` for easier porting from the desktop library.
|
||||
`micromesh.portnums_pb2`. A protobuf field named `from` is accessed as `from_` because
|
||||
`from` is a Python keyword.
|
||||
|
||||
Only the commonly useful schemas are modeled. Unknown fields survive a decode/encode
|
||||
round trip, but their contents are not interpreted. A field named `from` is accessed as
|
||||
`packet.from_` because `from` is a Python keyword.
|
||||
## Installation alternatives
|
||||
|
||||
Install from a local checkout with MicroPython's package installer:
|
||||
|
||||
```sh
|
||||
mpremote mip install package.json
|
||||
```
|
||||
|
||||
Or copy the package directory manually:
|
||||
|
||||
```sh
|
||||
mpremote fs cp -r micromesh :lib/
|
||||
```
|
||||
|
||||
For CPython development:
|
||||
|
||||
```sh
|
||||
python -m pip install -e .
|
||||
```
|
||||
|
||||
After the first PyPI release:
|
||||
|
||||
```sh
|
||||
python -m pip install micromesh
|
||||
```
|
||||
|
||||
## Supported scope
|
||||
|
||||
MicroMesh currently models the most useful portions of the Meshtastic API:
|
||||
|
||||
- `ToRadio` / `FromRadio` framing and configuration handshake
|
||||
- `MeshPacket`, `Data`, `Position`, `User`, and `NodeInfo`
|
||||
- Device metrics, waypoints, routing, queue status, and log records
|
||||
- Unknown-field preservation and stream resynchronization
|
||||
|
||||
Large configuration, admin, metadata, and module-configuration messages are retained
|
||||
as encoded bytes rather than fully interpreted.
|
||||
|
||||
The protocol definitions come from
|
||||
[`meshtastic/protobufs`](https://github.com/meshtastic/protobufs).
|
||||
|
||||
## Development
|
||||
|
||||
Set up the project and run the tests:
|
||||
|
||||
```sh
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
python -m pip install -e .
|
||||
python -m unittest discover -s tests
|
||||
```
|
||||
|
||||
The test suite covers encoding, decoding, framing, serial recovery, sending, and
|
||||
configuration state.
|
||||
|
||||
## Releasing to PyPI
|
||||
|
||||
PyPI publishing uses GitHub Trusted Publishing, so no API token is stored in the
|
||||
repository. Configure a pending publisher at
|
||||
[`pypi.org/manage/account/publishing`](https://pypi.org/manage/account/publishing/)
|
||||
with these values:
|
||||
Maintainers publish with GitHub Trusted Publishing; no long-lived PyPI token is stored
|
||||
in the repository. Configure the pending publisher with:
|
||||
|
||||
- PyPI project name: `micromesh`
|
||||
- GitHub owner: `pdxlocations`
|
||||
- Repository: `micromesh`
|
||||
- Workflow: `release.yaml`
|
||||
- Environment: `pypi`
|
||||
| Setting | Value |
|
||||
| --- | --- |
|
||||
| PyPI project | `micromesh` |
|
||||
| GitHub owner | `pdxlocations` |
|
||||
| Repository | `micromesh` |
|
||||
| Workflow | `release.yaml` |
|
||||
| Environment | `pypi` |
|
||||
|
||||
Keep the version in `pyproject.toml`, `package.json`, and `micromesh/__init__.py` in
|
||||
sync. Push the change, create a matching GitHub release such as `v0.1.0`, and publish
|
||||
the release. The workflow builds, validates, and uploads both the wheel and source
|
||||
distribution.
|
||||
sync. Publishing a matching GitHub release, such as `v0.1.0`, builds, validates, and
|
||||
uploads the wheel and source distribution.
|
||||
|
||||
The API protocol is defined by the
|
||||
[`meshtastic/protobufs`](https://github.com/meshtastic/protobufs) project. Update the
|
||||
field declarations in `micromesh/messages.py` when adopting newer schemas.
|
||||
## License
|
||||
|
||||
`manifest.py` is also included for freezing the package into custom MicroPython firmware.
|
||||
MicroMesh is released under the [GNU General Public License v3.0 or later](LICENSE).
|
||||
|
||||
Reference in New Issue
Block a user