A Meshtastic client for the TrimUI Brick and other NextUI/MinUI handhelds. Read and send messages, browse the mesh, and edit your radio's settings over Bluetooth LE or USB — no phone needed.
Small C core, pluggable transports, available in the
NextUI Pak Store or as a sideloadable
MeshClient.pak.
MeshClient is now live in the NextUI Pak Store. On your Brick, open Tools → Pak Store, find MeshClient under Miscellaneous Tools, and install it directly on the device. Launch it from Tools → MeshClient once installed.
To install a release by hand instead, download MeshClient.pak.zip from the
latest release and unzip it into a folder you
make — the zip holds the pak's contents, not the pak folder, because that is what the Pak Store
expects:
mkdir -p /Volumes/SDCARD/Tools/tg5040/MeshClient.pak
unzip MeshClient.pak.zip -d /Volumes/SDCARD/Tools/tg5040/MeshClient.pakUnzipping it without making that folder first gives you
MeshClient.pak/MeshClient.pak/launch.sh, which the launcher will not run. Copying the
already-unpacked dist/MeshClient.pak/ folder to Tools/tg5040/ works too.
The app then appears under Tools in the NextUI Launcher. Logs go to
/.userdata/tg5040/logs/MeshClient.txt. Once it is installed, Settings → About updates it in
place.
Experimental, and network only. The Miyoo has no Bluetooth, and its USB port does not host a radio, so it reaches a radio over Wi-Fi: an ESP32 radio with Wi-Fi turned on, or
meshtasticd. Tested on a Mini Plus running Onion OS.
Download MeshClient-miyoomini.zip from the
latest release and unzip it at the
root of the SD card. It holds App/MeshClient/, so it lands in the right place:
unzip MeshClient-miyoomini.zip -d /Volumes/SDCARDStart it from Apps → MeshClient. Then:
- Turn on network time (Apps → Tweaks → Network). The Miyoo's clock starts at 1970 without it, and the map downloads and update checks fail with this device's clock is wrong.
- Give it the radio's address in Radio → devices, on the last row. A Meshtastic radio
listens on port 4403 and a MeshCore Wi-Fi companion on 5000 (
192.168.1.50:5000). Or setMESHCLIENT_TCP_HOSTinApp/MeshClient/env.sh, which unzipping a new release never touches.
The log is App/MeshClient/MeshClient.txt. Settings → About updates it in place.
Experimental. The Brick is the target; the desktop builds are newer and less tested. Expect rough edges, and please open an issue when you hit one.
The same client runs in a window on a desktop and talks to a radio over Bluetooth, USB or the network. Each release carries an installer for both:
- macOS (Apple silicon and Intel, macOS 11 or later): download
MeshClient-macos.dmg, open it, and drag MeshClient to Applications. New releases are signed and notarized; older downloads may still require System Settings → Privacy & Security → Open Anyway. The app asks for Bluetooth the first time it looks for a radio. - Windows (x64, Windows 10 or later): download and run
MeshClient-windows-x86_64-setup.exe. It installs for your user only and needs no administrator. The installer is not code-signed yet, so SmartScreen may say "Windows protected your PC". Choose More info → Run anyway.
Either one then updates itself from Settings → About, exactly as the Brick does: check, install, and relaunch. On a Mac this works once the app is in Applications. Run straight from the disk image or from Downloads, macOS runs the app from a read-only copy that cannot be updated.
Five tabs — Messages, Nodes, Map, Radio, Settings — driven by the d-pad and face buttons. Connect from Radio → devices (it bonds and prompts for a PIN-mode node's six digits), or let auto-connect find your usual radio on its own.
There is a full CLI too, useful on a desktop and for scripting:
meshclient --list-devices # nearby nodes and USB ports
meshclient --status --json # handshake summary
meshclient --send-text "on my way" --dest '!433d1a2c' --ack # direct message, wait for the ackControls, flags and environment variables are in docs/cli.md.
No handheld needed for any of the above: the client's core has no framebuffer under it, so BLE, serial and TCP, the admin settings, the MQTT proxy and the firmware flasher all run headless. Releases carry that as one static binary with nothing to install — no Python, no runtime, no libdbus on the target:
curl -LO https://github.com/mcereal/mesh-client/releases/latest/download/meshclient-linux-x86_64
curl -LO https://github.com/mcereal/mesh-client/releases/latest/download/meshclient-linux-x86_64.sha256
sha256sum -c meshclient-linux-x86_64.sha256
chmod +x meshclient-linux-x86_64 && ./meshclient-linux-x86_64 --statusx86-64 only, as a download. On an ARM machine — a Raspberry Pi, an ARM server — build it
there instead with make linux-cli (needs musl-tools), which produces the same static binary
as meshclient-linux-aarch64. The other ARM binary in a release, meshclient-tg5040-aarch64,
is the handheld's: same client, built for the device, and what the in-app updater downloads.
It is not a general ARM CLI and a Pi will not run it.
Linux is the shipping desktop/server target. On a Linux host:
git submodule update --init --recursive # inkwell, inkcell, inkstand, nanopb, protobufs (Mbed TLS nests under inkwell)
make setup # libdbus-1-dev + the Python protobuf packages
make debug # needs CMake >= 3.21, Ninja and a C17 toolchain
make testThe build types are CMakePresets.json, so an editor that reads presets - VS Code's CMake
Tools, CLion, anything driving cmake --preset - configures exactly what make debug does:
cmake --preset debug && cmake --build build/debug
ctest --preset debugEither route writes build/debug/compile_commands.json, which is what .clangd points at, so
clangd indexes the tree after the build you were going to run anyway.
The native Windows SDL target builds with MSYS2's UCRT64 toolchain. Its scope, setup commands
and installer are in docs/windows.md. scripts/package-macos.sh builds the
Mac's .app and .dmg on a Mac; see docs/releasing.md.
On macOS, or any host with Docker, use the container targets — they bind-mount the repo and build
into build/linux/:
make docker-test # debug build + unit tests (image built on first use)
make docker-shell # bash inside the container
make docker-pak # static aarch64 build -> dist/MeshClient.pak.zip (+ .sha256)make help lists the rest. Sanitizers: make debug CMAKE_ARGS="-- -DMESHCLIENT_ENABLE_ASAN=ON"
(or UBSAN). make format runs clang-format over the tree.
make ui-capture walks the HUD through a scripted sequence of button presses and renders every
frame off-screen — the real navigation model and the real framebuffer renderer, drawing into
memory instead of /dev/fb0 — so a change is reviewable as a picture with no Brick anywhere
near. A GIF rather than a still, because most UI changes are about a transition:
make ui-capture ARGS="devtools/ui_capture/scenes/messages.scene -o messages.gif"
make docker-ui-capture ARGS="..." # on macOSScene scripts and the command list are in
docs/ui.md.
With the Brick on WiFi and the SSH Server pak installed, skip the SD card: set BRICK_HOST in
.brick.env (copy .brick.env.example), then make brick (build + push), make deploy,
make deploy-logs, make deploy-check, make deploy-shot (a PNG of the device's screen) and
make deploy-clip (a GIF of it). One-time setup and troubleshooting are in
docs/device.md.
| Path | Contents |
|---|---|
src/ |
core, event loop, transports, UI, utilities |
include/ |
public headers, mirroring src/ (core/, transport/, ui/, proto/, utils/) |
tests/ |
unit tests, one binary run via CTest |
scripts/ |
build/package automation, docker.sh, cross-build.sh, device deploy, frame encoding |
devtools/ |
host-only development tools; today the off-screen UI capture harness |
docker/ |
Dockerfile (dev and cross stages) and the cross toolchain bootstrap |
Tools/tg5040/MeshClient.pak/ |
pak scaffold: launch.sh |
proto/meshtastic/, third_party/nanopb/ |
upstream protobufs and nanopb (submodules); Mbed TLS is inkwell's, nested inside it |
docs/ |
architecture, transports, UI, CLI, device and release documentation |
site/ |
the meshclient.dev website (Astro, served by Cloudflare) |
docs/architecture.md— how the client is put together and whydocs/transport.md— BLE and USB serial, including the Brick's USB quirksdocs/ui.md— UI store, navigation model, framebuffer renderingdocs/i18n.md— the string catalog, adding a string, adding a languagedocs/mqtt.md— the MQTT client proxy, and the TLS that goes with itdocs/cli.md— flags, environment variables, on-device controlsdocs/device.md— Brick setup and the deploy loopdocs/help.md— the in-client help screen and what a note may saydocs/testing.md— test categories and filteringdocs/performance.md— what a press costs and how it was measureddocs/non-bugs.md— things that look like bugs and are notdocs/releasing.md— commit conventions and releases
pak.json at the repo root is the NextUI Pak Store
listing. Its version must match the release tag, so scripts/release-build.sh stamps it and
@semantic-release/git commits it — do not bump it by hand. The same step regenerates the
changelog entry for the release from its commit subjects (scripts/pak-changelog.py), so that
is not hand-maintained either. Its screenshots are the four stills above, rendered off-screen
from the scenes in devtools/ui_capture/scenes/shots/ by make screenshots — the renderer that
ships, at the panel's own 1024x768, so they are the frames the device would draw. Refresh them
after a UI change with that one command rather than by hand; make deploy-shot is still there
for a picture of the real panel.
Mozilla's CA roots are compiled into the binary, from third_party/mozilla-ca/cacert.pem
(curl.se/ca). The Brick has no system CA store, and a file in the
pak would not be delivered by self-update, so the roots travel with the binary instead. Refresh
them by re-downloading that file, running scripts/gen-ca-roots.py, and committing both.
Follow AGENTS.md for code style, testing and pull request expectations. Commits
follow Conventional Commits — see
docs/releasing.md.
Released under the terms of the license in LICENSE.




