From 3845a1c0219cc4011afa7671040c855eb7eeb44a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Ma=C5=82ek?= Date: Tue, 27 Jan 2026 16:29:31 +0100 Subject: [PATCH 01/23] Fix incorrect INA260 address in debug message --- src/helpers/sensors/EnvironmentSensorManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/sensors/EnvironmentSensorManager.cpp b/src/helpers/sensors/EnvironmentSensorManager.cpp index 8471d80d..a75d378c 100644 --- a/src/helpers/sensors/EnvironmentSensorManager.cpp +++ b/src/helpers/sensors/EnvironmentSensorManager.cpp @@ -284,7 +284,7 @@ bool EnvironmentSensorManager::begin() { INA260_initialized = true; } else { INA260_initialized = false; - MESH_DEBUG_PRINTLN("INA260 was not found at I2C address %02X", TELEM_INA219_ADDRESS); + MESH_DEBUG_PRINTLN("INA260 was not found at I2C address %02X", TELEM_INA260_ADDRESS); } #endif From edeafde51c5992ed67259901af013addd91a7f6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Br=C3=A1zio?= Date: Tue, 27 Jan 2026 19:36:12 +0000 Subject: [PATCH 02/23] Fix: Correct validation logic in isValidName function --- src/helpers/CommonCLI.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 42198b49..10ab8669 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -16,7 +16,7 @@ static uint32_t _atoi(const char* sp) { static bool isValidName(const char *n) { while (*n) { - if (*n == '[' || *n == ']' || *n == '/' || *n == '\\' || *n == ':' || *n == ',' || *n == '?' || *n == '*') return false; + if (*n == '[' || *n == ']' || *n == '\\' || *n == ':' || *n == ',' || *n == '?' || *n == '*') return false; n++; } return true; From d5a73b239437ed6a1c9f9512246169e8238b25c8 Mon Sep 17 00:00:00 2001 From: Rastislav Vysoky Date: Wed, 28 Jan 2026 17:18:39 +0100 Subject: [PATCH 03/23] fix: build errors because of changes in NRF52 base class --- variants/rak3401/RAK3401Board.h | 32 ++++++-------------------------- variants/rak3401/variant.h | 18 +++++++++++++----- 2 files changed, 19 insertions(+), 31 deletions(-) diff --git a/variants/rak3401/RAK3401Board.h b/variants/rak3401/RAK3401Board.h index 609393c3..20edf906 100644 --- a/variants/rak3401/RAK3401Board.h +++ b/variants/rak3401/RAK3401Board.h @@ -4,30 +4,6 @@ #include #include -// LoRa radio module pins for RAK13302 -#define P_LORA_SCLK 3 -#define P_LORA_MISO 29 -#define P_LORA_MOSI 30 -#define P_LORA_NSS 26 -#define P_LORA_DIO_1 10 -#define P_LORA_BUSY 9 -#define P_LORA_RESET 4 -#ifndef P_LORA_PA_EN - #define P_LORA_PA_EN 31 -#endif - -//#define PIN_GPS_SDA 13 //GPS SDA pin (output option) -//#define PIN_GPS_SCL 14 //GPS SCL pin (output option) -// #define PIN_GPS_TX 16 //GPS TX pin -// #define PIN_GPS_RX 15 //GPS RX pin -#define PIN_GPS_1PPS 17 //GPS PPS pin -#define GPS_BAUD_RATE 9600 -#define GPS_ADDRESS 0x42 //i2c address for GPS - -#define SX126X_DIO2_AS_RF_SWITCH -#define SX126X_DIO3_TCXO_VOLTAGE 1.8 - - // built-ins #define PIN_VBAT_READ 5 #define ADC_MULTIPLIER (3 * 1.73 * 1.187 * 1000) @@ -35,9 +11,13 @@ #define PIN_3V3_EN (34) #define WB_IO2 PIN_3V3_EN -class RAK3401Board : public NRF52BoardDCDC, public NRF52BoardOTA { +class RAK3401Board : public NRF52BoardDCDC { +protected: +#ifdef NRF52_POWER_MANAGEMENT + void initiateShutdown(uint8_t reason) override; +#endif public: - RAK3401Board() : NRF52BoardOTA("RAK3401_OTA") {} + RAK3401Board() : NRF52Board("RAK3401_OTA") {} void begin(); #define BATTERY_SAMPLES 8 diff --git a/variants/rak3401/variant.h b/variants/rak3401/variant.h index 9c182247..56fe0816 100644 --- a/variants/rak3401/variant.h +++ b/variants/rak3401/variant.h @@ -141,11 +141,6 @@ static const uint8_t AREF = PIN_AREF; #define EXTERNAL_FLASH_DEVICES IS25LP080D #define EXTERNAL_FLASH_USE_QSPI -#define P_LORA_SCK PIN_SPI1_SCK -#define P_LORA_MISO PIN_SPI1_MISO -#define P_LORA_MOSI PIN_SPI1_MOSI -#define P_LORA_CS 26 - #define USE_SX1262 #define SX126X_CS (26) #define SX126X_DIO1 (10) @@ -157,6 +152,15 @@ static const uint8_t AREF = PIN_AREF; #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#define P_LORA_SCLK PIN_SPI1_SCK +#define P_LORA_MISO PIN_SPI1_MISO +#define P_LORA_MOSI PIN_SPI1_MOSI +#define P_LORA_NSS SX126X_CS +#define P_LORA_DIO_1 SX126X_DIO1 +#define P_LORA_BUSY SX126X_BUSY +#define P_LORA_RESET SX126X_RESET +#define P_LORA_PA_EN 31 + // enables 3.3V periphery like GPS or IO Module // Do not toggle this for GPS power savings #define PIN_3V3_EN (34) @@ -173,6 +177,10 @@ static const uint8_t AREF = PIN_AREF; #define PIN_GPS_RX PIN_SERIAL1_RX #define PIN_GPS_TX PIN_SERIAL1_TX +#define PIN_GPS_1PPS PIN_GPS_PPS +#define GPS_BAUD_RATE 9600 +#define GPS_ADDRESS 0x42 //i2c address for GPS + // Battery // The battery sense is hooked to pin A0 (5) #define BATTERY_PIN PIN_A0 From f41872420e3166ffb8b5676d489934937717a4c4 Mon Sep 17 00:00:00 2001 From: Rastislav Vysoky Date: Wed, 28 Jan 2026 17:28:48 +0100 Subject: [PATCH 04/23] moved pindefs from board file to variant.h --- variants/rak4631/RAK4631Board.h | 21 ----------------- variants/rak4631/variant.h | 41 +++++++++++++++++++++++---------- 2 files changed, 29 insertions(+), 33 deletions(-) diff --git a/variants/rak4631/RAK4631Board.h b/variants/rak4631/RAK4631Board.h index ff4a5b7d..7e67165b 100644 --- a/variants/rak4631/RAK4631Board.h +++ b/variants/rak4631/RAK4631Board.h @@ -4,27 +4,6 @@ #include #include -// LoRa radio module pins for RAK4631 -#define P_LORA_DIO_1 47 -#define P_LORA_NSS 42 -#define P_LORA_RESET RADIOLIB_NC // 38 -#define P_LORA_BUSY 46 -#define P_LORA_SCLK 43 -#define P_LORA_MISO 45 -#define P_LORA_MOSI 44 -#define SX126X_POWER_EN 37 - -//#define PIN_GPS_SDA 13 //GPS SDA pin (output option) -//#define PIN_GPS_SCL 14 //GPS SCL pin (output option) -//#define PIN_GPS_TX 16 //GPS TX pin -//#define PIN_GPS_RX 15 //GPS RX pin -#define PIN_GPS_1PPS 17 //GPS PPS pin -#define GPS_BAUD_RATE 9600 -#define GPS_ADDRESS 0x42 //i2c address for GPS - -#define SX126X_DIO2_AS_RF_SWITCH true -#define SX126X_DIO3_TCXO_VOLTAGE 1.8 - // built-ins #define PIN_VBAT_READ 5 #define ADC_MULTIPLIER (3 * 1.73 * 1.187 * 1000) diff --git a/variants/rak4631/variant.h b/variants/rak4631/variant.h index b18335f8..142d93e9 100644 --- a/variants/rak4631/variant.h +++ b/variants/rak4631/variant.h @@ -144,6 +144,19 @@ extern "C" static const uint8_t MISO = PIN_SPI_MISO; static const uint8_t SCK = PIN_SPI_SCK; +// LoRa radio module pins for RAK4631 +#define P_LORA_DIO_1 (47) +#define P_LORA_NSS (42) +#define P_LORA_RESET (-1) +#define P_LORA_BUSY (46) +#define P_LORA_SCLK (43) +#define P_LORA_MISO (45) +#define P_LORA_MOSI (44) +#define SX126X_POWER_EN (37) + +#define SX126X_DIO2_AS_RF_SWITCH true +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + /* * Wire Interfaces */ @@ -155,19 +168,23 @@ extern "C" #define PIN_WIRE1_SDA (24) #define PIN_WIRE1_SCL (25) - // QSPI Pins - // QSPI occupied by GPIO's - #define PIN_QSPI_SCK 3 // 19 - #define PIN_QSPI_CS 26 // 17 - #define PIN_QSPI_IO0 30 // 20 - #define PIN_QSPI_IO1 29 // 21 - #define PIN_QSPI_IO2 28 // 22 - #define PIN_QSPI_IO3 2 // 23 +// QSPI Pins +// QSPI occupied by GPIO's +#define PIN_QSPI_SCK 3 // 19 +#define PIN_QSPI_CS 26 // 17 +#define PIN_QSPI_IO0 30 // 20 +#define PIN_QSPI_IO1 29 // 21 +#define PIN_QSPI_IO2 28 // 22 +#define PIN_QSPI_IO3 2 // 23 - // On-board QSPI Flash - // No onboard flash - #define EXTERNAL_FLASH_DEVICES IS25LP080D - #define EXTERNAL_FLASH_USE_QSPI +// On-board QSPI Flash +// No onboard flash +#define EXTERNAL_FLASH_DEVICES IS25LP080D +#define EXTERNAL_FLASH_USE_QSPI + +#define PIN_GPS_1PPS 17 //GPS PPS pin +#define GPS_BAUD_RATE 9600 +#define GPS_ADDRESS 0x42 //i2c address for GPS #ifdef __cplusplus } From dd2a9044f3c0a3e608eefc8325a2ea511973ba55 Mon Sep 17 00:00:00 2001 From: Max Litruv Boonzaayer Date: Thu, 29 Jan 2026 08:02:26 +1100 Subject: [PATCH 05/23] Refactor display scaling definitions for HELTEC_VISION_MASTER_T190 --- src/helpers/ui/ST7789Display.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/helpers/ui/ST7789Display.cpp b/src/helpers/ui/ST7789Display.cpp index 7ea35187..f7d20b8a 100644 --- a/src/helpers/ui/ST7789Display.cpp +++ b/src/helpers/ui/ST7789Display.cpp @@ -10,8 +10,13 @@ #define Y_OFFSET 1 // Vertical offset to prevent top row cutoff #endif -#define SCALE_X 1.875f // 240 / 128 -#define SCALE_Y 2.109375f // 135 / 64 +#ifdef HELTEC_VISION_MASTER_T190 + #define SCALE_X 2.5f // 320 / 128 + #define SCALE_Y 2.65625f // 170 / 64 +#else + #define SCALE_X 1.875f // 240 / 128 + #define SCALE_Y 2.109375f // 135 / 64 +#endif bool ST7789Display::begin() { if(!_isOn) { From f7e54ea7971121ab86501962f3e283889fafcf52 Mon Sep 17 00:00:00 2001 From: Steven Linn Date: Wed, 28 Jan 2026 13:24:22 -0700 Subject: [PATCH 06/23] Add LilyGO T-Beam 1W Support --- boards/t_beam_1w.json | 50 ++++++ variants/lilygo_tbeam_1w/TBeam1WBoard.cpp | 71 ++++++++ variants/lilygo_tbeam_1w/TBeam1WBoard.h | 45 ++++++ variants/lilygo_tbeam_1w/pins_arduino.h | 26 +++ variants/lilygo_tbeam_1w/platformio.ini | 189 ++++++++++++++++++++++ variants/lilygo_tbeam_1w/target.cpp | 64 ++++++++ variants/lilygo_tbeam_1w/target.h | 27 ++++ variants/lilygo_tbeam_1w/variant.h | 96 +++++++++++ 8 files changed, 568 insertions(+) create mode 100644 boards/t_beam_1w.json create mode 100644 variants/lilygo_tbeam_1w/TBeam1WBoard.cpp create mode 100644 variants/lilygo_tbeam_1w/TBeam1WBoard.h create mode 100644 variants/lilygo_tbeam_1w/pins_arduino.h create mode 100644 variants/lilygo_tbeam_1w/platformio.ini create mode 100644 variants/lilygo_tbeam_1w/target.cpp create mode 100644 variants/lilygo_tbeam_1w/target.h create mode 100644 variants/lilygo_tbeam_1w/variant.h diff --git a/boards/t_beam_1w.json b/boards/t_beam_1w.json new file mode 100644 index 00000000..2f1159aa --- /dev/null +++ b/boards/t_beam_1w.json @@ -0,0 +1,50 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DLILYGO_TBEAM_1W", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=0", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "psram_type": "opi", + "hwids": [ + [ + "0x303A", + "0x1001" + ] + ], + "mcu": "esp32s3", + "variant": "lilygo_tbeam_1w" + }, + "connectivity": [ + "wifi", + "bluetooth", + "lora" + ], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": [ + "arduino" + ], + "name": "LilyGo TBeam-1W", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "require_upload_port": true, + "speed": 921600 + }, + "url": "http://www.lilygo.cn/", + "vendor": "LilyGo" +} diff --git a/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp b/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp new file mode 100644 index 00000000..1719d733 --- /dev/null +++ b/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp @@ -0,0 +1,71 @@ +#include "TBeam1WBoard.h" + +void TBeam1WBoard::begin() { + ESP32Board::begin(); + + // Power on radio module (must be done before radio init) + pinMode(SX126X_POWER_EN, OUTPUT); + digitalWrite(SX126X_POWER_EN, HIGH); + radio_powered = true; + delay(10); // Allow radio to power up + + // RF switch RXEN pin handled by RadioLib via setRfSwitchPins() + + // Initialize LED + pinMode(LED_PIN, OUTPUT); + digitalWrite(LED_PIN, LOW); + + // Initialize fan control (on by default - 1W PA can overheat) + pinMode(FAN_CTRL_PIN, OUTPUT); + digitalWrite(FAN_CTRL_PIN, HIGH); +} + +void TBeam1WBoard::onBeforeTransmit() { + // RF switching handled by RadioLib via SX126X_DIO2_AS_RF_SWITCH and setRfSwitchPins() + digitalWrite(LED_PIN, HIGH); // TX LED on +} + +void TBeam1WBoard::onAfterTransmit() { + digitalWrite(LED_PIN, LOW); // TX LED off +} + +uint16_t TBeam1WBoard::getBattMilliVolts() { + // T-Beam 1W uses 7.4V battery with voltage divider + // ADC reads through divider - adjust multiplier based on actual divider ratio + analogReadResolution(12); + uint32_t raw = 0; + for (int i = 0; i < 8; i++) { + raw += analogRead(BATTERY_PIN); + } + raw = raw / 8; + // Assuming voltage divider ratio from ADC_MULTIPLIER + // 3.3V reference, 12-bit ADC (4095 max) + return static_cast((raw * 3300 * ADC_MULTIPLIER) / 4095); +} + +const char* TBeam1WBoard::getManufacturerName() const { + return "LilyGo T-Beam 1W"; +} + +void TBeam1WBoard::powerOff() { + // Turn off radio LNA (CTRL pin must be LOW when not receiving) + digitalWrite(SX126X_RXEN, LOW); + + // Turn off radio power + digitalWrite(SX126X_POWER_EN, LOW); + radio_powered = false; + + // Turn off LED and fan + digitalWrite(LED_PIN, LOW); + digitalWrite(FAN_CTRL_PIN, LOW); + + ESP32Board::powerOff(); +} + +void TBeam1WBoard::setFanEnabled(bool enabled) { + digitalWrite(FAN_CTRL_PIN, enabled ? HIGH : LOW); +} + +bool TBeam1WBoard::isFanEnabled() const { + return digitalRead(FAN_CTRL_PIN) == HIGH; +} diff --git a/variants/lilygo_tbeam_1w/TBeam1WBoard.h b/variants/lilygo_tbeam_1w/TBeam1WBoard.h new file mode 100644 index 00000000..d999dfd4 --- /dev/null +++ b/variants/lilygo_tbeam_1w/TBeam1WBoard.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include "variant.h" + +// LilyGo T-Beam 1W with SX1262 + external PA (XY16P35 module) +// +// Power architecture (LDO is separate chip on T-Beam board, not inside XY16P35): +// +// VCC (+4.0~+8.0V) ──┬──────────────────► XY16P35 VCC pin 5 (PA direct) +// (USB or Battery) │ +// │ ┌───────────┐ +// └──►│ LDO Chip │──► +3.3V ──► XY16P35 (SX1262 + LNA) +// │ EN=GPIO40 │ +// └───────────┘ +// LDO_EN (GPIO 40): H @ +1.2V~VIN, active high, not floating +// +// Control signals: +// - LDO_EN (GPIO 40): HIGH enables LDO → powers SX1262 + LNA +// - TCXO_EN (DIO3): HIGH enables TCXO (set to 1.8V per Meshtastic) +// - CTL (GPIO 21): HIGH=RX (LNA on), LOW=TX (LNA off) +// - DIO2: AUTO via SX126X_DIO2_AS_RF_SWITCH (TX path) +// +// Power notes: +// - PA needs VCC 4.0-8.0V for full 32dBm output +// - USB-C (3.9-6V) marginal; 7.4V battery recommended +// - Battery must support 2A+ discharge for high-power TX + +class TBeam1WBoard : public ESP32Board { +private: + bool radio_powered = false; + +public: + void begin(); + void onBeforeTransmit() override; + void onAfterTransmit() override; + uint16_t getBattMilliVolts() override; + const char* getManufacturerName() const override; + void powerOff() override; + + // Fan control methods + void setFanEnabled(bool enabled); + bool isFanEnabled() const; +}; diff --git a/variants/lilygo_tbeam_1w/pins_arduino.h b/variants/lilygo_tbeam_1w/pins_arduino.h new file mode 100644 index 00000000..c6f596f4 --- /dev/null +++ b/variants/lilygo_tbeam_1w/pins_arduino.h @@ -0,0 +1,26 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +// Serial (USB CDC) +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +// I2C for OLED and sensors +static const uint8_t SDA = 8; +static const uint8_t SCL = 9; + +// Default SPI mapped to Radio/SD +static const uint8_t SS = 15; // LoRa CS +static const uint8_t MOSI = 11; +static const uint8_t MISO = 12; +static const uint8_t SCK = 13; + +// SD Card CS +#define SDCARD_CS 10 + +#endif /* Pins_Arduino_h */ diff --git a/variants/lilygo_tbeam_1w/platformio.ini b/variants/lilygo_tbeam_1w/platformio.ini new file mode 100644 index 00000000..4b72b5e7 --- /dev/null +++ b/variants/lilygo_tbeam_1w/platformio.ini @@ -0,0 +1,189 @@ +[LilyGo_TBeam_1W] +extends = esp32_base +board = t_beam_1w +build_flags = + ${esp32_base.build_flags} + -I variants/lilygo_tbeam_1w + -D TBEAM_1W + + ; Radio - SX1262 with high-power PA (32dBm max output) + ; Note: Set SX1262 output to 22dBm max, external PA provides additional gain + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D P_LORA_DIO_1=1 + -D P_LORA_NSS=15 + -D P_LORA_RESET=3 + -D P_LORA_BUSY=38 + -D P_LORA_SCLK=13 + -D P_LORA_MISO=12 + -D P_LORA_MOSI=11 + + ; RF switch configuration: + ; DIO2 controls TX path (PA enable) via SX126X_DIO2_AS_RF_SWITCH + ; GPIO21 controls RX path (LNA enable) via SX126X_RXEN + ; Truth table: DIO2=1,RXEN=0 → TX | DIO2=0,RXEN=1 → RX + -D SX126X_DIO2_AS_RF_SWITCH=true + -D SX126X_RXEN=21 + -D SX126X_DIO3_TCXO_VOLTAGE=1.8 + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_RX_BOOSTED_GAIN=1 + + ; TX power: 22dBm to SX1262, PA module adds ~10dB for 32dBm total + -D LORA_TX_POWER=22 + + ; Display - SH1106 OLED at 0x3C + -D DISPLAY_CLASS=SH1106Display + + ; I2C pins + -D PIN_BOARD_SDA=8 + -D PIN_BOARD_SCL=9 + + ; GPS - L76K module + ; GNSS_TXD (IO5) = GPS transmits → MCU RX + ; GNSS_RXD (IO6) = GPS receives → MCU TX + -D PIN_GPS_TX=5 + -D PIN_GPS_RX=6 + -D PIN_GPS_EN=16 + -D ENV_INCLUDE_GPS=1 + + ; User interface + -D PIN_USER_BTN=17 + +build_src_filter = ${esp32_base.build_src_filter} + +<../variants/lilygo_tbeam_1w> + + + + + + + +lib_deps = + ${esp32_base.lib_deps} + adafruit/Adafruit SH110X @ ~2.1.13 + stevemarple/MicroNMEA @ ~2.0.6 + +; === LILYGO T-Beam 1W Repeater === +[env:LilyGo_TBeam_1W_repeater] +extends = LilyGo_TBeam_1W +build_flags = + ${LilyGo_TBeam_1W.build_flags} + -D ADVERT_NAME='"T-Beam 1W Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D PERSISTANT_GPS=1 + -D ENV_SKIP_GPS_DETECT=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${LilyGo_TBeam_1W.build_src_filter} + +<../examples/simple_repeater> +lib_deps = + ${LilyGo_TBeam_1W.lib_deps} + ${esp32_ota.lib_deps} + +; === LILYGO T-Beam 1W Room Server === +[env:LilyGo_TBeam_1W_room_server] +extends = LilyGo_TBeam_1W +build_flags = + ${LilyGo_TBeam_1W.build_flags} + -D ADVERT_NAME='"T-Beam 1W Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' + -D PERSISTANT_GPS=1 + -D ENV_SKIP_GPS_DETECT=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${LilyGo_TBeam_1W.build_src_filter} + +<../examples/simple_room_server> +lib_deps = + ${LilyGo_TBeam_1W.lib_deps} + ${esp32_ota.lib_deps} + +; === LILYGO T-Beam 1W Companion Radio (USB) === +[env:LilyGo_TBeam_1W_companion_radio_usb] +extends = LilyGo_TBeam_1W +build_flags = + ${LilyGo_TBeam_1W.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D PERSISTANT_GPS=1 + -D ENV_SKIP_GPS_DETECT=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${LilyGo_TBeam_1W.build_src_filter} + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${LilyGo_TBeam_1W.lib_deps} + densaugeo/base64 @ ~1.4.0 + +; === LILYGO T-Beam 1W Companion Radio (BLE) === +[env:LilyGo_TBeam_1W_companion_radio_ble] +extends = LilyGo_TBeam_1W +build_flags = + ${LilyGo_TBeam_1W.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D OFFLINE_QUEUE_SIZE=256 + -D PERSISTANT_GPS=1 + -D ENV_SKIP_GPS_DETECT=1 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${LilyGo_TBeam_1W.build_src_filter} + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${LilyGo_TBeam_1W.lib_deps} + densaugeo/base64 @ ~1.4.0 + +; === LILYGO T-Beam 1W Companion Radio (WiFi) === +[env:LilyGo_TBeam_1W_companion_radio_wifi] +extends = LilyGo_TBeam_1W +build_flags = + ${LilyGo_TBeam_1W.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D WIFI_DEBUG_LOGGING=1 + -D WIFI_SSID='"myssid"' + -D WIFI_PWD='"mypwd"' + -D PERSISTANT_GPS=1 + -D ENV_SKIP_GPS_DETECT=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${LilyGo_TBeam_1W.build_src_filter} + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${LilyGo_TBeam_1W.lib_deps} + densaugeo/base64 @ ~1.4.0 + +; === LILYGO T-Beam 1W Repeater with ESPNow Bridge === +[env:LilyGo_TBeam_1W_repeater_bridge_espnow] +extends = LilyGo_TBeam_1W +build_flags = + ${LilyGo_TBeam_1W.build_flags} + -D ADVERT_NAME='"T-Beam 1W ESPNow Bridge"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_ESPNOW_BRIDGE=1 + -D PERSISTANT_GPS=1 + -D ENV_SKIP_GPS_DETECT=1 +; -D BRIDGE_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${LilyGo_TBeam_1W.build_src_filter} + + + +<../examples/simple_repeater> +lib_deps = + ${LilyGo_TBeam_1W.lib_deps} + ${esp32_ota.lib_deps} diff --git a/variants/lilygo_tbeam_1w/target.cpp b/variants/lilygo_tbeam_1w/target.cpp new file mode 100644 index 00000000..fcdb42ed --- /dev/null +++ b/variants/lilygo_tbeam_1w/target.cpp @@ -0,0 +1,64 @@ +#include +#include "target.h" + +TBeam1WBoard board; + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; + MomentaryButton user_btn(PIN_USER_BTN, 1000, true); +#endif + +static SPIClass spi; + +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi); + +WRAPPER_CLASS radio_driver(radio, board); + +ESP32RTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); + +#if ENV_INCLUDE_GPS + #include + MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1); + EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); +#else + EnvironmentSensorManager sensors; +#endif + +bool radio_init() { + fallback_clock.begin(); + rtc_clock.begin(Wire); + + // Initialize SPI for radio + spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI); + + // GPS serial initialized by EnvironmentSensorManager::begin() + + bool success = radio.std_init(&spi); + if (success) { + // T-Beam 1W has external PA requiring longer ramp time (>800us recommended) + // RADIOLIB_SX126X_PA_RAMP_800U = 0x05 + radio.setTxParams(LORA_TX_POWER, RADIOLIB_SX126X_PA_RAMP_800U); + } + return success; +} + +uint32_t radio_get_rng_seed() { + return radio.random(0x7FFFFFFF); +} + +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) { + radio.setFrequency(freq); + radio.setSpreadingFactor(sf); + radio.setBandwidth(bw); + radio.setCodingRate(cr); +} + +void radio_set_tx_power(uint8_t dbm) { + radio.setOutputPower(dbm); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); +} diff --git a/variants/lilygo_tbeam_1w/target.h b/variants/lilygo_tbeam_1w/target.h new file mode 100644 index 00000000..2c3e8970 --- /dev/null +++ b/variants/lilygo_tbeam_1w/target.h @@ -0,0 +1,27 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include "TBeam1WBoard.h" + +#ifdef DISPLAY_CLASS + #include + #include + extern DISPLAY_CLASS display; + extern MomentaryButton user_btn; +#endif + +extern TBeam1WBoard board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern EnvironmentSensorManager sensors; + +bool radio_init(); +uint32_t radio_get_rng_seed(); +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); +void radio_set_tx_power(uint8_t dbm); +mesh::LocalIdentity radio_new_identity(); diff --git a/variants/lilygo_tbeam_1w/variant.h b/variants/lilygo_tbeam_1w/variant.h new file mode 100644 index 00000000..c05b1696 --- /dev/null +++ b/variants/lilygo_tbeam_1w/variant.h @@ -0,0 +1,96 @@ +// LilyGo T-Beam-1W variant.h +// Configuration based on Meshtastic PR #8967 and LilyGO documentation + +#pragma once + +// I2C for OLED display (SH1106 at 0x3C) +#define I2C_SDA 8 +#define I2C_SCL 9 + +// GPS - Quectel L76K +// GNSS_TXD (IO5) = GPS transmits → MCU RX (setPins rxPin) +// GNSS_RXD (IO6) = GPS receives → MCU TX (setPins txPin) +#define PIN_GPS_TX 5 // MCU receives from GPS TX +#define PIN_GPS_RX 6 // MCU transmits to GPS RX +#define PIN_GPS_PPS 7 // GPS PPS output +#define PIN_GPS_EN 16 // GPS wake-up/enable (GPS_EN_PIN in LilyGO code) +#define HAS_GPS 1 +#define GPS_BAUDRATE 9600 + +// Buttons +#define BUTTON_PIN 0 // BUTTON 1 (boot) +#define BUTTON_PIN_ALT 17 // BUTTON 2 + +// SPI (shared by LoRa and SD) +#define SPI_MOSI 11 +#define SPI_SCK 13 +#define SPI_MISO 12 +#define SPI_CS 10 + +// SD Card +#define HAS_SDCARD +#define SDCARD_USE_SPI1 +#define SDCARD_CS SPI_CS + +// LoRa Radio - SX1262 with 1W PA +#define USE_SX1262 + +#define LORA_SCK SPI_SCK +#define LORA_MISO SPI_MISO +#define LORA_MOSI SPI_MOSI +#define LORA_CS 15 +#define LORA_RESET 3 +#define LORA_DIO1 1 +#define LORA_BUSY 38 + +// CRITICAL: Radio power enable - MUST be HIGH before lora.begin()! +// GPIO 40 powers the SX1262 + PA module via LDO +#define SX126X_POWER_EN 40 + +#ifdef USE_SX1262 +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_BUSY +#define SX126X_RESET LORA_RESET + +// RF switching configuration for 1W PA module +// DIO2 controls PA (via SX126X_DIO2_AS_RF_SWITCH) +// CTRL PIN (GPIO 21) controls LNA - must be HIGH during RX +// Truth table: DIO2=1,CTRL=0 -> TX (PA on, LNA off) +// DIO2=0,CTRL=1 -> RX (PA off, LNA on) +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_RXEN 21 // LNA enable - HIGH during RX + +// TCXO voltage - required for radio init +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +#define SX126X_MAX_POWER 22 +#endif + +// LED +#define LED_PIN 18 +#define LED_STATE_ON 1 // HIGH = ON + +// Battery ADC +#define BATTERY_PIN 4 +#define ADC_CHANNEL ADC1_GPIO4_CHANNEL +#define BATTERY_SENSE_SAMPLES 30 +#define ADC_MULTIPLIER 3.0 + +// NTC temperature sensor +#define NTC_PIN 14 + +// Fan control +#define FAN_CTRL_PIN 41 + +// PA Ramp Time - T-Beam 1W requires >800us stabilization (default is 200us) +// Value 0x05 = RADIOLIB_SX126X_PA_RAMP_800U +#define SX126X_PA_RAMP_US 0x05 + +// Display - SH1106 OLED (128x64) +#define USE_SH1106 +#define OLED_WIDTH 128 +#define OLED_HEIGHT 64 + +// 32768 Hz crystal present +#define HAS_32768HZ 1 From 44e7c092c8bef63eede999481ede41f4c728a737 Mon Sep 17 00:00:00 2001 From: Steven Linn Date: Wed, 28 Jan 2026 14:23:36 -0700 Subject: [PATCH 07/23] Add battery min/max voltage parameter support --- examples/companion_radio/ui-new/UITask.cpp | 10 ++++++++-- examples/companion_radio/ui-orig/UITask.cpp | 10 ++++++++-- variants/lilygo_tbeam_1w/platformio.ini | 4 ++++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 8077627f..0690b45a 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -103,8 +103,14 @@ class HomeScreen : public UIScreen { void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts) { // Convert millivolts to percentage - const int minMilliVolts = 3000; // Minimum voltage (e.g., 3.0V) - const int maxMilliVolts = 4200; // Maximum voltage (e.g., 4.2V) +#ifndef BATT_MIN_MILLIVOLTS + #define BATT_MIN_MILLIVOLTS 3000 +#endif +#ifndef BATT_MAX_MILLIVOLTS + #define BATT_MAX_MILLIVOLTS 4200 +#endif + const int minMilliVolts = BATT_MIN_MILLIVOLTS; + const int maxMilliVolts = BATT_MAX_MILLIVOLTS; int batteryPercentage = ((batteryMilliVolts - minMilliVolts) * 100) / (maxMilliVolts - minMilliVolts); if (batteryPercentage < 0) batteryPercentage = 0; // Clamp to 0% if (batteryPercentage > 100) batteryPercentage = 100; // Clamp to 100% diff --git a/examples/companion_radio/ui-orig/UITask.cpp b/examples/companion_radio/ui-orig/UITask.cpp index 39cbf23a..3ad36fb0 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -149,8 +149,14 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i void UITask::renderBatteryIndicator(uint16_t batteryMilliVolts) { // Convert millivolts to percentage - const int minMilliVolts = 3000; // Minimum voltage (e.g., 3.0V) - const int maxMilliVolts = 4200; // Maximum voltage (e.g., 4.2V) +#ifndef BATT_MIN_MILLIVOLTS + #define BATT_MIN_MILLIVOLTS 3000 +#endif +#ifndef BATT_MAX_MILLIVOLTS + #define BATT_MAX_MILLIVOLTS 4200 +#endif + const int minMilliVolts = BATT_MIN_MILLIVOLTS; + const int maxMilliVolts = BATT_MAX_MILLIVOLTS; int batteryPercentage = ((batteryMilliVolts - minMilliVolts) * 100) / (maxMilliVolts - minMilliVolts); if (batteryPercentage < 0) batteryPercentage = 0; // Clamp to 0% if (batteryPercentage > 100) batteryPercentage = 100; // Clamp to 100% diff --git a/variants/lilygo_tbeam_1w/platformio.ini b/variants/lilygo_tbeam_1w/platformio.ini index 4b72b5e7..618a32a8 100644 --- a/variants/lilygo_tbeam_1w/platformio.ini +++ b/variants/lilygo_tbeam_1w/platformio.ini @@ -31,6 +31,10 @@ build_flags = ; TX power: 22dBm to SX1262, PA module adds ~10dB for 32dBm total -D LORA_TX_POWER=22 + ; Battery - 2S 7.4V LiPo (6.0V min, 8.4V max) + -D BATT_MIN_MILLIVOLTS=6000 + -D BATT_MAX_MILLIVOLTS=8400 + ; Display - SH1106 OLED at 0x3C -D DISPLAY_CLASS=SH1106Display From a9a8299e14172769dcc304d6d64fe203acaa01ce Mon Sep 17 00:00:00 2001 From: Steven Linn Date: Wed, 28 Jan 2026 14:24:53 -0700 Subject: [PATCH 08/23] Set LilyGO T-Beam 1W to use TX0 3.0V (within reference +2.85V~+3.15V) --- variants/lilygo_tbeam_1w/platformio.ini | 2 +- variants/lilygo_tbeam_1w/variant.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/variants/lilygo_tbeam_1w/platformio.ini b/variants/lilygo_tbeam_1w/platformio.ini index 618a32a8..cf17ae8b 100644 --- a/variants/lilygo_tbeam_1w/platformio.ini +++ b/variants/lilygo_tbeam_1w/platformio.ini @@ -24,7 +24,7 @@ build_flags = ; Truth table: DIO2=1,RXEN=0 → TX | DIO2=0,RXEN=1 → RX -D SX126X_DIO2_AS_RF_SWITCH=true -D SX126X_RXEN=21 - -D SX126X_DIO3_TCXO_VOLTAGE=1.8 + -D SX126X_DIO3_TCXO_VOLTAGE=3.0 -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 diff --git a/variants/lilygo_tbeam_1w/variant.h b/variants/lilygo_tbeam_1w/variant.h index c05b1696..f6807e56 100644 --- a/variants/lilygo_tbeam_1w/variant.h +++ b/variants/lilygo_tbeam_1w/variant.h @@ -62,7 +62,7 @@ #define SX126X_RXEN 21 // LNA enable - HIGH during RX // TCXO voltage - required for radio init -#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#define SX126X_DIO3_TCXO_VOLTAGE 3.0 #define SX126X_MAX_POWER 22 #endif From 465776d66758e9608db1402b747e841d37b205b7 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Thu, 29 Jan 2026 21:12:31 +1100 Subject: [PATCH 09/23] * ver 1.12.0 --- examples/companion_radio/MyMesh.h | 4 ++-- examples/simple_repeater/MyMesh.h | 4 ++-- examples/simple_room_server/MyMesh.h | 4 ++-- examples/simple_sensor/SensorMesh.h | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index a2b0033f..95265a19 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -8,11 +8,11 @@ #define FIRMWARE_VER_CODE 8 #ifndef FIRMWARE_BUILD_DATE -#define FIRMWARE_BUILD_DATE "30 Nov 2025" +#define FIRMWARE_BUILD_DATE "29 Jan 2026" #endif #ifndef FIRMWARE_VERSION -#define FIRMWARE_VERSION "v1.11.0" +#define FIRMWARE_VERSION "v1.12.0" #endif #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 60d22902..0d5cd28a 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -69,11 +69,11 @@ struct NeighbourInfo { }; #ifndef FIRMWARE_BUILD_DATE - #define FIRMWARE_BUILD_DATE "30 Nov 2025" + #define FIRMWARE_BUILD_DATE "29 Jan 2026" #endif #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.11.0" + #define FIRMWARE_VERSION "v1.12.0" #endif #define FIRMWARE_ROLE "repeater" diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index 4f3ed0e4..f470e55e 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -26,11 +26,11 @@ /* ------------------------------ Config -------------------------------- */ #ifndef FIRMWARE_BUILD_DATE - #define FIRMWARE_BUILD_DATE "30 Nov 2025" + #define FIRMWARE_BUILD_DATE "29 Jan 2026" #endif #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.11.0" + #define FIRMWARE_VERSION "v1.12.0" #endif #ifndef LORA_FREQ diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index eb2d90c5..ed352345 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -33,11 +33,11 @@ #define PERM_RECV_ALERTS_HI (1 << 7) // high priority alerts #ifndef FIRMWARE_BUILD_DATE - #define FIRMWARE_BUILD_DATE "30 Nov 2025" + #define FIRMWARE_BUILD_DATE "29 Jan 2026" #endif #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.11.0" + #define FIRMWARE_VERSION "v1.12.0" #endif #define FIRMWARE_ROLE "sensor" From 3a7ccc085d5dab694a0eeeb61b5bf341f56ab958 Mon Sep 17 00:00:00 2001 From: Rastislav Vysoky Date: Thu, 29 Jan 2026 15:32:51 +0100 Subject: [PATCH 10/23] fixed build errors and typos/inconsistencies --- variants/thinknode_m3/ThinknodeM3Board.cpp | 22 +++++++++--- variants/thinknode_m3/ThinknodeM3Board.h | 42 ++++++++++------------ variants/thinknode_m3/target.cpp | 16 ++++----- variants/thinknode_m3/target.h | 4 +-- variants/thinknode_m6/ThinkNodeM6Board.h | 13 ++++--- 5 files changed, 56 insertions(+), 41 deletions(-) diff --git a/variants/thinknode_m3/ThinknodeM3Board.cpp b/variants/thinknode_m3/ThinknodeM3Board.cpp index d7ecd62d..ac513ade 100644 --- a/variants/thinknode_m3/ThinknodeM3Board.cpp +++ b/variants/thinknode_m3/ThinknodeM3Board.cpp @@ -1,14 +1,28 @@ #include -#include "ThinknodeM3Board.h" +#include "ThinkNodeM3Board.h" #include #include -void ThinknodeM3Board::begin() { - Nrf52BoardDCDC::begin(); +void ThinkNodeM3Board::begin() { + NRF52Board::begin(); btn_prev_state = HIGH; Wire.begin(); delay(10); // give sx1262 some time to power up -} \ No newline at end of file +} + +uint16_t ThinkNodeM3Board::getBattMilliVolts() { + int adcvalue = 0; + + analogReference(AR_INTERNAL_2_4); + analogReadResolution(ADC_RESOLUTION); + delay(10); + + // ADC range is 0..2400mV and resolution is 12-bit (0..4095) + adcvalue = analogRead(PIN_VBAT_READ); + // Convert the raw value to compensated mv, taking the resistor- + // divider into account (providing the actual LIPO voltage) + return (uint16_t)((float)adcvalue * ADC_FACTOR); +} diff --git a/variants/thinknode_m3/ThinknodeM3Board.h b/variants/thinknode_m3/ThinknodeM3Board.h index 62694087..1435d31d 100644 --- a/variants/thinknode_m3/ThinknodeM3Board.h +++ b/variants/thinknode_m3/ThinknodeM3Board.h @@ -6,38 +6,26 @@ #define ADC_FACTOR ((1000.0*ADC_MULTIPLIER*AREF_VOLTAGE)/ADC_MAX) -class ThinknodeM3Board : public Nrf52BoardDCDC { +class ThinkNodeM3Board : public NRF52BoardDCDC { protected: +#if NRF52_POWER_MANAGEMENT + void initiateShutdown(uint8_t reason) override; +#endif uint8_t btn_prev_state; public: + ThinkNodeM3Board() : NRF52Board("THINKNODE_M3_OTA") {} void begin(); - - uint16_t getBattMilliVolts() override { - int adcvalue = 0; - - analogReference(AR_INTERNAL_2_4); - analogReadResolution(ADC_RESOLUTION); - delay(10); - - // ADC range is 0..2400mV and resolution is 12-bit (0..4095) - adcvalue = analogRead(PIN_VBAT_READ); - // Convert the raw value to compensated mv, taking the resistor- - // divider into account (providing the actual LIPO voltage) - return (uint16_t)((float)adcvalue * ADC_FACTOR); - } + uint16_t getBattMilliVolts() override; #if defined(P_LORA_TX_LED) -#if !defined(P_LORA_TX_LED_ON) -#define P_LORA_TX_LED_ON HIGH -#endif void onBeforeTransmit() override { - digitalWrite(P_LORA_TX_LED, P_LORA_TX_LED_ON); // turn TX LED on + digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on } void onAfterTransmit() override { - digitalWrite(P_LORA_TX_LED, !P_LORA_TX_LED_ON); // turn TX LED off + digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED off } - #endif +#endif const char* getManufacturerName() const override { return "Elecrow ThinkNode M3"; @@ -54,5 +42,13 @@ public: return 0; } - void powerOff() override { sd_power_system_off(); } -}; \ No newline at end of file + void powerOff() override { + // turn off all leds, sd_power_system_off will not do this for us + #ifdef P_LORA_TX_LED + digitalWrite(P_LORA_TX_LED, LOW); + #endif + + // power off board + sd_power_system_off(); + } +}; diff --git a/variants/thinknode_m3/target.cpp b/variants/thinknode_m3/target.cpp index c6708e4d..91d186dc 100644 --- a/variants/thinknode_m3/target.cpp +++ b/variants/thinknode_m3/target.cpp @@ -2,7 +2,7 @@ #include "target.h" #include -ThinknodeM3Board board; +ThinkNodeM3Board board; RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); @@ -30,26 +30,26 @@ static const uint32_t rfswitch_dios[Module::RFSWITCH_MAX_PINS] = { RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_NC, - RADIOLIB_NC, + RADIOLIB_NC, RADIOLIB_NC }; static const Module::RfSwitchMode_t rfswitch_table[] = { - // mode DIO5 DIO6 - { LR11x0::MODE_STBY, {LOW , LOW }}, + // mode DIO5 DIO6 + { LR11x0::MODE_STBY, {LOW , LOW }}, { LR11x0::MODE_RX, {HIGH, LOW }}, { LR11x0::MODE_TX, {HIGH, HIGH }}, { LR11x0::MODE_TX_HP, {LOW , HIGH }}, - { LR11x0::MODE_TX_HF, {LOW , LOW }}, + { LR11x0::MODE_TX_HF, {LOW , LOW }}, { LR11x0::MODE_GNSS, {LOW , LOW }}, - { LR11x0::MODE_WIFI, {LOW , LOW }}, + { LR11x0::MODE_WIFI, {LOW , LOW }}, END_OF_MODE_TABLE, }; #endif bool radio_init() { rtc_clock.begin(Wire); - + #ifdef LR11X0_DIO3_TCXO_VOLTAGE float tcxo = LR11X0_DIO3_TCXO_VOLTAGE; #else @@ -64,7 +64,7 @@ bool radio_init() { Serial.println(status); return false; // fail } - + radio.setCRC(2); radio.explicitHeader(); diff --git a/variants/thinknode_m3/target.h b/variants/thinknode_m3/target.h index f60a85b0..23e99581 100644 --- a/variants/thinknode_m3/target.h +++ b/variants/thinknode_m3/target.h @@ -3,7 +3,7 @@ #define RADIOLIB_STATIC_ONLY 1 #include #include -#include "ThinknodeM3Board.h" +#include "ThinkNodeM3Board.h" #include #include #include @@ -17,7 +17,7 @@ extern NullDisplayDriver display; #endif -extern ThinknodeM3Board board; +extern ThinkNodeM3Board board; extern WRAPPER_CLASS radio_driver; extern AutoDiscoverRTCClock rtc_clock; extern EnvironmentSensorManager sensors; diff --git a/variants/thinknode_m6/ThinkNodeM6Board.h b/variants/thinknode_m6/ThinkNodeM6Board.h index c03e1fbc..32baa2a0 100644 --- a/variants/thinknode_m6/ThinkNodeM6Board.h +++ b/variants/thinknode_m6/ThinkNodeM6Board.h @@ -12,9 +12,14 @@ #define PIN_VBAT_READ BATTERY_PIN #define REAL_VBAT_MV_PER_LSB (VBAT_DIVIDER_COMP * VBAT_MV_PER_LSB) -class ThinkNodeM6Board : public Nrf52BoardOTA { +class ThinkNodeM6Board : public NRF52BoardDCDC { +protected: +#if NRF52_POWER_MANAGEMENT + void initiateShutdown(uint8_t reason) override; +#endif + public: - ThinkNodeM6Board() : NRF52BoardOTA("THINKNODE_M1_OTA") {} + ThinkNodeM6Board() : NRF52Board("THINKNODE_M6_OTA") {} void begin(); uint16_t getBattMilliVolts() override; @@ -25,10 +30,10 @@ public: void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED off } - #endif +#endif const char* getManufacturerName() const override { - return "Elecrow ThinkNode-M6"; + return "Elecrow ThinkNode M6"; } void powerOff() override { From 2a321b53ebfeea2d0b2a9160f944564633b8f9aa Mon Sep 17 00:00:00 2001 From: Rastislav Vysoky Date: Thu, 29 Jan 2026 16:00:19 +0100 Subject: [PATCH 11/23] renamed board files --- .../thinknode_m3/{ThinknodeM3Board.cpp => ThinkNodeM3Board.cpp} | 0 variants/thinknode_m3/{ThinknodeM3Board.h => ThinkNodeM3Board.h} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename variants/thinknode_m3/{ThinknodeM3Board.cpp => ThinkNodeM3Board.cpp} (100%) rename variants/thinknode_m3/{ThinknodeM3Board.h => ThinkNodeM3Board.h} (100%) diff --git a/variants/thinknode_m3/ThinknodeM3Board.cpp b/variants/thinknode_m3/ThinkNodeM3Board.cpp similarity index 100% rename from variants/thinknode_m3/ThinknodeM3Board.cpp rename to variants/thinknode_m3/ThinkNodeM3Board.cpp diff --git a/variants/thinknode_m3/ThinknodeM3Board.h b/variants/thinknode_m3/ThinkNodeM3Board.h similarity index 100% rename from variants/thinknode_m3/ThinknodeM3Board.h rename to variants/thinknode_m3/ThinkNodeM3Board.h From c345f1da8e201bcac4aec02a56529192d15000ec Mon Sep 17 00:00:00 2001 From: Rastislav Vysoky Date: Fri, 30 Jan 2026 00:12:04 +0100 Subject: [PATCH 12/23] Revert "Remove _serial->isConnected() logic from buzzer notifications" --- examples/companion_radio/MyMesh.cpp | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 2dad7866..e0537707 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -330,10 +330,11 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE); _serial->writeFrame(out_frame, 1 + PUB_KEY_SIZE); } - } + } else { #ifdef DISPLAY_CLASS - if (_ui && !_prefs.buzzer_quiet) _ui->notify(UIEventType::newContactMessage); //buzz if enabled + if (_ui) _ui->notify(UIEventType::newContactMessage); #endif + } // add inbound-path to mem cache if (path && path_len <= sizeof(AdvertPath::path)) { // check path is valid @@ -440,7 +441,9 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe bool should_display = txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN; if (should_display && _ui) { _ui->newMsg(path_len, from.name, text, offline_queue_len); - if (!_prefs.buzzer_quiet) _ui->notify(UIEventType::contactMessage); //buzz if enabled + if (!_serial->isConnected()) { + _ui->notify(UIEventType::contactMessage); + } } #endif } @@ -525,8 +528,11 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe uint8_t frame[1]; frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle' _serial->writeFrame(frame, 1); + } else { +#ifdef DISPLAY_CLASS + if (_ui) _ui->notify(UIEventType::channelMessage); +#endif } - #ifdef DISPLAY_CLASS // Get the channel name from the channel index const char *channel_name = "Unknown"; @@ -534,10 +540,7 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe if (getChannel(channel_idx, channel_details)) { channel_name = channel_details.name; } - if (_ui) { - _ui->newMsg(path_len, channel_name, text, offline_queue_len); - if (!_prefs.buzzer_quiet) _ui->notify(UIEventType::channelMessage); //buzz if enabled - } + if (_ui) _ui->newMsg(path_len, channel_name, text, offline_queue_len); #endif } @@ -796,7 +799,6 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _prefs.bw = LORA_BW; _prefs.cr = LORA_CR; _prefs.tx_power_dbm = LORA_TX_POWER; - _prefs.buzzer_quiet = 0; _prefs.gps_enabled = 0; // GPS disabled by default _prefs.gps_interval = 0; // No automatic GPS updates by default //_prefs.rx_delay_base = 10.0f; enable once new algo fixed @@ -836,7 +838,6 @@ void MyMesh::begin(bool has_display) { _prefs.sf = constrain(_prefs.sf, 5, 12); _prefs.cr = constrain(_prefs.cr, 5, 8); _prefs.tx_power_dbm = constrain(_prefs.tx_power_dbm, 1, MAX_LORA_TX_POWER); - _prefs.buzzer_quiet = constrain(_prefs.buzzer_quiet, 0, 1); // Ensure boolean 0 or 1 _prefs.gps_enabled = constrain(_prefs.gps_enabled, 0, 1); // Ensure boolean 0 or 1 _prefs.gps_interval = constrain(_prefs.gps_interval, 0, 86400); // Max 24 hours From 019bbf74d336995653aa97cb15d2d5a7b3d612e9 Mon Sep 17 00:00:00 2001 From: agessaman Date: Thu, 29 Jan 2026 20:44:11 -0800 Subject: [PATCH 13/23] Add recv_errors to CMD_GET_STATS STATS_TYPE_PACKETS response Append uint32_t recv_errors (RadioLib receive/CRC errors) to packet stats binary frame. Frame size 26 -> 30 bytes. Update stats_binary_frames.md and Python/TypeScript parsing examples for backward compatibility (accept >=26). --- docs/stats_binary_frames.md | 26 +++++++++++++++++++++----- examples/companion_radio/MyMesh.cpp | 2 ++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/docs/stats_binary_frames.md b/docs/stats_binary_frames.md index 1b409912..f3b17da9 100644 --- a/docs/stats_binary_frames.md +++ b/docs/stats_binary_frames.md @@ -94,7 +94,7 @@ struct StatsRadio { ## RESP_CODE_STATS + STATS_TYPE_PACKETS (24, 2) -**Total Frame Size:** 26 bytes +**Total Frame Size:** 26 bytes (legacy) or 30 bytes (includes `recv_errors`) | Offset | Size | Type | Field Name | Description | Range/Notes | |--------|------|------|------------|-------------|-------------| @@ -106,12 +106,14 @@ struct StatsRadio { | 14 | 4 | uint32_t | direct_tx | Packets sent via direct routing | 0 - 4,294,967,295 | | 18 | 4 | uint32_t | flood_rx | Packets received via flood routing | 0 - 4,294,967,295 | | 22 | 4 | uint32_t | direct_rx | Packets received via direct routing | 0 - 4,294,967,295 | +| 26 | 4 | uint32_t | recv_errors | Receive/CRC errors (RadioLib); present only in 30-byte frame | 0 - 4,294,967,295 | ### Notes - Counters are cumulative from boot and may wrap. - `recv = flood_rx + direct_rx` - `sent = flood_tx + direct_tx` +- Clients should accept frame length ≥ 26; if length ≥ 30, parse `recv_errors` at offset 26. ### Example Structure (C/C++) @@ -125,6 +127,7 @@ struct StatsPackets { uint32_t direct_tx; uint32_t flood_rx; uint32_t direct_rx; + uint32_t recv_errors; // present when frame size is 30 } __attribute__((packed)); ``` @@ -183,11 +186,12 @@ def parse_stats_radio(frame): } def parse_stats_packets(frame): - """Parse RESP_CODE_STATS + STATS_TYPE_PACKETS frame (26 bytes)""" + """Parse RESP_CODE_STATS + STATS_TYPE_PACKETS frame (26 or 30 bytes)""" + assert len(frame) >= 26, "STATS_TYPE_PACKETS frame too short" response_code, stats_type, recv, sent, flood_tx, direct_tx, flood_rx, direct_rx = \ - struct.unpack('= 30: + (recv_errors,) = struct.unpack('= 30) { + result.recv_errors = view.getUint32(26, true); + } + return result; } ``` diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 2dad7866..cfe3b77d 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1688,12 +1688,14 @@ void MyMesh::handleCmdFrame(size_t len) { uint32_t n_sent_direct = getNumSentDirect(); uint32_t n_recv_flood = getNumRecvFlood(); uint32_t n_recv_direct = getNumRecvDirect(); + uint32_t n_recv_errors = radio_driver.getPacketsRecvErrors(); memcpy(&out_frame[i], &recv, 4); i += 4; memcpy(&out_frame[i], &sent, 4); i += 4; memcpy(&out_frame[i], &n_sent_flood, 4); i += 4; memcpy(&out_frame[i], &n_sent_direct, 4); i += 4; memcpy(&out_frame[i], &n_recv_flood, 4); i += 4; memcpy(&out_frame[i], &n_recv_direct, 4); i += 4; + memcpy(&out_frame[i], &n_recv_errors, 4); i += 4; _serial->writeFrame(out_frame, i); } else { writeErrFrame(ERR_CODE_ILLEGAL_ARG); // invalid stats sub-type From c786cfe613b37506dc48caac9273171d8a4df5b1 Mon Sep 17 00:00:00 2001 From: ViezeVingertjes Date: Sat, 31 Jan 2026 10:22:32 +0100 Subject: [PATCH 14/23] Add KISS Modem firmware --- README.md | 2 + docs/kiss_modem_protocol.md | 110 +++++++++ examples/kiss_modem/KissModem.cpp | 362 ++++++++++++++++++++++++++++++ examples/kiss_modem/KissModem.h | 124 ++++++++++ examples/kiss_modem/main.cpp | 108 +++++++++ 5 files changed, 706 insertions(+) create mode 100644 docs/kiss_modem_protocol.md create mode 100644 examples/kiss_modem/KissModem.cpp create mode 100644 examples/kiss_modem/KissModem.h create mode 100644 examples/kiss_modem/main.cpp diff --git a/README.md b/README.md index d3bcbbef..9d47bffe 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,11 @@ For developers; - Clone and open the MeshCore repository in Visual Studio Code. - See the example applications you can modify and run: - [Companion Radio](./examples/companion_radio) - For use with an external chat app, over BLE, USB or WiFi. + - [KISS Modem](./examples/kiss_modem) - Serial KISS protocol bridge for host applications. ([protocol docs](./docs/kiss_modem_protocol.md)) - [Simple Repeater](./examples/simple_repeater) - Extends network coverage by relaying messages. - [Simple Room Server](./examples/simple_room_server) - A simple BBS server for shared Posts. - [Simple Secure Chat](./examples/simple_secure_chat) - Secure terminal based text communication between devices. + - [Simple Sensor](./examples/simple_sensor) - Remote sensor node with telemetry and alerting. The Simple Secure Chat example can be interacted with through the Serial Monitor in Visual Studio Code, or with a Serial USB Terminal on Android. diff --git a/docs/kiss_modem_protocol.md b/docs/kiss_modem_protocol.md new file mode 100644 index 00000000..f85bfe6c --- /dev/null +++ b/docs/kiss_modem_protocol.md @@ -0,0 +1,110 @@ +# MeshCore KISS Modem Protocol + +Serial protocol for the KISS modem firmware. Enables sending/receiving MeshCore packets over LoRa and cryptographic operations using the modem's identity. + +## Serial Configuration + +115200 baud, 8N1, no flow control. + +## Frame Format + +Standard KISS framing with byte stuffing. + +| Byte | Name | Description | +|------|------|-------------| +| `0xC0` | FEND | Frame delimiter | +| `0xDB` | FESC | Escape character | +| `0xDC` | TFEND | Escaped FEND (FESC + TFEND = 0xC0) | +| `0xDD` | TFESC | Escaped FESC (FESC + TFESC = 0xDB) | + +``` +┌──────┬─────────┬──────────────┬──────┐ +│ FEND │ Command │ Data (escaped)│ FEND │ +│ 0xC0 │ 1 byte │ 0-510 bytes │ 0xC0 │ +└──────┴─────────┴──────────────┴──────┘ +``` + +Maximum unescaped frame size: 512 bytes. + +## Commands + +### Request Commands (Host → Modem) + +| Command | Value | Data | +|---------|-------|------| +| `CMD_DATA` | `0x00` | Packet (2-255 bytes) | +| `CMD_GET_IDENTITY` | `0x01` | - | +| `CMD_GET_RANDOM` | `0x02` | Length (1 byte, 1-64) | +| `CMD_VERIFY_SIGNATURE` | `0x03` | PubKey (32) + Signature (64) + Data | +| `CMD_SIGN_DATA` | `0x04` | Data to sign | +| `CMD_ENCRYPT_DATA` | `0x05` | Key (32) + Plaintext | +| `CMD_DECRYPT_DATA` | `0x06` | Key (32) + MAC (2) + Ciphertext | +| `CMD_KEY_EXCHANGE` | `0x07` | Remote PubKey (32) | +| `CMD_HASH` | `0x08` | Data to hash | +| `CMD_SET_RADIO` | `0x09` | Freq (4) + BW (4) + SF (1) + CR (1) | +| `CMD_SET_TX_POWER` | `0x0A` | Power dBm (1) | +| `CMD_SET_SYNC_WORD` | `0x0B` | Sync word (1) | +| `CMD_GET_RADIO` | `0x0C` | - | +| `CMD_GET_TX_POWER` | `0x0D` | - | +| `CMD_GET_SYNC_WORD` | `0x0E` | - | +| `CMD_GET_VERSION` | `0x0F` | - | + +### Response Commands (Modem → Host) + +| Command | Value | Data | +|---------|-------|------| +| `CMD_DATA` | `0x00` | SNR (1) + RSSI (1) + Packet | +| `RESP_IDENTITY` | `0x11` | PubKey (32) | +| `RESP_RANDOM` | `0x12` | Random bytes (1-64) | +| `RESP_VERIFY` | `0x13` | Result (1): 0x00=invalid, 0x01=valid | +| `RESP_SIGNATURE` | `0x14` | Signature (64) | +| `RESP_ENCRYPTED` | `0x15` | MAC (2) + Ciphertext | +| `RESP_DECRYPTED` | `0x16` | Plaintext | +| `RESP_SHARED_SECRET` | `0x17` | Shared secret (32) | +| `RESP_HASH` | `0x18` | SHA-256 hash (32) | +| `RESP_OK` | `0x19` | - | +| `RESP_RADIO` | `0x1A` | Freq (4) + BW (4) + SF (1) + CR (1) | +| `RESP_TX_POWER` | `0x1B` | Power dBm (1) | +| `RESP_SYNC_WORD` | `0x1C` | Sync word (1) | +| `RESP_VERSION` | `0x1D` | Version (1) + Reserved (1) | +| `RESP_ERROR` | `0x1E` | Error code (1) | +| `RESP_TX_DONE` | `0x1F` | Result (1): 0x00=failed, 0x01=success | + +## Error Codes + +| Code | Value | Description | +|------|-------|-------------| +| `ERR_INVALID_LENGTH` | `0x01` | Request data too short | +| `ERR_INVALID_PARAM` | `0x02` | Invalid parameter value | +| `ERR_NO_CALLBACK` | `0x03` | Radio callback not set | +| `ERR_MAC_FAILED` | `0x04` | MAC verification failed | +| `ERR_UNKNOWN_CMD` | `0x05` | Unknown command | +| `ERR_ENCRYPT_FAILED` | `0x06` | Encryption failed | + +## Data Formats + +### Radio Parameters (CMD_SET_RADIO / RESP_RADIO) + +All values little-endian. + +| Field | Size | Description | +|-------|------|-------------| +| Frequency | 4 bytes | Hz (e.g., 869618000) | +| Bandwidth | 4 bytes | Hz (e.g., 62500) | +| SF | 1 byte | Spreading factor (5-12) | +| CR | 1 byte | Coding rate (5-8) | + +### Received Packet (CMD_DATA response) + +| Field | Size | Description | +|-------|------|-------------| +| SNR | 1 byte | Signal-to-noise × 4, signed | +| RSSI | 1 byte | Signal strength dBm, signed | +| Packet | variable | Raw MeshCore packet | + +## Notes + +- Modem generates identity on first boot (stored in flash) +- SNR values multiplied by 4 for 0.25 dB precision +- Wait for `RESP_TX_DONE` before sending next packet +- See [packet_structure.md](./packet_structure.md) for packet format diff --git a/examples/kiss_modem/KissModem.cpp b/examples/kiss_modem/KissModem.cpp new file mode 100644 index 00000000..4e227d7f --- /dev/null +++ b/examples/kiss_modem/KissModem.cpp @@ -0,0 +1,362 @@ +#include "KissModem.h" + +KissModem::KissModem(Stream& serial, mesh::LocalIdentity& identity, mesh::RNG& rng) + : _serial(serial), _identity(identity), _rng(rng) { + _rx_len = 0; + _rx_escaped = false; + _rx_active = false; + _has_pending_tx = false; + _pending_tx_len = 0; + _setRadioCallback = nullptr; + _setTxPowerCallback = nullptr; + _setSyncWordCallback = nullptr; + _config = {0, 0, 0, 0, 0, 0x12}; +} + +void KissModem::begin() { + _rx_len = 0; + _rx_escaped = false; + _rx_active = false; + _has_pending_tx = false; +} + +void KissModem::writeByte(uint8_t b) { + if (b == KISS_FEND) { + _serial.write(KISS_FESC); + _serial.write(KISS_TFEND); + } else if (b == KISS_FESC) { + _serial.write(KISS_FESC); + _serial.write(KISS_TFESC); + } else { + _serial.write(b); + } +} + +void KissModem::writeFrame(uint8_t cmd, const uint8_t* data, uint16_t len) { + _serial.write(KISS_FEND); + writeByte(cmd); + for (uint16_t i = 0; i < len; i++) { + writeByte(data[i]); + } + _serial.write(KISS_FEND); +} + +void KissModem::writeErrorFrame(uint8_t error_code) { + writeFrame(RESP_ERROR, &error_code, 1); +} + +void KissModem::loop() { + while (_serial.available()) { + uint8_t b = _serial.read(); + + if (b == KISS_FEND) { + if (_rx_active && _rx_len > 0) { + processFrame(); + } + _rx_len = 0; + _rx_escaped = false; + _rx_active = true; + continue; + } + + if (!_rx_active) continue; + + if (b == KISS_FESC) { + _rx_escaped = true; + continue; + } + + if (_rx_escaped) { + _rx_escaped = false; + if (b == KISS_TFEND) b = KISS_FEND; + else if (b == KISS_TFESC) b = KISS_FESC; + } + + if (_rx_len < KISS_MAX_FRAME_SIZE) { + _rx_buf[_rx_len++] = b; + } + } +} + +void KissModem::processFrame() { + if (_rx_len < 1) return; + + uint8_t cmd = _rx_buf[0]; + const uint8_t* data = &_rx_buf[1]; + uint16_t data_len = _rx_len - 1; + + switch (cmd) { + case CMD_DATA: + if (data_len < 2) { + writeErrorFrame(ERR_INVALID_LENGTH); + } else if (data_len > KISS_MAX_PACKET_SIZE) { + writeErrorFrame(ERR_INVALID_LENGTH); + } else { + memcpy(_pending_tx, data, data_len); + _pending_tx_len = data_len; + _has_pending_tx = true; + } + break; + case CMD_GET_IDENTITY: + handleGetIdentity(); + break; + case CMD_GET_RANDOM: + handleGetRandom(data, data_len); + break; + case CMD_VERIFY_SIGNATURE: + handleVerifySignature(data, data_len); + break; + case CMD_SIGN_DATA: + handleSignData(data, data_len); + break; + case CMD_ENCRYPT_DATA: + handleEncryptData(data, data_len); + break; + case CMD_DECRYPT_DATA: + handleDecryptData(data, data_len); + break; + case CMD_KEY_EXCHANGE: + handleKeyExchange(data, data_len); + break; + case CMD_HASH: + handleHash(data, data_len); + break; + case CMD_SET_RADIO: + handleSetRadio(data, data_len); + break; + case CMD_SET_TX_POWER: + handleSetTxPower(data, data_len); + break; + case CMD_SET_SYNC_WORD: + handleSetSyncWord(data, data_len); + break; + case CMD_GET_RADIO: + handleGetRadio(); + break; + case CMD_GET_TX_POWER: + handleGetTxPower(); + break; + case CMD_GET_SYNC_WORD: + handleGetSyncWord(); + break; + case CMD_GET_VERSION: + handleGetVersion(); + break; + default: + writeErrorFrame(ERR_UNKNOWN_CMD); + break; + } +} + +void KissModem::handleGetIdentity() { + writeFrame(RESP_IDENTITY, _identity.pub_key, PUB_KEY_SIZE); +} + +void KissModem::handleGetRandom(const uint8_t* data, uint16_t len) { + if (len < 1) { + writeErrorFrame(ERR_INVALID_LENGTH); + return; + } + + uint8_t requested = data[0]; + if (requested < 1 || requested > 64) { + writeErrorFrame(ERR_INVALID_PARAM); + return; + } + + uint8_t buf[64]; + _rng.random(buf, requested); + writeFrame(RESP_RANDOM, buf, requested); +} + +void KissModem::handleVerifySignature(const uint8_t* data, uint16_t len) { + if (len < PUB_KEY_SIZE + SIGNATURE_SIZE + 1) { + writeErrorFrame(ERR_INVALID_LENGTH); + return; + } + + mesh::Identity signer(data); + const uint8_t* signature = data + PUB_KEY_SIZE; + const uint8_t* msg = data + PUB_KEY_SIZE + SIGNATURE_SIZE; + uint16_t msg_len = len - PUB_KEY_SIZE - SIGNATURE_SIZE; + + uint8_t result = signer.verify(signature, msg, msg_len) ? 0x01 : 0x00; + writeFrame(RESP_VERIFY, &result, 1); +} + +void KissModem::handleSignData(const uint8_t* data, uint16_t len) { + if (len < 1) { + writeErrorFrame(ERR_INVALID_LENGTH); + return; + } + + uint8_t signature[SIGNATURE_SIZE]; + _identity.sign(signature, data, len); + writeFrame(RESP_SIGNATURE, signature, SIGNATURE_SIZE); +} + +void KissModem::handleEncryptData(const uint8_t* data, uint16_t len) { + if (len < PUB_KEY_SIZE + 1) { + writeErrorFrame(ERR_INVALID_LENGTH); + return; + } + + const uint8_t* key = data; + const uint8_t* plaintext = data + PUB_KEY_SIZE; + uint16_t plaintext_len = len - PUB_KEY_SIZE; + + uint8_t buf[KISS_MAX_FRAME_SIZE]; + int encrypted_len = mesh::Utils::encryptThenMAC(key, buf, plaintext, plaintext_len); + + if (encrypted_len > 0) { + writeFrame(RESP_ENCRYPTED, buf, encrypted_len); + } else { + writeErrorFrame(ERR_ENCRYPT_FAILED); + } +} + +void KissModem::handleDecryptData(const uint8_t* data, uint16_t len) { + if (len < PUB_KEY_SIZE + CIPHER_MAC_SIZE + 1) { + writeErrorFrame(ERR_INVALID_LENGTH); + return; + } + + const uint8_t* key = data; + const uint8_t* ciphertext = data + PUB_KEY_SIZE; + uint16_t ciphertext_len = len - PUB_KEY_SIZE; + + uint8_t buf[KISS_MAX_FRAME_SIZE]; + int decrypted_len = mesh::Utils::MACThenDecrypt(key, buf, ciphertext, ciphertext_len); + + if (decrypted_len > 0) { + writeFrame(RESP_DECRYPTED, buf, decrypted_len); + } else { + writeErrorFrame(ERR_MAC_FAILED); + } +} + +void KissModem::handleKeyExchange(const uint8_t* data, uint16_t len) { + if (len < PUB_KEY_SIZE) { + writeErrorFrame(ERR_INVALID_LENGTH); + return; + } + + uint8_t shared_secret[PUB_KEY_SIZE]; + _identity.calcSharedSecret(shared_secret, data); + writeFrame(RESP_SHARED_SECRET, shared_secret, PUB_KEY_SIZE); +} + +void KissModem::handleHash(const uint8_t* data, uint16_t len) { + if (len < 1) { + writeErrorFrame(ERR_INVALID_LENGTH); + return; + } + + uint8_t hash[32]; + mesh::Utils::sha256(hash, 32, data, len); + writeFrame(RESP_HASH, hash, 32); +} + +bool KissModem::getPacketToSend(uint8_t* packet, uint16_t* len) { + if (!_has_pending_tx) return false; + + memcpy(packet, _pending_tx, _pending_tx_len); + *len = _pending_tx_len; + _has_pending_tx = false; + return true; +} + +void KissModem::onPacketReceived(int8_t snr, int8_t rssi, const uint8_t* packet, uint16_t len) { + uint8_t buf[2 + KISS_MAX_PACKET_SIZE]; + buf[0] = (uint8_t)snr; + buf[1] = (uint8_t)rssi; + memcpy(&buf[2], packet, len); + writeFrame(CMD_DATA, buf, 2 + len); +} + +void KissModem::handleSetRadio(const uint8_t* data, uint16_t len) { + if (len < 10) { + writeErrorFrame(ERR_INVALID_LENGTH); + return; + } + if (!_setRadioCallback) { + writeErrorFrame(ERR_NO_CALLBACK); + return; + } + + uint32_t freq_hz, bw_hz; + memcpy(&freq_hz, data, 4); + memcpy(&bw_hz, data + 4, 4); + uint8_t sf = data[8]; + uint8_t cr = data[9]; + + _config.freq_hz = freq_hz; + _config.bw_hz = bw_hz; + _config.sf = sf; + _config.cr = cr; + + float freq = freq_hz / 1000000.0f; + float bw = bw_hz / 1000.0f; + + _setRadioCallback(freq, bw, sf, cr); + writeFrame(RESP_OK, nullptr, 0); +} + +void KissModem::handleSetTxPower(const uint8_t* data, uint16_t len) { + if (len < 1) { + writeErrorFrame(ERR_INVALID_LENGTH); + return; + } + if (!_setTxPowerCallback) { + writeErrorFrame(ERR_NO_CALLBACK); + return; + } + + _config.tx_power = data[0]; + _setTxPowerCallback(data[0]); + writeFrame(RESP_OK, nullptr, 0); +} + +void KissModem::handleSetSyncWord(const uint8_t* data, uint16_t len) { + if (len < 1) { + writeErrorFrame(ERR_INVALID_LENGTH); + return; + } + if (!_setSyncWordCallback) { + writeErrorFrame(ERR_NO_CALLBACK); + return; + } + + _config.sync_word = data[0]; + _setSyncWordCallback(data[0]); + writeFrame(RESP_OK, nullptr, 0); +} + +void KissModem::handleGetRadio() { + uint8_t buf[10]; + memcpy(buf, &_config.freq_hz, 4); + memcpy(buf + 4, &_config.bw_hz, 4); + buf[8] = _config.sf; + buf[9] = _config.cr; + writeFrame(RESP_RADIO, buf, 10); +} + +void KissModem::handleGetTxPower() { + writeFrame(RESP_TX_POWER, &_config.tx_power, 1); +} + +void KissModem::handleGetSyncWord() { + writeFrame(RESP_SYNC_WORD, &_config.sync_word, 1); +} + +void KissModem::handleGetVersion() { + uint8_t buf[2]; + buf[0] = KISS_FIRMWARE_VERSION; + buf[1] = 0; + writeFrame(RESP_VERSION, buf, 2); +} + +void KissModem::onTxComplete(bool success) { + uint8_t result = success ? 0x01 : 0x00; + writeFrame(RESP_TX_DONE, &result, 1); +} diff --git a/examples/kiss_modem/KissModem.h b/examples/kiss_modem/KissModem.h new file mode 100644 index 00000000..34d9577f --- /dev/null +++ b/examples/kiss_modem/KissModem.h @@ -0,0 +1,124 @@ +#pragma once + +#include +#include +#include + +#define KISS_FEND 0xC0 +#define KISS_FESC 0xDB +#define KISS_TFEND 0xDC +#define KISS_TFESC 0xDD + +#define KISS_MAX_FRAME_SIZE 512 +#define KISS_MAX_PACKET_SIZE 255 + +#define CMD_DATA 0x00 +#define CMD_GET_IDENTITY 0x01 +#define CMD_GET_RANDOM 0x02 +#define CMD_VERIFY_SIGNATURE 0x03 +#define CMD_SIGN_DATA 0x04 +#define CMD_ENCRYPT_DATA 0x05 +#define CMD_DECRYPT_DATA 0x06 +#define CMD_KEY_EXCHANGE 0x07 +#define CMD_HASH 0x08 +#define CMD_SET_RADIO 0x09 +#define CMD_SET_TX_POWER 0x0A +#define CMD_SET_SYNC_WORD 0x0B +#define CMD_GET_RADIO 0x0C +#define CMD_GET_TX_POWER 0x0D +#define CMD_GET_SYNC_WORD 0x0E +#define CMD_GET_VERSION 0x0F + +#define RESP_IDENTITY 0x11 +#define RESP_RANDOM 0x12 +#define RESP_VERIFY 0x13 +#define RESP_SIGNATURE 0x14 +#define RESP_ENCRYPTED 0x15 +#define RESP_DECRYPTED 0x16 +#define RESP_SHARED_SECRET 0x17 +#define RESP_HASH 0x18 +#define RESP_OK 0x19 +#define RESP_RADIO 0x1A +#define RESP_TX_POWER 0x1B +#define RESP_SYNC_WORD 0x1C +#define RESP_VERSION 0x1D +#define RESP_ERROR 0x1E +#define RESP_TX_DONE 0x1F + +#define ERR_INVALID_LENGTH 0x01 +#define ERR_INVALID_PARAM 0x02 +#define ERR_NO_CALLBACK 0x03 +#define ERR_MAC_FAILED 0x04 +#define ERR_UNKNOWN_CMD 0x05 +#define ERR_ENCRYPT_FAILED 0x06 + +#define KISS_FIRMWARE_VERSION 1 + +typedef void (*SetRadioCallback)(float freq, float bw, uint8_t sf, uint8_t cr); +typedef void (*SetTxPowerCallback)(uint8_t power); +typedef void (*SetSyncWordCallback)(uint8_t syncWord); + +struct RadioConfig { + uint32_t freq_hz; + uint32_t bw_hz; + uint8_t sf; + uint8_t cr; + uint8_t tx_power; + uint8_t sync_word; +}; + +class KissModem { + Stream& _serial; + mesh::LocalIdentity& _identity; + mesh::RNG& _rng; + + uint8_t _rx_buf[KISS_MAX_FRAME_SIZE]; + uint16_t _rx_len; + bool _rx_escaped; + bool _rx_active; + + uint8_t _pending_tx[KISS_MAX_PACKET_SIZE]; + uint16_t _pending_tx_len; + bool _has_pending_tx; + + SetRadioCallback _setRadioCallback; + SetTxPowerCallback _setTxPowerCallback; + SetSyncWordCallback _setSyncWordCallback; + + RadioConfig _config; + + void writeByte(uint8_t b); + void writeFrame(uint8_t cmd, const uint8_t* data, uint16_t len); + void writeErrorFrame(uint8_t error_code); + void processFrame(); + + void handleGetIdentity(); + void handleGetRandom(const uint8_t* data, uint16_t len); + void handleVerifySignature(const uint8_t* data, uint16_t len); + void handleSignData(const uint8_t* data, uint16_t len); + void handleEncryptData(const uint8_t* data, uint16_t len); + void handleDecryptData(const uint8_t* data, uint16_t len); + void handleKeyExchange(const uint8_t* data, uint16_t len); + void handleHash(const uint8_t* data, uint16_t len); + void handleSetRadio(const uint8_t* data, uint16_t len); + void handleSetTxPower(const uint8_t* data, uint16_t len); + void handleSetSyncWord(const uint8_t* data, uint16_t len); + void handleGetRadio(); + void handleGetTxPower(); + void handleGetSyncWord(); + void handleGetVersion(); + +public: + KissModem(Stream& serial, mesh::LocalIdentity& identity, mesh::RNG& rng); + + void begin(); + void loop(); + + void setRadioCallback(SetRadioCallback cb) { _setRadioCallback = cb; } + void setTxPowerCallback(SetTxPowerCallback cb) { _setTxPowerCallback = cb; } + void setSyncWordCallback(SetSyncWordCallback cb) { _setSyncWordCallback = cb; } + + bool getPacketToSend(uint8_t* packet, uint16_t* len); + void onPacketReceived(int8_t snr, int8_t rssi, const uint8_t* packet, uint16_t len); + void onTxComplete(bool success); +}; diff --git a/examples/kiss_modem/main.cpp b/examples/kiss_modem/main.cpp new file mode 100644 index 00000000..2f843a99 --- /dev/null +++ b/examples/kiss_modem/main.cpp @@ -0,0 +1,108 @@ +#include +#include +#include +#include +#include "KissModem.h" + +#if defined(NRF52_PLATFORM) + #include +#elif defined(RP2040_PLATFORM) + #include +#elif defined(ESP32) + #include +#endif + +StdRNG rng; +mesh::LocalIdentity identity; +KissModem* modem; + +void halt() { + while (1) ; +} + +void loadOrCreateIdentity() { +#if defined(NRF52_PLATFORM) + InternalFS.begin(); + IdentityStore store(InternalFS, ""); +#elif defined(ESP32) + SPIFFS.begin(true); + IdentityStore store(SPIFFS, "/identity"); +#elif defined(RP2040_PLATFORM) + LittleFS.begin(); + IdentityStore store(LittleFS, "/identity"); + store.begin(); +#else + #error "Filesystem not defined" +#endif + + if (!store.load("_main", identity)) { + identity = radio_new_identity(); + while (identity.pub_key[0] == 0x00 || identity.pub_key[0] == 0xFF) { + identity = radio_new_identity(); + } + store.save("_main", identity); + } +} + +void onSetRadio(float freq, float bw, uint8_t sf, uint8_t cr) { + radio_set_params(freq, bw, sf, cr); +} + +void onSetTxPower(uint8_t power) { + radio_set_tx_power(power); +} + +void onSetSyncWord(uint8_t sync_word) { + radio_set_sync_word(sync_word); +} + +void setup() { + board.begin(); + + if (!radio_init()) { + halt(); + } + + radio_driver.begin(); + + rng.begin(radio_get_rng_seed()); + loadOrCreateIdentity(); + + Serial.begin(115200); + uint32_t start = millis(); + while (!Serial && millis() - start < 3000) delay(10); + delay(100); + + modem = new KissModem(Serial, identity, rng); + modem->setRadioCallback(onSetRadio); + modem->setTxPowerCallback(onSetTxPower); + modem->setSyncWordCallback(onSetSyncWord); + modem->begin(); +} + +void loop() { + modem->loop(); + + uint8_t packet[KISS_MAX_PACKET_SIZE]; + uint16_t len; + + if (modem->getPacketToSend(packet, &len)) { + radio_driver.startSendRaw(packet, len); + while (!radio_driver.isSendComplete()) { + delay(1); + } + radio_driver.onSendFinished(); + modem->onTxComplete(true); + } + + uint8_t rx_buf[256]; + int rx_len = radio_driver.recvRaw(rx_buf, sizeof(rx_buf)); + + if (rx_len > 0) { + int8_t snr = (int8_t)(radio_driver.getLastSNR() * 4); + int8_t rssi = (int8_t)radio_driver.getLastRSSI(); + modem->onPacketReceived(snr, rssi, rx_buf, rx_len); + } + + radio_driver.loop(); +} From c5b1d30280c837214f18db2e19bfcae9151568a1 Mon Sep 17 00:00:00 2001 From: taco Date: Sat, 31 Jan 2026 23:48:28 +1100 Subject: [PATCH 15/23] t114: remove extra DCDC enable --- variants/heltec_t114/T114Board.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/variants/heltec_t114/T114Board.cpp b/variants/heltec_t114/T114Board.cpp index 2a36bd90..c03d39af 100644 --- a/variants/heltec_t114/T114Board.cpp +++ b/variants/heltec_t114/T114Board.cpp @@ -34,7 +34,6 @@ void T114Board::initiateShutdown(uint8_t reason) { void T114Board::begin() { NRF52Board::begin(); - NRF_POWER->DCDCEN = 1; pinMode(PIN_VBAT_READ, INPUT); From e6e1b810f874491b1e7cf96869f5076069ddc6fa Mon Sep 17 00:00:00 2001 From: taco Date: Tue, 27 Jan 2026 17:51:30 +1100 Subject: [PATCH 16/23] add DataStore::deleteBlobByKey() --- examples/companion_radio/DataStore.cpp | 16 ++++++++++++++++ examples/companion_radio/DataStore.h | 1 + examples/companion_radio/MyMesh.cpp | 2 ++ 3 files changed, 19 insertions(+) diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index f61f53ae..6cc77671 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -560,6 +560,9 @@ bool DataStore::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src } return false; // error } +bool DataStore::deleteBlobByKey(const uint8_t key[], int key_len) { + return true; // this is just a stub on NRF52/STM32 platforms +} #else uint8_t DataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) { char path[64]; @@ -598,4 +601,17 @@ bool DataStore::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src } return false; // error } + +bool DataStore::deleteBlobByKey(const uint8_t key[], int key_len) { + char path[64]; + char fname[18]; + + if (key_len > 8) key_len = 8; // just use first 8 bytes (prefix) + mesh::Utils::toHex(fname, key, key_len); + sprintf(path, "/bl/%s", fname); + + _fs->remove(path); + + return true; // return true even if file did not exist +} #endif diff --git a/examples/companion_radio/DataStore.h b/examples/companion_radio/DataStore.h index 62580942..58b4d5d2 100644 --- a/examples/companion_radio/DataStore.h +++ b/examples/companion_radio/DataStore.h @@ -42,6 +42,7 @@ public: void migrateToSecondaryFS(); uint8_t getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]); bool putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len); + bool deleteBlobByKey(const uint8_t key[], int key_len); File openRead(const char* filename); File openRead(FILESYSTEM* fs, const char* filename); bool removeFile(const char* filename); diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 1e4115da..9bb747e7 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -307,6 +307,7 @@ bool MyMesh::shouldOverwriteWhenFull() const { } void MyMesh::onContactOverwrite(const uint8_t* pub_key) { + _store->deleteBlobByKey(pub_key, PUB_KEY_SIZE); // delete from storage if (_serial->isConnected()) { out_frame[0] = PUSH_CODE_CONTACT_DELETED; memcpy(&out_frame[1], pub_key, PUB_KEY_SIZE); @@ -1124,6 +1125,7 @@ void MyMesh::handleCmdFrame(size_t len) { uint8_t *pub_key = &cmd_frame[1]; ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); if (recipient && removeContact(*recipient)) { + _store->deleteBlobByKey(pub_key, PUB_KEY_SIZE); dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); writeOKFrame(); } else { From 31ba971c60e7367de3265a0f006edacbc2ebbe4c Mon Sep 17 00:00:00 2001 From: taco Date: Tue, 27 Jan 2026 17:53:05 +1100 Subject: [PATCH 17/23] only store advblob when adding/updating contacts --- src/helpers/BaseChatMesh.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index aebfc1b6..6de7469d 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -131,7 +131,6 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, plen = packet->writeTo(temp_buf); packet->header = save; } - putBlobByKey(id.pub_key, PUB_KEY_SIZE, temp_buf, plen); bool is_new = false; // true = not in contacts[], false = exists in contacts[] if (from == NULL) { @@ -157,6 +156,7 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, from->shared_secret_valid = false; } // update + putBlobByKey(id.pub_key, PUB_KEY_SIZE, temp_buf, plen); StrHelper::strncpy(from->name, parser.getName(), sizeof(from->name)); from->type = parser.getType(); if (parser.hasLatLon()) { From 8d5eaf500d8e744d204339be078f10074213d9e2 Mon Sep 17 00:00:00 2001 From: taco Date: Tue, 27 Jan 2026 19:31:07 +1100 Subject: [PATCH 18/23] add makeBlobPath inline helper for esp32 --- examples/companion_radio/DataStore.cpp | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index 6cc77671..c0f2c021 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -564,13 +564,16 @@ bool DataStore::deleteBlobByKey(const uint8_t key[], int key_len) { return true; // this is just a stub on NRF52/STM32 platforms } #else -uint8_t DataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) { - char path[64]; +inline void makeBlobPath(const uint8_t key[], int key_len, char* path, size_t path_size) { char fname[18]; - if (key_len > 8) key_len = 8; // just use first 8 bytes (prefix) mesh::Utils::toHex(fname, key, key_len); sprintf(path, "/bl/%s", fname); +} + +uint8_t DataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) { + char path[64]; + makeBlobPath(key, key_len, path, sizeof(path)); if (_fs->exists(path)) { File f = openRead(_fs, path); @@ -585,11 +588,7 @@ uint8_t DataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_b bool DataStore::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len) { char path[64]; - char fname[18]; - - if (key_len > 8) key_len = 8; // just use first 8 bytes (prefix) - mesh::Utils::toHex(fname, key, key_len); - sprintf(path, "/bl/%s", fname); + makeBlobPath(key, key_len, path, sizeof(path)); File f = openWrite(_fs, path); if (f) { @@ -604,11 +603,7 @@ bool DataStore::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src bool DataStore::deleteBlobByKey(const uint8_t key[], int key_len) { char path[64]; - char fname[18]; - - if (key_len > 8) key_len = 8; // just use first 8 bytes (prefix) - mesh::Utils::toHex(fname, key, key_len); - sprintf(path, "/bl/%s", fname); + makeBlobPath(key, key_len, path, sizeof(path)); _fs->remove(path); From b5248faec4872a52001ee1ed425492db043ee943 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Br=C3=A1zio?= Date: Sat, 31 Jan 2026 13:45:58 +0000 Subject: [PATCH 19/23] Revert "Merge pull request #1428 from etienn01/update-t114-i2c" This reverts commit 616eb57b163f2123727347ab0425e1ad4fbca564, reversing changes made to 537acd7ea144ee077595c1171cd96770eb924b67. This patch needs to be reverted because it boot freezes t114 433Mhz variant. --- variants/heltec_t114/variant.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/variants/heltec_t114/variant.h b/variants/heltec_t114/variant.h index ac9dbbe6..aa7f4022 100644 --- a/variants/heltec_t114/variant.h +++ b/variants/heltec_t114/variant.h @@ -58,8 +58,8 @@ //////////////////////////////////////////////////////////////////////////////// // I2C pin definition -#define PIN_WIRE_SDA (16) // P0.16 -#define PIN_WIRE_SCL (13) // P0.13 +#define PIN_WIRE_SDA (26) // P0.26 +#define PIN_WIRE_SCL (27) // P0.27 //////////////////////////////////////////////////////////////////////////////// // SPI pin definition From 1bcb52bab318926b014d0a46d98ebc2f35ff5e3f Mon Sep 17 00:00:00 2001 From: ViezeVingertjes Date: Sat, 31 Jan 2026 15:05:25 +0100 Subject: [PATCH 20/23] Add new commands and responses for RSSI, channel status, airtime, noise floor, statistics, battery, and sensors. --- docs/kiss_modem_protocol.md | 74 +++++++++++++---- examples/kiss_modem/KissModem.cpp | 128 ++++++++++++++++++++++++++++++ examples/kiss_modem/KissModem.h | 78 ++++++++++++++---- examples/kiss_modem/main.cpp | 46 +++++++++++ 4 files changed, 294 insertions(+), 32 deletions(-) diff --git a/docs/kiss_modem_protocol.md b/docs/kiss_modem_protocol.md index f85bfe6c..e80c3b29 100644 --- a/docs/kiss_modem_protocol.md +++ b/docs/kiss_modem_protocol.md @@ -48,27 +48,43 @@ Maximum unescaped frame size: 512 bytes. | `CMD_GET_TX_POWER` | `0x0D` | - | | `CMD_GET_SYNC_WORD` | `0x0E` | - | | `CMD_GET_VERSION` | `0x0F` | - | +| `CMD_GET_CURRENT_RSSI` | `0x10` | - | +| `CMD_IS_CHANNEL_BUSY` | `0x11` | - | +| `CMD_GET_AIRTIME` | `0x12` | Packet length (1) | +| `CMD_GET_NOISE_FLOOR` | `0x13` | - | +| `CMD_GET_STATS` | `0x14` | - | +| `CMD_GET_BATTERY` | `0x15` | - | +| `CMD_PING` | `0x16` | - | +| `CMD_GET_SENSORS` | `0x17` | Permissions (1) | ### Response Commands (Modem → Host) | Command | Value | Data | |---------|-------|------| | `CMD_DATA` | `0x00` | SNR (1) + RSSI (1) + Packet | -| `RESP_IDENTITY` | `0x11` | PubKey (32) | -| `RESP_RANDOM` | `0x12` | Random bytes (1-64) | -| `RESP_VERIFY` | `0x13` | Result (1): 0x00=invalid, 0x01=valid | -| `RESP_SIGNATURE` | `0x14` | Signature (64) | -| `RESP_ENCRYPTED` | `0x15` | MAC (2) + Ciphertext | -| `RESP_DECRYPTED` | `0x16` | Plaintext | -| `RESP_SHARED_SECRET` | `0x17` | Shared secret (32) | -| `RESP_HASH` | `0x18` | SHA-256 hash (32) | -| `RESP_OK` | `0x19` | - | -| `RESP_RADIO` | `0x1A` | Freq (4) + BW (4) + SF (1) + CR (1) | -| `RESP_TX_POWER` | `0x1B` | Power dBm (1) | -| `RESP_SYNC_WORD` | `0x1C` | Sync word (1) | -| `RESP_VERSION` | `0x1D` | Version (1) + Reserved (1) | -| `RESP_ERROR` | `0x1E` | Error code (1) | -| `RESP_TX_DONE` | `0x1F` | Result (1): 0x00=failed, 0x01=success | +| `RESP_IDENTITY` | `0x21` | PubKey (32) | +| `RESP_RANDOM` | `0x22` | Random bytes (1-64) | +| `RESP_VERIFY` | `0x23` | Result (1): 0x00=invalid, 0x01=valid | +| `RESP_SIGNATURE` | `0x24` | Signature (64) | +| `RESP_ENCRYPTED` | `0x25` | MAC (2) + Ciphertext | +| `RESP_DECRYPTED` | `0x26` | Plaintext | +| `RESP_SHARED_SECRET` | `0x27` | Shared secret (32) | +| `RESP_HASH` | `0x28` | SHA-256 hash (32) | +| `RESP_OK` | `0x29` | - | +| `RESP_RADIO` | `0x2A` | Freq (4) + BW (4) + SF (1) + CR (1) | +| `RESP_TX_POWER` | `0x2B` | Power dBm (1) | +| `RESP_SYNC_WORD` | `0x2C` | Sync word (1) | +| `RESP_VERSION` | `0x2D` | Version (1) + Reserved (1) | +| `RESP_ERROR` | `0x2E` | Error code (1) | +| `RESP_TX_DONE` | `0x2F` | Result (1): 0x00=failed, 0x01=success | +| `RESP_CURRENT_RSSI` | `0x30` | RSSI dBm (1, signed) | +| `RESP_CHANNEL_BUSY` | `0x31` | Result (1): 0x00=clear, 0x01=busy | +| `RESP_AIRTIME` | `0x32` | Milliseconds (4) | +| `RESP_NOISE_FLOOR` | `0x33` | dBm (2, signed) | +| `RESP_STATS` | `0x34` | RX (4) + TX (4) + Errors (4) | +| `RESP_BATTERY` | `0x35` | Millivolts (2) | +| `RESP_PONG` | `0x36` | - | +| `RESP_SENSORS` | `0x37` | CayenneLPP payload | ## Error Codes @@ -76,10 +92,11 @@ Maximum unescaped frame size: 512 bytes. |------|-------|-------------| | `ERR_INVALID_LENGTH` | `0x01` | Request data too short | | `ERR_INVALID_PARAM` | `0x02` | Invalid parameter value | -| `ERR_NO_CALLBACK` | `0x03` | Radio callback not set | +| `ERR_NO_CALLBACK` | `0x03` | Feature not available | | `ERR_MAC_FAILED` | `0x04` | MAC verification failed | | `ERR_UNKNOWN_CMD` | `0x05` | Unknown command | | `ERR_ENCRYPT_FAILED` | `0x06` | Encryption failed | +| `ERR_TX_PENDING` | `0x07` | TX already pending | ## Data Formats @@ -102,9 +119,34 @@ All values little-endian. | RSSI | 1 byte | Signal strength dBm, signed | | Packet | variable | Raw MeshCore packet | +### Stats (RESP_STATS) + +All values little-endian. + +| Field | Size | Description | +|-------|------|-------------| +| RX | 4 bytes | Packets received | +| TX | 4 bytes | Packets transmitted | +| Errors | 4 bytes | Receive errors | + +### Sensor Permissions (CMD_GET_SENSORS) + +| Bit | Value | Description | +|-----|-------|-------------| +| 0 | `0x01` | Base (battery) | +| 1 | `0x02` | Location (GPS) | +| 2 | `0x04` | Environment (temp, humidity, pressure) | + +Use `0x07` for all permissions. + +### Sensor Data (RESP_SENSORS) + +Data returned in CayenneLPP format. See [CayenneLPP documentation](https://docs.mydevices.com/docs/lorawan/cayenne-lpp) for parsing. + ## Notes - Modem generates identity on first boot (stored in flash) - SNR values multiplied by 4 for 0.25 dB precision - Wait for `RESP_TX_DONE` before sending next packet +- Sending `CMD_DATA` while TX is pending returns `ERR_TX_PENDING` - See [packet_structure.md](./packet_structure.md) for packet format diff --git a/examples/kiss_modem/KissModem.cpp b/examples/kiss_modem/KissModem.cpp index 4e227d7f..c6e2f2bd 100644 --- a/examples/kiss_modem/KissModem.cpp +++ b/examples/kiss_modem/KissModem.cpp @@ -10,6 +10,13 @@ KissModem::KissModem(Stream& serial, mesh::LocalIdentity& identity, mesh::RNG& r _setRadioCallback = nullptr; _setTxPowerCallback = nullptr; _setSyncWordCallback = nullptr; + _getCurrentRssiCallback = nullptr; + _isChannelBusyCallback = nullptr; + _getAirtimeCallback = nullptr; + _getNoiseFloorCallback = nullptr; + _getStatsCallback = nullptr; + _getBatteryCallback = nullptr; + _getSensorsCallback = nullptr; _config = {0, 0, 0, 0, 0, 0x12}; } @@ -91,6 +98,8 @@ void KissModem::processFrame() { writeErrorFrame(ERR_INVALID_LENGTH); } else if (data_len > KISS_MAX_PACKET_SIZE) { writeErrorFrame(ERR_INVALID_LENGTH); + } else if (_has_pending_tx) { + writeErrorFrame(ERR_TX_PENDING); } else { memcpy(_pending_tx, data, data_len); _pending_tx_len = data_len; @@ -142,6 +151,30 @@ void KissModem::processFrame() { case CMD_GET_VERSION: handleGetVersion(); break; + case CMD_GET_CURRENT_RSSI: + handleGetCurrentRssi(); + break; + case CMD_IS_CHANNEL_BUSY: + handleIsChannelBusy(); + break; + case CMD_GET_AIRTIME: + handleGetAirtime(data, data_len); + break; + case CMD_GET_NOISE_FLOOR: + handleGetNoiseFloor(); + break; + case CMD_GET_STATS: + handleGetStats(); + break; + case CMD_GET_BATTERY: + handleGetBattery(); + break; + case CMD_PING: + handlePing(); + break; + case CMD_GET_SENSORS: + handleGetSensors(data, data_len); + break; default: writeErrorFrame(ERR_UNKNOWN_CMD); break; @@ -360,3 +393,98 @@ void KissModem::onTxComplete(bool success) { uint8_t result = success ? 0x01 : 0x00; writeFrame(RESP_TX_DONE, &result, 1); } + +void KissModem::handleGetCurrentRssi() { + if (!_getCurrentRssiCallback) { + writeErrorFrame(ERR_NO_CALLBACK); + return; + } + + float rssi = _getCurrentRssiCallback(); + int8_t rssi_byte = (int8_t)rssi; + writeFrame(RESP_CURRENT_RSSI, (uint8_t*)&rssi_byte, 1); +} + +void KissModem::handleIsChannelBusy() { + if (!_isChannelBusyCallback) { + writeErrorFrame(ERR_NO_CALLBACK); + return; + } + + uint8_t busy = _isChannelBusyCallback() ? 0x01 : 0x00; + writeFrame(RESP_CHANNEL_BUSY, &busy, 1); +} + +void KissModem::handleGetAirtime(const uint8_t* data, uint16_t len) { + if (len < 1) { + writeErrorFrame(ERR_INVALID_LENGTH); + return; + } + if (!_getAirtimeCallback) { + writeErrorFrame(ERR_NO_CALLBACK); + return; + } + + uint8_t packet_len = data[0]; + uint32_t airtime = _getAirtimeCallback(packet_len); + writeFrame(RESP_AIRTIME, (uint8_t*)&airtime, 4); +} + +void KissModem::handleGetNoiseFloor() { + if (!_getNoiseFloorCallback) { + writeErrorFrame(ERR_NO_CALLBACK); + return; + } + + int16_t noise_floor = _getNoiseFloorCallback(); + writeFrame(RESP_NOISE_FLOOR, (uint8_t*)&noise_floor, 2); +} + +void KissModem::handleGetStats() { + if (!_getStatsCallback) { + writeErrorFrame(ERR_NO_CALLBACK); + return; + } + + uint32_t rx, tx, errors; + _getStatsCallback(&rx, &tx, &errors); + uint8_t buf[12]; + memcpy(buf, &rx, 4); + memcpy(buf + 4, &tx, 4); + memcpy(buf + 8, &errors, 4); + writeFrame(RESP_STATS, buf, 12); +} + +void KissModem::handleGetBattery() { + if (!_getBatteryCallback) { + writeErrorFrame(ERR_NO_CALLBACK); + return; + } + + uint16_t mv = _getBatteryCallback(); + writeFrame(RESP_BATTERY, (uint8_t*)&mv, 2); +} + +void KissModem::handlePing() { + writeFrame(RESP_PONG, nullptr, 0); +} + +void KissModem::handleGetSensors(const uint8_t* data, uint16_t len) { + if (len < 1) { + writeErrorFrame(ERR_INVALID_LENGTH); + return; + } + if (!_getSensorsCallback) { + writeErrorFrame(ERR_NO_CALLBACK); + return; + } + + uint8_t permissions = data[0]; + uint8_t buf[255]; + uint8_t result_len = _getSensorsCallback(permissions, buf, 255); + if (result_len > 0) { + writeFrame(RESP_SENSORS, buf, result_len); + } else { + writeFrame(RESP_SENSORS, nullptr, 0); + } +} diff --git a/examples/kiss_modem/KissModem.h b/examples/kiss_modem/KissModem.h index 34d9577f..e223d92d 100644 --- a/examples/kiss_modem/KissModem.h +++ b/examples/kiss_modem/KissModem.h @@ -28,22 +28,38 @@ #define CMD_GET_TX_POWER 0x0D #define CMD_GET_SYNC_WORD 0x0E #define CMD_GET_VERSION 0x0F +#define CMD_GET_CURRENT_RSSI 0x10 +#define CMD_IS_CHANNEL_BUSY 0x11 +#define CMD_GET_AIRTIME 0x12 +#define CMD_GET_NOISE_FLOOR 0x13 +#define CMD_GET_STATS 0x14 +#define CMD_GET_BATTERY 0x15 +#define CMD_PING 0x16 +#define CMD_GET_SENSORS 0x17 -#define RESP_IDENTITY 0x11 -#define RESP_RANDOM 0x12 -#define RESP_VERIFY 0x13 -#define RESP_SIGNATURE 0x14 -#define RESP_ENCRYPTED 0x15 -#define RESP_DECRYPTED 0x16 -#define RESP_SHARED_SECRET 0x17 -#define RESP_HASH 0x18 -#define RESP_OK 0x19 -#define RESP_RADIO 0x1A -#define RESP_TX_POWER 0x1B -#define RESP_SYNC_WORD 0x1C -#define RESP_VERSION 0x1D -#define RESP_ERROR 0x1E -#define RESP_TX_DONE 0x1F +#define RESP_IDENTITY 0x21 +#define RESP_RANDOM 0x22 +#define RESP_VERIFY 0x23 +#define RESP_SIGNATURE 0x24 +#define RESP_ENCRYPTED 0x25 +#define RESP_DECRYPTED 0x26 +#define RESP_SHARED_SECRET 0x27 +#define RESP_HASH 0x28 +#define RESP_OK 0x29 +#define RESP_RADIO 0x2A +#define RESP_TX_POWER 0x2B +#define RESP_SYNC_WORD 0x2C +#define RESP_VERSION 0x2D +#define RESP_ERROR 0x2E +#define RESP_TX_DONE 0x2F +#define RESP_CURRENT_RSSI 0x30 +#define RESP_CHANNEL_BUSY 0x31 +#define RESP_AIRTIME 0x32 +#define RESP_NOISE_FLOOR 0x33 +#define RESP_STATS 0x34 +#define RESP_BATTERY 0x35 +#define RESP_PONG 0x36 +#define RESP_SENSORS 0x37 #define ERR_INVALID_LENGTH 0x01 #define ERR_INVALID_PARAM 0x02 @@ -51,12 +67,20 @@ #define ERR_MAC_FAILED 0x04 #define ERR_UNKNOWN_CMD 0x05 #define ERR_ENCRYPT_FAILED 0x06 +#define ERR_TX_PENDING 0x07 -#define KISS_FIRMWARE_VERSION 1 +#define KISS_FIRMWARE_VERSION 2 typedef void (*SetRadioCallback)(float freq, float bw, uint8_t sf, uint8_t cr); typedef void (*SetTxPowerCallback)(uint8_t power); typedef void (*SetSyncWordCallback)(uint8_t syncWord); +typedef float (*GetCurrentRssiCallback)(); +typedef bool (*IsChannelBusyCallback)(); +typedef uint32_t (*GetAirtimeCallback)(uint8_t len); +typedef int16_t (*GetNoiseFloorCallback)(); +typedef void (*GetStatsCallback)(uint32_t* rx, uint32_t* tx, uint32_t* errors); +typedef uint16_t (*GetBatteryCallback)(); +typedef uint8_t (*GetSensorsCallback)(uint8_t permissions, uint8_t* buffer, uint8_t max_len); struct RadioConfig { uint32_t freq_hz; @@ -84,6 +108,13 @@ class KissModem { SetRadioCallback _setRadioCallback; SetTxPowerCallback _setTxPowerCallback; SetSyncWordCallback _setSyncWordCallback; + GetCurrentRssiCallback _getCurrentRssiCallback; + IsChannelBusyCallback _isChannelBusyCallback; + GetAirtimeCallback _getAirtimeCallback; + GetNoiseFloorCallback _getNoiseFloorCallback; + GetStatsCallback _getStatsCallback; + GetBatteryCallback _getBatteryCallback; + GetSensorsCallback _getSensorsCallback; RadioConfig _config; @@ -107,6 +138,14 @@ class KissModem { void handleGetTxPower(); void handleGetSyncWord(); void handleGetVersion(); + void handleGetCurrentRssi(); + void handleIsChannelBusy(); + void handleGetAirtime(const uint8_t* data, uint16_t len); + void handleGetNoiseFloor(); + void handleGetStats(); + void handleGetBattery(); + void handlePing(); + void handleGetSensors(const uint8_t* data, uint16_t len); public: KissModem(Stream& serial, mesh::LocalIdentity& identity, mesh::RNG& rng); @@ -117,6 +156,13 @@ public: void setRadioCallback(SetRadioCallback cb) { _setRadioCallback = cb; } void setTxPowerCallback(SetTxPowerCallback cb) { _setTxPowerCallback = cb; } void setSyncWordCallback(SetSyncWordCallback cb) { _setSyncWordCallback = cb; } + void setGetCurrentRssiCallback(GetCurrentRssiCallback cb) { _getCurrentRssiCallback = cb; } + void setIsChannelBusyCallback(IsChannelBusyCallback cb) { _isChannelBusyCallback = cb; } + void setGetAirtimeCallback(GetAirtimeCallback cb) { _getAirtimeCallback = cb; } + void setGetNoiseFloorCallback(GetNoiseFloorCallback cb) { _getNoiseFloorCallback = cb; } + void setGetStatsCallback(GetStatsCallback cb) { _getStatsCallback = cb; } + void setGetBatteryCallback(GetBatteryCallback cb) { _getBatteryCallback = cb; } + void setGetSensorsCallback(GetSensorsCallback cb) { _getSensorsCallback = cb; } bool getPacketToSend(uint8_t* packet, uint16_t* len); void onPacketReceived(int8_t snr, int8_t rssi, const uint8_t* packet, uint16_t len); diff --git a/examples/kiss_modem/main.cpp b/examples/kiss_modem/main.cpp index 2f843a99..0a54c9d3 100644 --- a/examples/kiss_modem/main.cpp +++ b/examples/kiss_modem/main.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include "KissModem.h" #if defined(NRF52_PLATFORM) @@ -56,6 +57,42 @@ void onSetSyncWord(uint8_t sync_word) { radio_set_sync_word(sync_word); } +float onGetCurrentRssi() { + return radio_driver.getCurrentRSSI(); +} + +bool onIsChannelBusy() { + return radio_driver.isReceiving(); +} + +uint32_t onGetAirtime(uint8_t len) { + return radio_driver.getEstAirtimeFor(len); +} + +int16_t onGetNoiseFloor() { + return radio_driver.getNoiseFloor(); +} + +void onGetStats(uint32_t* rx, uint32_t* tx, uint32_t* errors) { + *rx = radio_driver.getPacketsRecv(); + *tx = radio_driver.getPacketsSent(); + *errors = radio_driver.getPacketsRecvErrors(); +} + +uint16_t onGetBattery() { + return board.getBattMilliVolts(); +} + +uint8_t onGetSensors(uint8_t permissions, uint8_t* buffer, uint8_t max_len) { + CayenneLPP telemetry(max_len); + if (sensors.querySensors(permissions, telemetry)) { + uint8_t len = telemetry.getSize(); + memcpy(buffer, telemetry.getBuffer(), len); + return len; + } + return 0; +} + void setup() { board.begin(); @@ -73,10 +110,19 @@ void setup() { while (!Serial && millis() - start < 3000) delay(10); delay(100); + sensors.begin(); + modem = new KissModem(Serial, identity, rng); modem->setRadioCallback(onSetRadio); modem->setTxPowerCallback(onSetTxPower); modem->setSyncWordCallback(onSetSyncWord); + modem->setGetCurrentRssiCallback(onGetCurrentRssi); + modem->setIsChannelBusyCallback(onIsChannelBusy); + modem->setGetAirtimeCallback(onGetAirtime); + modem->setGetNoiseFloorCallback(onGetNoiseFloor); + modem->setGetStatsCallback(onGetStats); + modem->setGetBatteryCallback(onGetBattery); + modem->setGetSensorsCallback(onGetSensors); modem->begin(); } From 240b5ea1e33fdc44808c87a268e4295cfa474ded Mon Sep 17 00:00:00 2001 From: ViezeVingertjes Date: Sat, 31 Jan 2026 15:08:25 +0100 Subject: [PATCH 21/23] Refactor KissModem to integrate radio and sensor management directly, removing callback dependencies. --- examples/kiss_modem/KissModem.cpp | 49 +++++++------------------------ examples/kiss_modem/KissModem.h | 25 +++++----------- examples/kiss_modem/main.cpp | 34 +-------------------- 3 files changed, 20 insertions(+), 88 deletions(-) diff --git a/examples/kiss_modem/KissModem.cpp b/examples/kiss_modem/KissModem.cpp index c6e2f2bd..d11e8217 100644 --- a/examples/kiss_modem/KissModem.cpp +++ b/examples/kiss_modem/KissModem.cpp @@ -1,7 +1,9 @@ #include "KissModem.h" +#include -KissModem::KissModem(Stream& serial, mesh::LocalIdentity& identity, mesh::RNG& rng) - : _serial(serial), _identity(identity), _rng(rng) { +KissModem::KissModem(Stream& serial, mesh::LocalIdentity& identity, mesh::RNG& rng, + mesh::Radio& radio, mesh::MainBoard& board, SensorManager& sensors) + : _serial(serial), _identity(identity), _rng(rng), _radio(radio), _board(board), _sensors(sensors) { _rx_len = 0; _rx_escaped = false; _rx_active = false; @@ -11,12 +13,7 @@ KissModem::KissModem(Stream& serial, mesh::LocalIdentity& identity, mesh::RNG& r _setTxPowerCallback = nullptr; _setSyncWordCallback = nullptr; _getCurrentRssiCallback = nullptr; - _isChannelBusyCallback = nullptr; - _getAirtimeCallback = nullptr; - _getNoiseFloorCallback = nullptr; _getStatsCallback = nullptr; - _getBatteryCallback = nullptr; - _getSensorsCallback = nullptr; _config = {0, 0, 0, 0, 0, 0x12}; } @@ -406,12 +403,7 @@ void KissModem::handleGetCurrentRssi() { } void KissModem::handleIsChannelBusy() { - if (!_isChannelBusyCallback) { - writeErrorFrame(ERR_NO_CALLBACK); - return; - } - - uint8_t busy = _isChannelBusyCallback() ? 0x01 : 0x00; + uint8_t busy = _radio.isReceiving() ? 0x01 : 0x00; writeFrame(RESP_CHANNEL_BUSY, &busy, 1); } @@ -420,23 +412,14 @@ void KissModem::handleGetAirtime(const uint8_t* data, uint16_t len) { writeErrorFrame(ERR_INVALID_LENGTH); return; } - if (!_getAirtimeCallback) { - writeErrorFrame(ERR_NO_CALLBACK); - return; - } uint8_t packet_len = data[0]; - uint32_t airtime = _getAirtimeCallback(packet_len); + uint32_t airtime = _radio.getEstAirtimeFor(packet_len); writeFrame(RESP_AIRTIME, (uint8_t*)&airtime, 4); } void KissModem::handleGetNoiseFloor() { - if (!_getNoiseFloorCallback) { - writeErrorFrame(ERR_NO_CALLBACK); - return; - } - - int16_t noise_floor = _getNoiseFloorCallback(); + int16_t noise_floor = _radio.getNoiseFloor(); writeFrame(RESP_NOISE_FLOOR, (uint8_t*)&noise_floor, 2); } @@ -456,12 +439,7 @@ void KissModem::handleGetStats() { } void KissModem::handleGetBattery() { - if (!_getBatteryCallback) { - writeErrorFrame(ERR_NO_CALLBACK); - return; - } - - uint16_t mv = _getBatteryCallback(); + uint16_t mv = _board.getBattMilliVolts(); writeFrame(RESP_BATTERY, (uint8_t*)&mv, 2); } @@ -474,16 +452,11 @@ void KissModem::handleGetSensors(const uint8_t* data, uint16_t len) { writeErrorFrame(ERR_INVALID_LENGTH); return; } - if (!_getSensorsCallback) { - writeErrorFrame(ERR_NO_CALLBACK); - return; - } uint8_t permissions = data[0]; - uint8_t buf[255]; - uint8_t result_len = _getSensorsCallback(permissions, buf, 255); - if (result_len > 0) { - writeFrame(RESP_SENSORS, buf, result_len); + CayenneLPP telemetry(255); + if (_sensors.querySensors(permissions, telemetry)) { + writeFrame(RESP_SENSORS, telemetry.getBuffer(), telemetry.getSize()); } else { writeFrame(RESP_SENSORS, nullptr, 0); } diff --git a/examples/kiss_modem/KissModem.h b/examples/kiss_modem/KissModem.h index e223d92d..bc7560f4 100644 --- a/examples/kiss_modem/KissModem.h +++ b/examples/kiss_modem/KissModem.h @@ -3,6 +3,8 @@ #include #include #include +#include +#include #define KISS_FEND 0xC0 #define KISS_FESC 0xDB @@ -69,18 +71,13 @@ #define ERR_ENCRYPT_FAILED 0x06 #define ERR_TX_PENDING 0x07 -#define KISS_FIRMWARE_VERSION 2 +#define KISS_FIRMWARE_VERSION 1 typedef void (*SetRadioCallback)(float freq, float bw, uint8_t sf, uint8_t cr); typedef void (*SetTxPowerCallback)(uint8_t power); typedef void (*SetSyncWordCallback)(uint8_t syncWord); typedef float (*GetCurrentRssiCallback)(); -typedef bool (*IsChannelBusyCallback)(); -typedef uint32_t (*GetAirtimeCallback)(uint8_t len); -typedef int16_t (*GetNoiseFloorCallback)(); typedef void (*GetStatsCallback)(uint32_t* rx, uint32_t* tx, uint32_t* errors); -typedef uint16_t (*GetBatteryCallback)(); -typedef uint8_t (*GetSensorsCallback)(uint8_t permissions, uint8_t* buffer, uint8_t max_len); struct RadioConfig { uint32_t freq_hz; @@ -95,6 +92,9 @@ class KissModem { Stream& _serial; mesh::LocalIdentity& _identity; mesh::RNG& _rng; + mesh::Radio& _radio; + mesh::MainBoard& _board; + SensorManager& _sensors; uint8_t _rx_buf[KISS_MAX_FRAME_SIZE]; uint16_t _rx_len; @@ -109,12 +109,7 @@ class KissModem { SetTxPowerCallback _setTxPowerCallback; SetSyncWordCallback _setSyncWordCallback; GetCurrentRssiCallback _getCurrentRssiCallback; - IsChannelBusyCallback _isChannelBusyCallback; - GetAirtimeCallback _getAirtimeCallback; - GetNoiseFloorCallback _getNoiseFloorCallback; GetStatsCallback _getStatsCallback; - GetBatteryCallback _getBatteryCallback; - GetSensorsCallback _getSensorsCallback; RadioConfig _config; @@ -148,7 +143,8 @@ class KissModem { void handleGetSensors(const uint8_t* data, uint16_t len); public: - KissModem(Stream& serial, mesh::LocalIdentity& identity, mesh::RNG& rng); + KissModem(Stream& serial, mesh::LocalIdentity& identity, mesh::RNG& rng, + mesh::Radio& radio, mesh::MainBoard& board, SensorManager& sensors); void begin(); void loop(); @@ -157,12 +153,7 @@ public: void setTxPowerCallback(SetTxPowerCallback cb) { _setTxPowerCallback = cb; } void setSyncWordCallback(SetSyncWordCallback cb) { _setSyncWordCallback = cb; } void setGetCurrentRssiCallback(GetCurrentRssiCallback cb) { _getCurrentRssiCallback = cb; } - void setIsChannelBusyCallback(IsChannelBusyCallback cb) { _isChannelBusyCallback = cb; } - void setGetAirtimeCallback(GetAirtimeCallback cb) { _getAirtimeCallback = cb; } - void setGetNoiseFloorCallback(GetNoiseFloorCallback cb) { _getNoiseFloorCallback = cb; } void setGetStatsCallback(GetStatsCallback cb) { _getStatsCallback = cb; } - void setGetBatteryCallback(GetBatteryCallback cb) { _getBatteryCallback = cb; } - void setGetSensorsCallback(GetSensorsCallback cb) { _getSensorsCallback = cb; } bool getPacketToSend(uint8_t* packet, uint16_t* len); void onPacketReceived(int8_t snr, int8_t rssi, const uint8_t* packet, uint16_t len); diff --git a/examples/kiss_modem/main.cpp b/examples/kiss_modem/main.cpp index 0a54c9d3..e81161bf 100644 --- a/examples/kiss_modem/main.cpp +++ b/examples/kiss_modem/main.cpp @@ -2,7 +2,6 @@ #include #include #include -#include #include "KissModem.h" #if defined(NRF52_PLATFORM) @@ -61,38 +60,12 @@ float onGetCurrentRssi() { return radio_driver.getCurrentRSSI(); } -bool onIsChannelBusy() { - return radio_driver.isReceiving(); -} - -uint32_t onGetAirtime(uint8_t len) { - return radio_driver.getEstAirtimeFor(len); -} - -int16_t onGetNoiseFloor() { - return radio_driver.getNoiseFloor(); -} - void onGetStats(uint32_t* rx, uint32_t* tx, uint32_t* errors) { *rx = radio_driver.getPacketsRecv(); *tx = radio_driver.getPacketsSent(); *errors = radio_driver.getPacketsRecvErrors(); } -uint16_t onGetBattery() { - return board.getBattMilliVolts(); -} - -uint8_t onGetSensors(uint8_t permissions, uint8_t* buffer, uint8_t max_len) { - CayenneLPP telemetry(max_len); - if (sensors.querySensors(permissions, telemetry)) { - uint8_t len = telemetry.getSize(); - memcpy(buffer, telemetry.getBuffer(), len); - return len; - } - return 0; -} - void setup() { board.begin(); @@ -112,17 +85,12 @@ void setup() { sensors.begin(); - modem = new KissModem(Serial, identity, rng); + modem = new KissModem(Serial, identity, rng, radio_driver, board, sensors); modem->setRadioCallback(onSetRadio); modem->setTxPowerCallback(onSetTxPower); modem->setSyncWordCallback(onSetSyncWord); modem->setGetCurrentRssiCallback(onGetCurrentRssi); - modem->setIsChannelBusyCallback(onIsChannelBusy); - modem->setGetAirtimeCallback(onGetAirtime); - modem->setGetNoiseFloorCallback(onGetNoiseFloor); modem->setGetStatsCallback(onGetStats); - modem->setGetBatteryCallback(onGetBattery); - modem->setGetSensorsCallback(onGetSensors); modem->begin(); } From f0ba14ff7580fd8dfc866ec0490606485390cb3c Mon Sep 17 00:00:00 2001 From: ViezeVingertjes Date: Mon, 2 Feb 2026 18:05:26 +0100 Subject: [PATCH 22/23] Remove sync word handling from KissModem. --- examples/kiss_modem/KissModem.cpp | 28 +--------------------------- examples/kiss_modem/KissModem.h | 9 --------- examples/kiss_modem/main.cpp | 5 ----- 3 files changed, 1 insertion(+), 41 deletions(-) diff --git a/examples/kiss_modem/KissModem.cpp b/examples/kiss_modem/KissModem.cpp index d11e8217..d9c71bf8 100644 --- a/examples/kiss_modem/KissModem.cpp +++ b/examples/kiss_modem/KissModem.cpp @@ -11,10 +11,9 @@ KissModem::KissModem(Stream& serial, mesh::LocalIdentity& identity, mesh::RNG& r _pending_tx_len = 0; _setRadioCallback = nullptr; _setTxPowerCallback = nullptr; - _setSyncWordCallback = nullptr; _getCurrentRssiCallback = nullptr; _getStatsCallback = nullptr; - _config = {0, 0, 0, 0, 0, 0x12}; + _config = {0, 0, 0, 0, 0}; } void KissModem::begin() { @@ -133,18 +132,12 @@ void KissModem::processFrame() { case CMD_SET_TX_POWER: handleSetTxPower(data, data_len); break; - case CMD_SET_SYNC_WORD: - handleSetSyncWord(data, data_len); - break; case CMD_GET_RADIO: handleGetRadio(); break; case CMD_GET_TX_POWER: handleGetTxPower(); break; - case CMD_GET_SYNC_WORD: - handleGetSyncWord(); - break; case CMD_GET_VERSION: handleGetVersion(); break; @@ -347,21 +340,6 @@ void KissModem::handleSetTxPower(const uint8_t* data, uint16_t len) { writeFrame(RESP_OK, nullptr, 0); } -void KissModem::handleSetSyncWord(const uint8_t* data, uint16_t len) { - if (len < 1) { - writeErrorFrame(ERR_INVALID_LENGTH); - return; - } - if (!_setSyncWordCallback) { - writeErrorFrame(ERR_NO_CALLBACK); - return; - } - - _config.sync_word = data[0]; - _setSyncWordCallback(data[0]); - writeFrame(RESP_OK, nullptr, 0); -} - void KissModem::handleGetRadio() { uint8_t buf[10]; memcpy(buf, &_config.freq_hz, 4); @@ -375,10 +353,6 @@ void KissModem::handleGetTxPower() { writeFrame(RESP_TX_POWER, &_config.tx_power, 1); } -void KissModem::handleGetSyncWord() { - writeFrame(RESP_SYNC_WORD, &_config.sync_word, 1); -} - void KissModem::handleGetVersion() { uint8_t buf[2]; buf[0] = KISS_FIRMWARE_VERSION; diff --git a/examples/kiss_modem/KissModem.h b/examples/kiss_modem/KissModem.h index bc7560f4..170bb0c2 100644 --- a/examples/kiss_modem/KissModem.h +++ b/examples/kiss_modem/KissModem.h @@ -25,10 +25,8 @@ #define CMD_HASH 0x08 #define CMD_SET_RADIO 0x09 #define CMD_SET_TX_POWER 0x0A -#define CMD_SET_SYNC_WORD 0x0B #define CMD_GET_RADIO 0x0C #define CMD_GET_TX_POWER 0x0D -#define CMD_GET_SYNC_WORD 0x0E #define CMD_GET_VERSION 0x0F #define CMD_GET_CURRENT_RSSI 0x10 #define CMD_IS_CHANNEL_BUSY 0x11 @@ -50,7 +48,6 @@ #define RESP_OK 0x29 #define RESP_RADIO 0x2A #define RESP_TX_POWER 0x2B -#define RESP_SYNC_WORD 0x2C #define RESP_VERSION 0x2D #define RESP_ERROR 0x2E #define RESP_TX_DONE 0x2F @@ -75,7 +72,6 @@ typedef void (*SetRadioCallback)(float freq, float bw, uint8_t sf, uint8_t cr); typedef void (*SetTxPowerCallback)(uint8_t power); -typedef void (*SetSyncWordCallback)(uint8_t syncWord); typedef float (*GetCurrentRssiCallback)(); typedef void (*GetStatsCallback)(uint32_t* rx, uint32_t* tx, uint32_t* errors); @@ -85,7 +81,6 @@ struct RadioConfig { uint8_t sf; uint8_t cr; uint8_t tx_power; - uint8_t sync_word; }; class KissModem { @@ -107,7 +102,6 @@ class KissModem { SetRadioCallback _setRadioCallback; SetTxPowerCallback _setTxPowerCallback; - SetSyncWordCallback _setSyncWordCallback; GetCurrentRssiCallback _getCurrentRssiCallback; GetStatsCallback _getStatsCallback; @@ -128,10 +122,8 @@ class KissModem { void handleHash(const uint8_t* data, uint16_t len); void handleSetRadio(const uint8_t* data, uint16_t len); void handleSetTxPower(const uint8_t* data, uint16_t len); - void handleSetSyncWord(const uint8_t* data, uint16_t len); void handleGetRadio(); void handleGetTxPower(); - void handleGetSyncWord(); void handleGetVersion(); void handleGetCurrentRssi(); void handleIsChannelBusy(); @@ -151,7 +143,6 @@ public: void setRadioCallback(SetRadioCallback cb) { _setRadioCallback = cb; } void setTxPowerCallback(SetTxPowerCallback cb) { _setTxPowerCallback = cb; } - void setSyncWordCallback(SetSyncWordCallback cb) { _setSyncWordCallback = cb; } void setGetCurrentRssiCallback(GetCurrentRssiCallback cb) { _getCurrentRssiCallback = cb; } void setGetStatsCallback(GetStatsCallback cb) { _getStatsCallback = cb; } diff --git a/examples/kiss_modem/main.cpp b/examples/kiss_modem/main.cpp index e81161bf..959222b9 100644 --- a/examples/kiss_modem/main.cpp +++ b/examples/kiss_modem/main.cpp @@ -52,10 +52,6 @@ void onSetTxPower(uint8_t power) { radio_set_tx_power(power); } -void onSetSyncWord(uint8_t sync_word) { - radio_set_sync_word(sync_word); -} - float onGetCurrentRssi() { return radio_driver.getCurrentRSSI(); } @@ -88,7 +84,6 @@ void setup() { modem = new KissModem(Serial, identity, rng, radio_driver, board, sensors); modem->setRadioCallback(onSetRadio); modem->setTxPowerCallback(onSetTxPower); - modem->setSyncWordCallback(onSetSyncWord); modem->setGetCurrentRssiCallback(onGetCurrentRssi); modem->setGetStatsCallback(onGetStats); modem->begin(); From 598489be471e978ea76d6f759f6367124a0b335a Mon Sep 17 00:00:00 2001 From: taco Date: Mon, 26 Jan 2026 16:41:08 +1100 Subject: [PATCH 23/23] refactor ui with ring buffer and display most recent --- examples/companion_radio/ui-new/UITask.cpp | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 0690b45a..ae2d9375 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -458,15 +458,17 @@ class MsgPreviewScreen : public UIScreen { }; #define MAX_UNREAD_MSGS 32 int num_unread; + int head = MAX_UNREAD_MSGS - 1; // index of latest unread message MsgEntry unread[MAX_UNREAD_MSGS]; public: MsgPreviewScreen(UITask* task, mesh::RTCClock* rtc) : _task(task), _rtc(rtc) { num_unread = 0; } void addPreview(uint8_t path_len, const char* from_name, const char* msg) { - if (num_unread >= MAX_UNREAD_MSGS) return; // full + head = (head + 1) % MAX_UNREAD_MSGS; + if (num_unread < MAX_UNREAD_MSGS) num_unread++; - auto p = &unread[num_unread++]; + auto p = &unread[head]; p->timestamp = _rtc->getCurrentTime(); if (path_len == 0xFF) { sprintf(p->origin, "(D) %s:", from_name); @@ -484,7 +486,7 @@ public: sprintf(tmp, "Unread: %d", num_unread); display.print(tmp); - auto p = &unread[0]; + auto p = &unread[head]; int secs = _rtc->getCurrentTime() - p->timestamp; if (secs < 60) { @@ -520,14 +522,10 @@ public: bool handleInput(char c) override { if (c == KEY_NEXT || c == KEY_RIGHT) { + head = (head + MAX_UNREAD_MSGS - 1) % MAX_UNREAD_MSGS; num_unread--; if (num_unread == 0) { _task->gotoHomeScreen(); - } else { - // delete first/curr item from unread queue - for (int i = 0; i < num_unread; i++) { - unread[i] = unread[i + 1]; - } } return true; }