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, no malloc, 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 (see test/), and drops into ESP-IDF, bare nRF52/STM32, or a host bridge unchanged.
  • MeshCoreCompanion.h/.cpp — a thin C++ wrapper. Inject any Stream (Serial1 on a Grove UART, USB CDC, SoftwareSerial), call loop() often, register lambda callbacks. It also auto-drains the radio's queue: on the MsgWaiting push it loops SyncNextMessage until empty, so you just receive onChannelMessage(...).

Transport is injected; the core never touches a port. "Physical serial only" is simply which Stream you hand the wrapper.

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");
}

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, getChannelonChannelInfo
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)

cd test
cc -std=c99 -Wall -Wextra -I../src test_codec.c ../src/meshcore_companion.c -o t && ./t

Exercises command encoding, frame reassembly across split reads, every parser, and resync past line garbage.

Notes

  • Timestamps: channel messages carry an epoch-seconds sender timestamp. Call getDeviceTime() (or setDeviceTime()) once so sendChannelText can stamp outgoing messages; otherwise it sends 0.
  • PSK derivation: setChannel takes a raw 16-byte secret. If you need to derive one from a passphrase, do it host-side (e.g. SHA-256 prefix, as meshcore.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_DATA before including the header if your traffic needs more.

License

MIT. Protocol derived from MeshCore and meshcore.js (MIT, © Liam Cottle).

S
Description
MeshCore C access to companion radio. Meshcore DEV uses _py for python and .js for Javascript. This matches. It is pure portable C and also has and Arduino C++ wrapper.
Readme MIT 260 KiB
Languages
C 80.1%
C++ 12.7%
Shell 4.8%
CMake 1.9%
Python 0.5%