diff --git a/README.md b/README.md index 8259c05..5c4f89b 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,11 @@ radio plays the *server* — the inverse of the phone/web app. Sibling of [`meshcore.js`](https://github.com/meshcore-dev/meshcore.js) (JS) and `meshcore_py` (Python): this is the **C / C++** one. +SEE ALSO: +* RUST - meshcore-rs (aka meschore_rs) - https://github.com/andrewdavidmackenzie/meshcore-rs.git +* Javascript - meshcore.js - https://github.com/meshcore-dev/meshcore.js +* Python - meshcore_py - https://github.com/meshcore-dev/meshcore_py + ## Two ways to use this repo The protocol logic is a single portable **C99 core** with no I/O, no `malloc`, and @@ -19,10 +24,9 @@ two audiences from **one source of truth** (`src/meshcore_companion.{c,h}`): 1. **Portable C library** — drop `src/meshcore_companion.{c,h}` into any project. It assembles inbound frames from a byte stream, builds outbound command frames into buffers you own, and parses payloads into plain structs. You supply the - transport. A complete Linux example (POSIX `termios`) lives in - `examples-linux/`, and the host unit test in `test/` runs with no hardware. - The same core drops into ESP-IDF, bare nRF52/STM32, or any host bridge - unchanged (those examples are planned). + transport. The same core drops into Linux, ESP-IDF, bare STM32/nRF52, or any + host bridge unchanged — see the example for each platform under `examples-*/`, + plus the host unit test in `test/` that runs with no hardware. 2. **C++ Arduino library** — `src/MeshCoreCompanion.{h,cpp}` wrap the core in an Arduino-friendly class: inject any `Stream` (`Serial1` on a Grove UART, USB @@ -45,11 +49,18 @@ meshcore_c/ ├── examples/ │ └── SensorChannelBridge/ # Arduino sketch (.ino) ├── examples-linux/ -│ └── tty_bridge/ # portable C example: any Linux tty (USB or raw UART) +│ └── tty_bridge/ # portable C: any Linux tty (USB or raw UART) +├── examples-esp-idf/ +│ └── tty_bridge/ # portable C: ESP-IDF UART driver (esp32/s3/c3) +├── examples-stm32/ +│ └── uart_bridge/ # portable C: STM32 HAL UART drop-in └── test/ └── test_codec.c # host unit test (no hardware) ``` +The portable-C examples all compile the *same* `src/meshcore_companion.c`; only +the byte transport differs per platform. + The Arduino/PlatformIO build only compiles `src/` (the manifests' `srcFilter` is scoped there), so the Linux example and host test never enter a firmware build. The CMake build only compiles the pure-C parts and ignores the Arduino wrapper @@ -84,6 +95,26 @@ cc -std=c99 -Wall -Wextra -Isrc \ examples-linux/tty_bridge/meshcore_tty.c src/meshcore_companion.c -o meshcore_tty ``` +## Other platform examples (same C core) + +Each example compiles `src/meshcore_companion.c` directly and supplies only a +platform-specific UART transport — no copy of the core is kept anywhere. + +- **ESP-IDF** — `examples-esp-idf/tty_bridge/`. A full IDF project using the UART + driver: + ```sh + cd examples-esp-idf/tty_bridge + idf.py set-target esp32s3 && idf.py build flash monitor + ``` +- **STM32** — `examples-stm32/uart_bridge/`. A HAL drop-in (STM32 builds are + board/toolchain specific): add `meshcore_stm32.c` + `src/` to a CubeMX/CubeIDE + project and call `meshcore_setup()` / `meshcore_poll()` from `main()`. See its + README. + +The transport is just two operations — *write bytes* and *read available bytes* — +so porting to bare-metal STM32, nRF52 (nRF5 SDK or Zephyr), or any other MCU only +changes those calls; the protocol code is identical. + ## Use the Arduino library Install via PlatformIO (`lib_deps = symlink:///path/to/meshcore_c`), the Arduino diff --git a/examples-esp-idf/tty_bridge/CMakeLists.txt b/examples-esp-idf/tty_bridge/CMakeLists.txt new file mode 100644 index 0000000..834911c --- /dev/null +++ b/examples-esp-idf/tty_bridge/CMakeLists.txt @@ -0,0 +1,5 @@ +# Project-level CMake for the ESP-IDF build system. +# Build with: idf.py set-target esp32s3 && idf.py build flash monitor +cmake_minimum_required(VERSION 3.16) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(meshcore_tty_bridge) diff --git a/examples-esp-idf/tty_bridge/README.md b/examples-esp-idf/tty_bridge/README.md new file mode 100644 index 0000000..3e03e20 --- /dev/null +++ b/examples-esp-idf/tty_bridge/README.md @@ -0,0 +1,30 @@ +# ESP-IDF example: tty_bridge + +Drives a MeshCore Companion Radio over a hardware UART using the ESP-IDF UART +driver. The protocol logic is the repo's portable C core +(`src/meshcore_companion.c`) — this example only supplies the transport and an +`app_main()` loop. + +## Wiring + +| Companion radio | ESP32 (default pins) | +|-----------------|----------------------| +| TX → host RX | GPIO16 (`MC_UART_RX_PIN`) | +| RX ← host TX | GPIO17 (`MC_UART_TX_PIN`) | +| GND | GND | + +Change the pins/UART at the top of `main/main.c`. The radio must run the serial +companion firmware (`companion_radio_usb`) bound to this UART. + +## Build + +Requires an installed [ESP-IDF](https://docs.espressif.com/projects/esp-idf/) +(v5.x). From this directory: + +```sh +idf.py set-target esp32s3 # or esp32, esp32c3, ... +idf.py build flash monitor +``` + +`main/CMakeLists.txt` compiles `../../../src/meshcore_companion.c` directly, so +there is no copy of the core to keep in sync. diff --git a/examples-esp-idf/tty_bridge/main/CMakeLists.txt b/examples-esp-idf/tty_bridge/main/CMakeLists.txt new file mode 100644 index 0000000..b07865f --- /dev/null +++ b/examples-esp-idf/tty_bridge/main/CMakeLists.txt @@ -0,0 +1,8 @@ +# Compile the portable C core straight from the repo's src/ -- no copy, no +# duplication. The path is relative to this component directory: +# examples-esp-idf/tty_bridge/main -> ../../../src +set(MC_CORE_DIR "${CMAKE_CURRENT_LIST_DIR}/../../../src") + +idf_component_register( + SRCS "main.c" "${MC_CORE_DIR}/meshcore_companion.c" + INCLUDE_DIRS "." "${MC_CORE_DIR}") diff --git a/examples-esp-idf/tty_bridge/main/main.c b/examples-esp-idf/tty_bridge/main/main.c new file mode 100644 index 0000000..322de81 --- /dev/null +++ b/examples-esp-idf/tty_bridge/main/main.c @@ -0,0 +1,130 @@ +/* + * main.c -- ESP-IDF example: drive a MeshCore Companion Radio over a UART. + * + * Same portable C99 core as every other example (src/meshcore_companion.{c,h}), + * here with the ESP-IDF UART driver as the byte transport. Wire the companion + * radio's serial lines to MC_UART_TX_PIN / MC_UART_RX_PIN and flash this onto an + * ESP32 / ESP32-S3 / ESP32-C3 etc. + * + * idf.py set-target esp32s3 + * idf.py build flash monitor + * + * The companion radio must run the serial companion firmware (companion_radio_usb) + * with its interface bound to this UART. + * + * SPDX-License-Identifier: MIT + * Author: Scott Penrose / Digital Dimensions. + */ +#include +#include + +#include "driver/uart.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#include "meshcore_companion.h" + +/* --- UART wiring: change to free pins on your board --- */ +#define MC_UART_NUM UART_NUM_1 +#define MC_UART_TX_PIN 17 /* host TX -> companion RX */ +#define MC_UART_RX_PIN 16 /* host RX <- companion TX */ +#define MC_UART_BAUD 115200 +#define MC_UART_BUFSZ 1024 + +static const char *TAG = "meshcore"; + +static void send_payload(const uint8_t *payload, size_t len) +{ + uint8_t frame[MC_RX_BUFSZ]; + size_t flen = mc_frame_encode(payload, len, frame, sizeof frame); + if (flen) uart_write_bytes(MC_UART_NUM, (const char *)frame, flen); +} + +static void on_event(const mc_event_t *ev) +{ + switch (ev->code) { + case MC_RESP_DEVICE_INFO: + ESP_LOGI(TAG, "radio model=%s fw=%d channels=%u build=%s", + ev->u.device_info.model, ev->u.device_info.fw_ver, + (unsigned)ev->u.device_info.max_channels, + ev->u.device_info.build_date); + break; + case MC_RESP_CHANNEL_MSG_RECV: /* body is "SenderName: message" */ + ESP_LOGI(TAG, "[ch %d] %s", ev->u.channel_msg.channel_idx, + ev->u.channel_msg.text); + break; + case MC_RESP_CHANNEL_DATA_RECV: { + /* SNR is q4 (x4). Print exact dB without floating-point printf. */ + int centi = ev->u.channel_data.snr_q4 * 25; /* /4 then *100 */ + const char *sign = centi < 0 ? "-" : ""; + ESP_LOGI(TAG, "[ch %d] %u bytes type=0x%04X snr=%s%d.%02d dB %s", + ev->u.channel_data.channel_idx, + (unsigned)ev->u.channel_data.data_len, + (unsigned)ev->u.channel_data.data_type, + sign, abs(centi) / 100, abs(centi) % 100, + ev->u.channel_data.path_len == MC_PATH_DIRECT ? "direct" : "flood"); + break; + } + case MC_RESP_CURR_TIME: + ESP_LOGI(TAG, "device time = %u (epoch secs)", (unsigned)ev->u.curr_time); + break; + case MC_RESP_ERR: + ESP_LOGW(TAG, "radio error response (code=%d)", ev->u.err_code); + break; + default: + break; + } +} + +void app_main(void) +{ + const uart_config_t cfg = { + .baud_rate = MC_UART_BAUD, + .data_bits = UART_DATA_8_BITS, + .parity = UART_PARITY_DISABLE, + .stop_bits = UART_STOP_BITS_1, + .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, + .source_clk = UART_SCLK_DEFAULT, + }; + ESP_ERROR_CHECK(uart_driver_install(MC_UART_NUM, MC_UART_BUFSZ, 0, 0, NULL, 0)); + ESP_ERROR_CHECK(uart_param_config(MC_UART_NUM, &cfg)); + ESP_ERROR_CHECK(uart_set_pin(MC_UART_NUM, MC_UART_TX_PIN, MC_UART_RX_PIN, + UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE)); + + /* Handshake: AppStart -> SelfInfo, DeviceQuery -> DeviceInfo. */ + uint8_t cmd[MC_MAX_PAYLOAD]; + size_t n; + n = mc_cmd_app_start(cmd, sizeof cmd, "esp-idf"); if (n) send_payload(cmd, n); + n = mc_cmd_device_query(cmd, sizeof cmd, 1); if (n) send_payload(cmd, n); + n = mc_cmd_get_device_time(cmd, sizeof cmd); if (n) send_payload(cmd, n); + + mc_rx_t rx; + mc_rx_init(&rx); + ESP_LOGI(TAG, "listening on UART%d (rx=%d tx=%d)", + MC_UART_NUM, MC_UART_RX_PIN, MC_UART_TX_PIN); + + for (;;) { + uint8_t in[128]; + int got = uart_read_bytes(MC_UART_NUM, in, sizeof in, pdMS_TO_TICKS(100)); + if (got <= 0) continue; + + mc_rx_feed(&rx, in, (size_t)got); + + uint8_t payload[MC_MAX_PAYLOAD]; + size_t plen; + while (mc_rx_poll(&rx, payload, sizeof payload, &plen)) { + mc_event_t ev; + if (!mc_parse(payload, plen, &ev)) continue; + on_event(&ev); + /* Drain the radio's queue when it signals waiting messages. */ + if (ev.code == MC_PUSH_MSG_WAITING || + ev.code == MC_RESP_CHANNEL_MSG_RECV || + ev.code == MC_RESP_CHANNEL_DATA_RECV || + ev.code == MC_RESP_CONTACT_MSG_RECV) { + n = mc_cmd_sync_next_message(cmd, sizeof cmd); + if (n) send_payload(cmd, n); + } + } + } +} diff --git a/examples-stm32/uart_bridge/README.md b/examples-stm32/uart_bridge/README.md new file mode 100644 index 0000000..77780c7 --- /dev/null +++ b/examples-stm32/uart_bridge/README.md @@ -0,0 +1,44 @@ +# STM32 example: uart_bridge + +Integration example showing how to drive a MeshCore Companion Radio from an STM32 +using the CubeMX-generated HAL. The protocol logic is the repo's portable C core +(`src/meshcore_companion.c`); this file supplies only the UART transport and two +entry points you call from your generated `main()`. + +Because an STM32 firmware build is tied to your specific MCU, clocks, pins and +linker script (all produced by STM32CubeIDE / CubeMX), this is **not** a +standalone buildable project — it is a drop-in. + +## Steps (STM32CubeIDE / CubeMX) + +1. Generate a project with one USART enabled at **115200 8N1** (e.g. `USART1`). + Wire it to the companion radio: host TX → radio RX, host RX ← radio TX, GND↔GND. + The radio must run the serial companion firmware (`companion_radio_usb`). +2. Add `src/meshcore_companion.c` and `src/meshcore_companion.h` from this repo to + your project (or add this repo's `src/` to the include paths). +3. Add `meshcore_stm32.c` to your project. If your USART handle isn't `huart1`, + change the `extern UART_HandleTypeDef huart1;` line near the top. +4. In the generated `main.c`: + ```c + /* USER CODE BEGIN 2 */ + meshcore_setup(); + /* USER CODE END 2 */ + + while (1) { + /* USER CODE BEGIN 3 */ + meshcore_poll(); + } + ``` + +## Notes + +- `meshcore_poll()` polls the UART one byte at a time, which is fine for the + companion's low data rate. For high throughput, switch the RX side to + interrupt/DMA into a ring buffer and feed that to `mc_rx_feed()` — the core + code is unchanged. +- Logging uses `printf()`. Retarget it to a **separate** debug UART or SWO/ITM + (commonly USART2 = the ST-Link VCP) by implementing `_write()`; do not point it + at the companion UART. +- The same two-function transport pattern (`send bytes` / `read available bytes`) + ports directly to bare-metal STM32, nRF52 (nRF5 SDK or Zephyr), or any other + MCU — only the HAL calls change. diff --git a/examples-stm32/uart_bridge/meshcore_stm32.c b/examples-stm32/uart_bridge/meshcore_stm32.c new file mode 100644 index 0000000..fd60983 --- /dev/null +++ b/examples-stm32/uart_bridge/meshcore_stm32.c @@ -0,0 +1,123 @@ +/* + * meshcore_stm32.c -- STM32 HAL integration example. + * + * Unlike the Linux and ESP-IDF examples, STM32 builds are board- and + * toolchain-specific (the CubeMX-generated HAL, startup code and linker script + * belong to your project), so this is an *integration example* rather than a + * standalone build. It shows the only two things the portable core needs from a + * platform: write some bytes, and read whatever bytes have arrived. + * + * How to use (STM32CubeIDE / CubeMX): + * 1. Generate a project with one USART enabled at 115200 8N1 (e.g. USART1). + * Wire it to the companion radio (host TX -> radio RX, host RX <- radio TX). + * 2. Add src/meshcore_companion.c and src/meshcore_companion.h to the project + * (Core/Src and Core/Inc, or add this repo's src/ to the include paths). + * 3. Add this file to the project. + * 4. In the generated main(): call meshcore_setup() once after MX_USARTx_UART_Init(), + * then call meshcore_poll() every iteration of the main while(1) loop. + * + * Byte-at-a-time polling is fine for the companion's low data rate; for high + * throughput switch the transport to interrupt/DMA RX into a ring buffer and + * feed that buffer to mc_rx_feed() — the core code does not change. + * + * Logging here uses printf(); retarget it to a *separate* debug UART or SWO/ITM + * (do not point it at the companion UART). On many CubeIDE projects that means + * implementing _write() to HAL_UART_Transmit on USART2 (the ST-Link VCP). + * + * SPDX-License-Identifier: MIT + * Author: Scott Penrose / Digital Dimensions. + */ +#include "main.h" /* CubeMX-generated: pulls in stm32xxxx_hal.h + handles */ + +#include +#include + +#include "meshcore_companion.h" + +/* The USART you enabled in CubeMX and wired to the companion radio. */ +extern UART_HandleTypeDef huart1; +#define MC_UART (&huart1) + +static mc_rx_t s_rx; + +static void send_payload(const uint8_t *payload, size_t len) +{ + uint8_t frame[MC_RX_BUFSZ]; + size_t flen = mc_frame_encode(payload, len, frame, sizeof frame); + if (flen) HAL_UART_Transmit(MC_UART, frame, (uint16_t)flen, HAL_MAX_DELAY); +} + +static void on_event(const mc_event_t *ev) +{ + switch (ev->code) { + case MC_RESP_DEVICE_INFO: + printf("radio model=%s fw=%d channels=%u build=%s\r\n", + ev->u.device_info.model, ev->u.device_info.fw_ver, + (unsigned)ev->u.device_info.max_channels, + ev->u.device_info.build_date); + break; + case MC_RESP_CHANNEL_MSG_RECV: /* body is "SenderName: message" */ + printf("[ch %d] %s\r\n", ev->u.channel_msg.channel_idx, + ev->u.channel_msg.text); + break; + case MC_RESP_CHANNEL_DATA_RECV: { + int centi = ev->u.channel_data.snr_q4 * 25; /* q4 -> /4 then *100 */ + const char *sign = centi < 0 ? "-" : ""; + printf("[ch %d] %u bytes type=0x%04X snr=%s%d.%02d dB %s\r\n", + ev->u.channel_data.channel_idx, + (unsigned)ev->u.channel_data.data_len, + (unsigned)ev->u.channel_data.data_type, + sign, abs(centi) / 100, abs(centi) % 100, + ev->u.channel_data.path_len == MC_PATH_DIRECT ? "direct" : "flood"); + break; + } + case MC_RESP_CURR_TIME: + printf("device time = %u (epoch secs)\r\n", (unsigned)ev->u.curr_time); + break; + case MC_RESP_ERR: + printf("radio error response (code=%d)\r\n", ev->u.err_code); + break; + default: + break; + } +} + +/* Call once after MX_USARTx_UART_Init(). */ +void meshcore_setup(void) +{ + mc_rx_init(&s_rx); + + uint8_t cmd[MC_MAX_PAYLOAD]; + size_t n; + n = mc_cmd_app_start(cmd, sizeof cmd, "stm32"); if (n) send_payload(cmd, n); + n = mc_cmd_device_query(cmd, sizeof cmd, 1); if (n) send_payload(cmd, n); + n = mc_cmd_get_device_time(cmd, sizeof cmd); if (n) send_payload(cmd, n); +} + +/* Call from your main while(1) loop. Non-blocking. */ +void meshcore_poll(void) +{ + uint8_t cmd[MC_MAX_PAYLOAD]; + size_t n; + uint8_t b; + + /* Drain every byte currently available (timeout 0 = return immediately). */ + while (HAL_UART_Receive(MC_UART, &b, 1, 0) == HAL_OK) { + mc_rx_feed(&s_rx, &b, 1); + + uint8_t payload[MC_MAX_PAYLOAD]; + size_t plen; + while (mc_rx_poll(&s_rx, payload, sizeof payload, &plen)) { + mc_event_t ev; + if (!mc_parse(payload, plen, &ev)) continue; + on_event(&ev); + if (ev.code == MC_PUSH_MSG_WAITING || + ev.code == MC_RESP_CHANNEL_MSG_RECV || + ev.code == MC_RESP_CHANNEL_DATA_RECV || + ev.code == MC_RESP_CONTACT_MSG_RECV) { + n = mc_cmd_sync_next_message(cmd, sizeof cmd); + if (n) send_payload(cmd, n); + } + } + } +}