Metadata-Version: 2.4
Name: micromesh
Version: 0.1.0
Summary: A small, dependency-free Meshtastic client for MicroPython
Author: MicroMesh contributors
License-Expression: GPL-3.0-or-later
Project-URL: Homepage, https://github.com/pdxlocations/micromesh
Project-URL: Repository, https://github.com/pdxlocations/micromesh.git
Project-URL: Issues, https://github.com/pdxlocations/micromesh/issues
Keywords: meshtastic,micropython,lora,protobuf
Classifier: Development Status :: 3 - Alpha
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: Implementation :: MicroPython
Classifier: Topic :: Communications :: Ham Radio
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# 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).
