# MicroMesh πŸ“» **A tiny, dependency-free Meshtastic client for MicroPython.** 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. No protobuf compiler runs on the board. No threads. No `google.protobuf`. Just a UART, three wires, and regular Python. > [!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. ## What can it do? - 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 optionally report and skip undecodable frames - Run a curses dashboard with live stats, nodes, and messages - Work on MicroPython without third-party runtime dependencies ## 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 β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` The XIAO does not contain a LoRa radio. It controls a separate Meshtastic device over UART. ## Before you begin 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.8 or newer on your Mac, Linux computer, or Windows PC For the UART-through-microcontroller setups, configure the Meshtastic radio's serial module for: ```text Enabled: yes Mode: PROTO Baud: 115200 (must match the MicroPython UART) ``` Meshtastic's serial-module default is 38400 baud; these examples deliberately use 115200. Set both ends to the same value. A direct USB connection to the radio does not use this external-UART setup. The radio's UART pin names depend on its model. Consult that board's pinout before connecting wires. > [!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 ``` 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 replace an automatically running script, upload a different `main.py`; `mpremote` stops the running program before filesystem commands. To disable automatic startup without replacing it: ```sh mpremote connect /dev/cu.usbmodem2101 rm :main.py mpremote connect /dev/cu.usbmodem2101 reset ``` ## 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 header shows `Config: complete` after a matching configuration handshake. Some radios do not return the matching completion ID; once local and node data are usable, the dashboard shows `Config: ready` instead. 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. Complete the XIAO quick-start installation through step 4 first, so the `micromesh` package is present on the board. ### 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. `message queued` means the radio accepted the packet for transmission; it is not a delivery receipt. | 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. Unknown fields are retained automatically. When an `on_error` callback is configuredβ€”as it is in the board examplesβ€”MicroMesh reports and skips a frame if it is malformed or conflicts with the compact schema; later frames continue processing. Without that callback, `poll()` raises the decoding error. 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 Pin, UART from time import sleep_ms from micromesh import PortNum, SerialInterface def received(packet): if packet.WhichOneof("payload_variant") != "decoded": return if packet.decoded.portnum == PortNum.TEXT_MESSAGE_APP: try: text = packet.decoded.payload.decode("utf-8") except UnicodeError: text = repr(packet.decoded.payload) print("from !%08x: %s" % (packet.from_, text)) def decode_error(error, payload): print("skipping undecodable frame:", error, "(%d bytes)" % len(payload)) uart = UART(0, 115200, tx=Pin(0), rx=Pin(1), timeout=0, rxbuf=1024) mesh = SerialInterface(uart, on_packet=received, on_error=decode_error) mesh.connect() while True: mesh.poll() sleep_ms(10) ``` ### 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) ``` Data payloads may be at most 233 bytes, including UTF-8 encoded text bytes rather than characters. `poll()` is non-blocking when the UART uses `timeout=0`, so call it frequently from your main loop. ### 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 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`. A protobuf field named `from` is accessed as `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, field presence, framing, serial recovery, sending, input validation, and configuration state. ## Releasing to PyPI Maintainers publish with GitHub Trusted Publishing; no long-lived PyPI token is stored in the repository. Configure the pending publisher with: | 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. Publishing a matching GitHub release, such as `v0.1.0`, builds, validates, and uploads the wheel and source distribution. ## License MicroMesh is released under the [GNU General Public License v3.0 or later](LICENSE).