diff --git a/README.md b/README.md index ef1697b..ce97c4a 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,19 @@ Two layers, so the protocol is reusable and testable far beyond Arduino: 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. +## Why talk to a companion radio? + +The alternative is to compile the full MeshCore stack into your own MCU and drive +an SX-series LoRa modem (e.g. an SX1262) directly over SPI. That works, but it is +heavy on flash and RAM, and it needs several free GPIOs for the SPI bus plus the +radio's control/IRQ lines — pins many boards (or your own design) simply can't +spare once a display, sensors and other peripherals are wired up. + +Running the radio as a *separate companion module* keeps all of that — the LoRa +modem, mesh routing and crypto — on the radio, and lets your application talk to +it over a single UART/USB serial link. Your host stays light, and because the +core is plain C99 with no dependencies it runs equally on a small microcontroller +or on a low-level embedded Linux SBC/SoC where C is the natural language. ## Wire protocol @@ -153,13 +165,104 @@ cd test && cc -std=c99 -Wall -Wextra -I../src test_codec.c ../src/meshcore_compa The unit test exercises command encoding, frame reassembly across split reads, every parser, and resync past line garbage. -## Raw Serial Port +## Connecting to the radio: USB vs TTL serial -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. +On a desktop or SBC (Linux, Windows, macOS) the companion's USB driver exposes a +serial port you open straight from C — `/dev/ttyACM0` and friends. From a +bare-metal / FreeRTOS host there's no USB stack, so you wire the companion's +**TTL UART** to a hardware serial port on your host. That's straightforward on +most MeshCore companion hardware, but it can't be done from the online flasher — +you have to build the firmware yourself. -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 +### Build a serial companion (XIAO ESP32S3 + SX1262) -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. +The companion firmware is built with [PlatformIO](https://platformio.org/), so +you need its source. Clone the MeshCore firmware repo (VS Code + PlatformIO, or +the `pio` CLI): + +```sh +git clone https://github.com/ripplebiz/MeshCore +cd MeshCore +``` + +A companion compiles in **one** host interface at a time, and the XIAO + +Wio-SX1262 variant ships a ready-made environment for each — pick the **serial** +one (no source edit needed): + +| PlatformIO environment | Host link | +|---|---| +| `Xiao_S3_WIO_companion_radio_usb` | native USB-CDC | +| `Xiao_S3_WIO_companion_radio_ble` | BLE | +| `Xiao_S3_WIO_companion_radio_wifi` | TCP / WiFi | +| **`Xiao_S3_WIO_companion_radio_serial`** | **hardware UART ← use this** | + +The `…_serial` env already routes the command interface to `Serial1`: it sets +`-D SERIAL_TX=D6 -D SERIAL_RX=D7` in `variants/xiao_s3_wio/platformio.ini`. Build +and flash it over USB: + +```sh +pio run -e Xiao_S3_WIO_companion_radio_serial -t upload +``` + +(If the upload can't find the board, put the XIAO in bootloader mode: hold **B**, +tap **R**, release **B**.) + +Then wire the radio to your host — companion **D6 (TX, GPIO43) → host RX**, +companion **D7 (RX, GPIO44) ← host TX**, **GND↔GND**. Those host pins are exactly +what `UART_RX_PIN` / `UART_TX_PIN` select in the AutoProvision example. The +Wio-SX1262 hat uses the SPI bus (D8–D10) plus GPIO38–42 for the radio, so D6/D7 +(and D0–D5) stay free; to use a different pair just override `SERIAL_TX` / +`SERIAL_RX` in that env's `build_flags`. + +### XIAO nRF52840 + SX1262 + +The `xiao_nrf52` variant is also a XIAO + SX1262, but it only ships +`companion_radio_usb` and `companion_radio_ble` — so how you connect decides how +much work it is: + +- **Host over USB → already works.** Flash `Xiao_nrf52_companion_radio_usb`: + ```sh + pio run -e Xiao_nrf52_companion_radio_usb -t upload + ``` + The nRF52840's native USB-CDC enumerates as `/dev/ttyACM*`, which is a serial + port like any other — the Linux examples and AutoProvision work unchanged. No + firmware edit needed. + +- **Host needs raw TTL UART → small firmware edit.** Unlike the ESP32-S3/RP2040, + the nRF52 path in `examples/companion_radio/main.cpp` only does + `serial_interface.begin(Serial)` (USB-CDC); the `SERIAL_RX`/`SERIAL_TX` + HardwareSerial route is guarded to ESP32/RP2040 only, so adding those flags on + nRF52 does nothing. To expose a TTL UART, bind `Serial1` in that `#else` branch: + ```cpp + Serial1.setPins(/*rx*/D7, /*tx*/D6); + Serial1.begin(115200); + serial_interface.begin(Serial1); // instead of begin(Serial) + ``` + (On the XIAO nRF52840 `Serial1` defaults to D6/D7 under the Adafruit core, but + `setPins` makes it explicit.) It's a few lines, mirroring the ESP32 path. + +Either way the host-side code here is identical — a USB-CDC companion and a +TTL-UART companion look the same to this library. + +### Zero-config provisioning from the host + +`examples/AutoProvision/` is a plug-and-go host (basic ESP32-S3 dev board): on +boot it programs the companion the way it wants — node name, the **AU narrow +band** region, and a channel — then loops watching that channel and replies to +any `hello` with a response. Wire it to the companion's UART, flash, and the +radio comes up configured with no manual steps: + +```sh +cd examples/AutoProvision +pio run -t upload && pio device monitor +``` + +It's a self-contained PlatformIO project (`platformio.ini` + `src/main.cpp`). The +region is set the same way you'd compile MeshCore firmware — `LORA_FREQ` / +`LORA_BW` / `LORA_SF` / `LORA_CR` / `LORA_TX_POWER` in `platformio.ini`'s +`build_flags` (916.575 MHz / 62.5 kHz / SF7 / CR4/8 / 20 dBm). The code is plain +Arduino C++, so you can rename `src/main.cpp` to `AutoProvision.ino` for the +Arduino IDE instead. ## Notes diff --git a/examples/AutoProvision/platformio.ini b/examples/AutoProvision/platformio.ini new file mode 100644 index 0000000..beb87db --- /dev/null +++ b/examples/AutoProvision/platformio.ini @@ -0,0 +1,30 @@ +; Self-contained PlatformIO project for the AutoProvision example. +; +; The radio region is set via build flags — the same LORA_* defines you'd use +; when compiling MeshCore firmware. Override them here for your region. +; +; cd examples/AutoProvision +; pio run -t upload +; pio device monitor +; +; The library is pulled from the repo root (two levels up) via a relative +; symlink dependency, so there's no copy of the source. + +[env:esp32-s3] +platform = espressif32 +board = esp32-s3-devkitc-1 +framework = arduino +monitor_speed = 115200 +lib_deps = symlink://../.. + +build_flags = + ; host UART pins to the companion + -D UART_RX_PIN=17 + -D UART_TX_PIN=18 + -D UART_BAUD=115200 + ; AU narrow band radio region + -D LORA_FREQ=916.575 + -D LORA_BW=62.5 + -D LORA_SF=7 + -D LORA_CR=8 + -D LORA_TX_POWER=20 diff --git a/examples/AutoProvision/src/main.cpp b/examples/AutoProvision/src/main.cpp new file mode 100644 index 0000000..4e0890e --- /dev/null +++ b/examples/AutoProvision/src/main.cpp @@ -0,0 +1,103 @@ +/* + * AutoProvision — main.cpp + * + * Zero-config host: plug in an ESP32-S3 dev board wired to a MeshCore companion + * radio and it programs the radio the way it wants on boot — node name, radio + * region (AU narrow band here) and a channel — then loops watching that channel + * and replies to any "hello" with a response. + * + * The radio keeps these settings, so this is a one-button way to provision a + * fresh companion. The LORA_* region values come from platformio.ini build + * flags (the same defines you'd compile MeshCore firmware with); the #ifndef + * fallbacks below just document the defaults. + * + * Project layout: this is a PlatformIO project (platformio.ini + src/main.cpp). + * The code is plain Arduino C++ — to use it as an Arduino IDE sketch instead, + * rename this file to AutoProvision.ino, move it up one level, and drop + * platformio.ini (set the LORA_* defines below or in the IDE build flags). + * + * Wiring: host TX -> companion RX, host RX <- companion TX, GND<->GND. The + * companion must run the serial companion firmware with its interface on a + * hardware UART (see the README "Build a companion radio" section). + * + * SPDX-License-Identifier: MIT + */ +#include +#include "MeshCoreCompanion.h" + +// --- Host UART to the companion (set in platformio.ini build_flags) --- +#ifndef UART_RX_PIN +#define UART_RX_PIN 17 // host RX <- companion TX +#endif +#ifndef UART_TX_PIN +#define UART_TX_PIN 18 // host TX -> companion RX +#endif +#ifndef UART_BAUD +#define UART_BAUD 115200 +#endif + +// --- Radio region: AU narrow band (normally set in platformio.ini build_flags) --- +#ifndef LORA_FREQ +#define LORA_FREQ 916.575 // MHz +#endif +#ifndef LORA_BW +#define LORA_BW 62.5 // kHz +#endif +#ifndef LORA_SF +#define LORA_SF 7 +#endif +#ifndef LORA_CR +#define LORA_CR 8 +#endif +#ifndef LORA_TX_POWER +#define LORA_TX_POWER 20 // dBm +#endif + +// --- Identity + channel to provision --- +static const char* NODE_NAME = "auto-host"; +static const uint8_t CHANNEL_IDX = 2; +static const char* CHANNEL_NAME = "auto"; +static const char* CHANNEL_PSK_HEX = "000102030405060708090a0b0c0d0e0f"; // replace + +static const char* HELLO_TEXT = "hello"; +static const char* REPLY_TEXT = "hi there, auto-host here"; + +MeshCoreCompanion mc(Serial1); + +static void provision() { + mc.setAdvertName(NODE_NAME); + mc.setRadioParams(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR); + mc.setTxPower(LORA_TX_POWER); + mc.setChannelHexSecret(CHANNEL_IDX, CHANNEL_NAME, CHANNEL_PSK_HEX); + mc.getDeviceTime(); // so our replies get a sensible timestamp + mc.sendSelfAdvert(true); // announce ourselves on the new settings +} + +void setup() { + Serial.begin(115200); + Serial1.begin(UART_BAUD, SERIAL_8N1, UART_RX_PIN, UART_TX_PIN); + + mc.onDeviceInfo([](const mc_device_info_t& d) { + Serial.printf("[radio] %s fw=%d — provisioning...\n", d.model, d.fw_ver); + provision(); + Serial.printf("[radio] set name=%s %.3f MHz / %.1f kHz SF%d CR4/%d %ddBm channel '%s'\n", + NODE_NAME, (double)LORA_FREQ, (double)LORA_BW, + (int)LORA_SF, (int)LORA_CR, (int)LORA_TX_POWER, CHANNEL_NAME); + }); + + // Watch the channel; reply to any message whose body is "hello". + mc.onChannelMessage([](const mc_channel_msg_t& m) { + Serial.printf("[ch %d] %s\n", m.channel_idx, m.text); // "Sender: body" + const char* body = strchr(m.text, ':'); + body = body ? body + 1 : m.text; + while (*body == ' ') body++; + if (strcasecmp(body, HELLO_TEXT) == 0) + mc.sendChannelText(CHANNEL_IDX, REPLY_TEXT); + }); + + mc.begin(); // AppStart + DeviceQuery -> onDeviceInfo fires when the radio answers +} + +void loop() { + mc.loop(); // pump + auto-drain +} diff --git a/src/MeshCoreCompanion.cpp b/src/MeshCoreCompanion.cpp index 54707a2..cf97faa 100644 --- a/src/MeshCoreCompanion.cpp +++ b/src/MeshCoreCompanion.cpp @@ -128,6 +128,22 @@ void MeshCoreCompanion::getStats(uint8_t statsType) { uint8_t p[2]; sendPayload(p, mc_cmd_get_stats(p, sizeof(p), statsType)); } +/* ---- provisioning ---- */ +void MeshCoreCompanion::setAdvertName(const char *name) { + uint8_t p[1 + MC_MAX_TEXT]; + sendPayload(p, mc_cmd_set_advert_name(p, sizeof(p), name)); +} +void MeshCoreCompanion::setRadioParams(float freqMHz, float bwKHz, uint8_t sf, uint8_t cr) { + /* wire units: freq = MHz*1000 (kHz), bw = kHz*1000 (Hz) */ + uint32_t freq = (uint32_t)(freqMHz * 1000.0f + 0.5f); + uint32_t bw = (uint32_t)(bwKHz * 1000.0f + 0.5f); + uint8_t p[11]; + sendPayload(p, mc_cmd_set_radio_params(p, sizeof(p), freq, bw, sf, cr)); +} +void MeshCoreCompanion::setTxPower(uint32_t dbm) { + uint8_t p[5]; sendPayload(p, mc_cmd_set_tx_power(p, sizeof(p), dbm)); +} + /* ---- contacts ---- */ void MeshCoreCompanion::getContacts(uint32_t sinceLastmod) { uint8_t p[5]; sendPayload(p, mc_cmd_get_contacts(p, sizeof(p), sinceLastmod)); diff --git a/src/MeshCoreCompanion.h b/src/MeshCoreCompanion.h index 1134394..73b3b36 100644 --- a/src/MeshCoreCompanion.h +++ b/src/MeshCoreCompanion.h @@ -50,6 +50,10 @@ public: void getDeviceTime(); void setDeviceTime(uint32_t epochSecs); void sendSelfAdvert(bool flood = true); + /* Provisioning: node name, radio region, tx power. */ + void setAdvertName(const char *name); + void setRadioParams(float freqMHz, float bwKHz, uint8_t sf, uint8_t cr); + void setTxPower(uint32_t dbm); void getChannel(uint8_t idx); void setChannel(uint8_t idx, const char *name, const uint8_t secret[MC_SECRET_LEN]); /* Set a channel using a 32-hex-char PSK string (-> 16 bytes). false if malformed. */ diff --git a/src/meshcore_companion.c b/src/meshcore_companion.c index e393adb..db91074 100644 --- a/src/meshcore_companion.c +++ b/src/meshcore_companion.c @@ -252,6 +252,20 @@ size_t mc_cmd_set_radio_params(uint8_t *out, size_t cap, uint32_t freq_hz_x1000, return i; } +size_t mc_cmd_set_advert_name(uint8_t *out, size_t cap, const char *name) { + size_t nlen = name ? strlen(name) : 0; + size_t total = 1 + nlen; + if (cap < total) return 0; + out[0] = MC_CMD_SET_ADVERT_NAME; + if (nlen) memcpy(out + 1, name, nlen); + return total; +} + +size_t mc_cmd_set_tx_power(uint8_t *out, size_t cap, uint32_t dbm) { + if (cap < 5) return 0; + out[0] = MC_CMD_SET_TX_POWER; put_u32(out + 1, dbm); return 5; +} + size_t mc_cmd_get_stats(uint8_t *out, size_t cap, uint8_t stats_type) { if (cap < 2) return 0; out[0] = MC_CMD_GET_STATS; out[1] = stats_type; return 2; diff --git a/src/meshcore_companion.h b/src/meshcore_companion.h index 4ecee46..5c2e736 100644 --- a/src/meshcore_companion.h +++ b/src/meshcore_companion.h @@ -208,6 +208,10 @@ size_t mc_cmd_send_cmd (uint8_t *out, size_t cap, uint32_t sender_ts, const char *cmd); size_t mc_cmd_set_radio_params (uint8_t *out, size_t cap, uint32_t freq_hz_x1000, uint32_t bw, uint8_t sf, uint8_t cr); +/* Node name shown in adverts (cmd 8). */ +size_t mc_cmd_set_advert_name (uint8_t *out, size_t cap, const char *name); +/* Radio TX power in dBm (cmd 12). */ +size_t mc_cmd_set_tx_power (uint8_t *out, size_t cap, uint32_t dbm); size_t mc_cmd_get_stats (uint8_t *out, size_t cap, uint8_t stats_type); /* ---- contacts (Phase 2) ---- */ diff --git a/test/test_codec.c b/test/test_codec.c index 60cd949..4bd7854 100644 --- a/test/test_codec.c +++ b/test/test_codec.c @@ -415,6 +415,22 @@ int main(void) { "parse_status fields"); } + printf("== build: provisioning (name / radio / tx power) ==\n"); + { + plen = mc_cmd_set_advert_name(scratch, sizeof scratch, "MyNode"); + CHECK(plen == 7 && scratch[0] == MC_CMD_SET_ADVERT_NAME && + memcmp(scratch + 1, "MyNode", 6) == 0, "set_advert_name"); + plen = mc_cmd_set_tx_power(scratch, sizeof scratch, 20); + CHECK(plen == 5 && scratch[0] == MC_CMD_SET_TX_POWER && scratch[1] == 20 && + scratch[2] == 0 && scratch[4] == 0, "set_tx_power (u32 LE)"); + /* AU narrowband: 916.575 MHz -> 916575 (0x000DFC5F), 62.5 kHz -> 62500 (0xF424) */ + plen = mc_cmd_set_radio_params(scratch, sizeof scratch, 916575, 62500, 7, 8); + CHECK(plen == 11 && scratch[0] == MC_CMD_SET_RADIO_PARAMS && + scratch[1] == 0x5F && scratch[2] == 0xFC && scratch[3] == 0x0D && scratch[4] == 0x00 && + scratch[5] == 0x24 && scratch[6] == 0xF4 && scratch[9] == 7 && scratch[10] == 8, + "set_radio_params freq/bw LE + sf/cr"); + } + printf("== resync: garbage before a valid frame ==\n"); uint8_t junk[3] = { 0x00, 0x99, 0x01 }; mc_rx_feed(&rx, junk, 3);