7.6 KiB
MeshCoreCompanion
A client library for the MeshCore Companion Radio serial protocol, for when the radio lives on one MCU and your application (display, sensor hub, gateway) lives on another. Talk to a separate companion radio — e.g. a Seeed XIAO + Wio-SX1262 — over a UART/USB serial link and exchange channel messages, set channel PSKs, and read link metadata.
This is the inverse of the phone/web app: your host MCU plays the client, the companion radio plays the server.
Design: portable C core + Arduino wrapper
Two layers, so the protocol is reusable and testable far beyond Arduino:
meshcore_companion.h/.c— a portable C99 core. No I/O, nomalloc, no Arduino. It assembles inbound frames from a byte stream, builds outbound command frames into buffers you own, and parses payloads into plain structs. It compiles and runs on a desktop for unit testing (seetest/), and drops into ESP-IDF, bare nRF52/STM32, or a host bridge unchanged.MeshCoreCompanion.h/.cpp— a thin C++ wrapper. Inject anyStream(Serial1on a Grove UART, USB CDC,SoftwareSerial), callloop()often, register lambda callbacks. It also auto-drains the radio's queue: on theMsgWaitingpush it loopsSyncNextMessageuntil empty, so you just receiveonChannelMessage(...).
Transport is injected; the core never touches a port. "Physical serial only" is
simply which Stream you hand the wrapper.
XXX Add a why - because I want control from other embedded systems, talk about how I do compile meshcore directly in and juse use a SX??? lora modem, that however uses a lot of resources and memroy and some hardware doesn't have access to enough pins to do SPI. Also I write code that runs on low level embedded linux SBC or SOC that are dependent on C.
Wire protocol
frame = [type:u8][len:u16 LE][payload:len bytes], type 0x3C for app→radio,
0x3E for radio→app. payload[0] is the command/response/push code. Derived
from the MeshCore companion protocol and the meshcore.js reference client.
Install (PlatformIO)
Drop this folder into your project's lib/, or add to platformio.ini:
lib_deps = symlink:///path/to/MeshCoreCompanion
The companion radio must be flashed with the serial companion variant
(companion_radio_usb) and have its serial interface bound to the UART you wire
to — not BLE, and not the WiFi/TCP variant. Companions compile in one interface
at a time. If you are using an OS (e.g. Linux) you can use USB variant as the drivers will present a /dev/ttyX interface that looks same as serial. If wanting to use bare metal evices such as AVR/ESP32/nRF52/STM serial, then see notes below about changes to radio.
Quick start
#include "MeshCoreCompanion.h"
MeshCoreCompanion mc(Serial1);
void setup() {
Serial1.begin(115200, SERIAL_8N1, /*RX*/16, /*TX*/17);
mc.onDeviceInfo([](const mc_device_info_t& d){
mc.setChannelHexSecret(2, "sensors", "000102...0f"); // 32 hex chars
mc.getDeviceTime();
});
mc.onChannelMessage([](const mc_channel_msg_t& m){
// m.text is "SenderName: body"
});
mc.onChannelData([](const mc_channel_data_t& d){
// d.data / d.data_len, plus MC_SNR_DB(d.snr_q4), d.path_len
});
mc.begin(); // AppStart + DeviceQuery
}
void loop() {
mc.loop(); // pump + auto-drain
// mc.sendChannelText(2, "hello");
}
Linux / host build (CMake)
The portable C core needs no Arduino and no hardware — it builds and runs on any
desktop. A CMakeLists.txt at the repo root builds the core, the host unit test,
and the Linux examples (the Arduino C++ wrapper is skipped here — it needs
<Arduino.h>):
cmake -B build && cmake --build build
ctest --test-dir build --output-on-failure # run the codec unit test
Linux examples (examples-linux/)
Both talk to a companion radio over any tty — a USB-CDC device
(/dev/ttyACM0), a USB-serial adapter (/dev/ttyUSB0), or a raw UART
(/dev/serial0):
tty_bridge— live: receive/echo channel traffic and send messages../build/meshcore_tty /dev/ttyACM0info— one-shot: dump everything the radio reports (model, firmware, radio params, device time, channels + PSKs, stats), then exit../build/meshcore_info /dev/ttyACM0
No CMake? Compile a single example directly:
cc -std=c99 -Wall -Wextra -Isrc \
examples-linux/info/meshcore_info.c src/meshcore_companion.c -o meshcore_info
The same C core also ships starter examples for other toolchains in
examples-esp-idf/ (ESP-IDF UART driver) and examples-stm32/ (STM32 HAL).
What's covered
| Need | API |
|---|---|
| Handshake / identity | begin(), appStart(), deviceQuery() → onDeviceInfo, onSelfInfo |
| Receive channel text | onChannelMessage (auto-drained) |
| Receive channel data + metadata | onChannelData (SNR, path length, data type) |
| Send on a channel | sendChannelText(idx, text) |
| Set / read channel PSK | setChannel, setChannelHexSecret, getChannel → onChannelInfo |
| Device time | getDeviceTime, setDeviceTime, deviceEpochNow() |
| Adverts | sendSelfAdvert(flood) |
| Radio params / stats | getStats, plus mc_cmd_set_radio_params in the core |
The C core also exposes raw command builders and mc_parse for codes the
wrapper doesn't surface yet (contacts, battery, send-confirmed, etc.) — adding a
new command is one builder plus one case.
Testing the core (no hardware)
Run everything at once with ./testall.sh — it runs the CMake/ctest, PlatformIO
native, and Arduino-compile suites, and auto-skips whichever tools aren't
installed (so it works on a cmake-only or PlatformIO-only box):
./testall.sh
Or run a suite individually:
ctest --test-dir build --output-on-failure # via CMake (see above)
pio test -e native # via PlatformIO's native env
cd test && cc -std=c99 -Wall -Wextra -I../src test_codec.c ../src/meshcore_companion.c -o t && ./t
The unit test exercises command encoding, frame reassembly across split reads, every parser, and resync past line garbage.
Raw Serial Port
For embedded linux, windows etc, there is USB drivvers that expose a serial port we can access from the raw c. But from a bare metal (or FreeRTOS etc) embedded CPU we need TTL serial port access. To do this is relatively simple on most of the MeshCore hardware supported for Companion mode, however can't be done from online flasher.
XXX How to build example, lets start with a XIAO ESP32S3 with SX1262. Minimum steps here to show somone how to do it and work out what pins can be used on that board for RX/TX
And add a Arduino example (basic ESP32S3 dev) that also programs it, e.g. Name, Network to Australia Narrow band and to add a channel. Basically a zero config just plug it in and the host device sets it up how it needs. For completeness this examle code then loops watching the channel programmed and responding to any hello sent with a response.
Notes
- Timestamps: channel messages carry an epoch-seconds sender timestamp.
Call
getDeviceTime()(orsetDeviceTime()) once sosendChannelTextcan stamp outgoing messages; otherwise it sends0. - PSK derivation:
setChanneltakes a raw 16-byte secret. If you need to derive one from a passphrase, do it host-side (e.g. SHA-256 prefix, asmeshcore.js's transport-key util shows) — kept out of the core to stay dependency-free. - Buffer sizes: override
MC_MAX_PAYLOAD,MC_MAX_TEXT,MC_MAX_DATAbefore including the header if your traffic needs more.
License
MIT. Protocol derived from MeshCore and meshcore.js (MIT, © Liam Cottle).