From 9844b541b6e37b9fbcbe789f28590fede7c72515 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Thu, 26 Feb 2026 20:37:27 -0800 Subject: [PATCH 01/70] fix: JSON parsing regression in build --- build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sh b/build.sh index 313c4c47..23e31ab2 100755 --- a/build.sh +++ b/build.sh @@ -93,7 +93,7 @@ get_pio_envs_ending_with_string() { # $1 should be the environment name get_platform_for_env() { local env_name=$1 - echo "$PIO_CONFIG_JSON" | python3 -c " + printf '%s' "$PIO_CONFIG_JSON" | python3 -c " import sys, json, re data = json.load(sys.stdin) for section, options in data: From dea5ed790fe2166f7248da97270f186c1f05e752 Mon Sep 17 00:00:00 2001 From: Nick Le Mouton Date: Fri, 5 Jun 2026 21:25:25 +1200 Subject: [PATCH 02/70] Add SECURITY.md --- SECURITY.md | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..a4b2207d --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,57 @@ +# Security Policy + +## Supported Versions + +Security fixes are applied to the latest release only. We do not backport +fixes to older versions. + +| Version | Supported | +|---------|-----------| +| 1.15+ | ✅ | +| <1.15 | ❌ | + +## Reporting a Vulnerability + +**Please do not report security vulnerabilities through public GitHub issues.** + +Use GitHub's private vulnerability reporting instead: +1. Go to the **Security** tab of this repository +2. Click **Report a vulnerability** +3. Fill in the details and submit + +### What to include + +A useful report tells us: +- Which component or file is affected +- What an attacker can do (impact) and under what conditions +- A minimal reproduction case or proof-of-concept if you have one +- Whether you believe it is remotely exploitable + +You do not need a working exploit to report. An incomplete report is better +than no report. + +## What to expect + +This is a volunteer-maintained open-source project. We will do our best to +respond in a reasonable timeframe, but cannot commit to specific deadlines. + +We ask that you give us a fair opportunity to investigate and address the +issue before any public disclosure. If you have not heard back after +**90 days**, feel free to follow up or proceed with disclosure at your +discretion. + +## Scope + +In scope: +- Remote code execution, memory corruption, or denial-of-service via crafted + radio packets +- Authentication or encryption bypasses +- Vulnerabilities in the packet routing or path handling logic + +Out of scope: +- Physical access attacks (e.g., JTAG, UART extraction of keys) +- Regulatory compliance (duty cycle, frequency restrictions) +- Jamming or other physical-layer radio interference +- Issues in third-party libraries (RadioLib, Crypto, etc.) — report those + upstream +- "Best practice" suggestions without a demonstrated attack path From c94ed29ca36e73d9026319af18b8977648ec8809 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Fri, 12 Jun 2026 03:18:10 +1200 Subject: [PATCH 03/70] add github workflow to close stale issues --- .github/workflows/stale-bot.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/stale-bot.yml diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml new file mode 100644 index 00000000..afe874f8 --- /dev/null +++ b/.github/workflows/stale-bot.yml @@ -0,0 +1,32 @@ +name: 'Run Stale Bot' +on: + schedule: + - cron: '30 1 * * *' # daily at 1:30am + workflow_dispatch: {} + +permissions: + actions: write + issues: write + pull-requests: write + +jobs: + close-issues: + # only run on main repo, not forks + if: github.repository == "meshcore-dev/MeshCore" + runs-on: ubuntu-latest + steps: + - name: Close Stale Issues + uses: actions/stale@v10 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + # auto close issues + days-before-issue-stale: 60 + days-before-issue-close: 7 + exempt-issue-labels: "keep-open" + stale-issue-label: "stale" + stale-issue-message: "This issue is stale because it has been open for 60 days with no activity. Remove the stale label or add a comment if this issue is still relevant, otherwise this issue will automatically close in 7 days." + close-issue-message: "This issue was closed because it has been inactive for 7 days since being marked as stale." + # don't auto close prs + days-before-pr-stale: -1 + days-before-pr-close: -1 + \ No newline at end of file From 3b3992539945b861801ab709b2361cb03edee657 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Fri, 12 Jun 2026 03:20:44 +1200 Subject: [PATCH 04/70] use single quotes for repo name --- .github/workflows/stale-bot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml index afe874f8..ec166587 100644 --- a/.github/workflows/stale-bot.yml +++ b/.github/workflows/stale-bot.yml @@ -12,7 +12,7 @@ permissions: jobs: close-issues: # only run on main repo, not forks - if: github.repository == "meshcore-dev/MeshCore" + if: github.repository == 'meshcore-dev/MeshCore' runs-on: ubuntu-latest steps: - name: Close Stale Issues From 06130dce29c907ebc014dc52156fbb1e52210903 Mon Sep 17 00:00:00 2001 From: formtapez Date: Fri, 12 Jun 2026 12:11:12 +0200 Subject: [PATCH 05/70] added some missing CLI commands --- docs/cli_commands.md | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 9accb299..f482cfcb 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -28,12 +28,25 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Usage:** - `reboot` +**Note:** No reply is sent. + +--- + +### Power-off the node +**Usage:** +- `poweroff`, or +- `shutdown +` +**Note:** No reply is sent. + --- ### Reset the clock and reboot **Usage:** - `clkreboot` +**Note:** No reply is sent. + --- ### Sync the clock with the remote device @@ -632,10 +645,21 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Parameters:** - `value`: Maximum flood hop count (0-64) for a packet without a scope (no region set) -**Default:** `0xFF` - indicates it hasn't been set, will track flood.max until it is. +**Default:** `64` - indicates it hasn't been set, will track flood.max until it is. **Note:** An alternative to `region denyf *`, setting `flood.max.unscoped` to a lower value such as `3` would allow for local unscoped messages to propagate, while preventing noisy neighbors from flooding a local region. +--- + +#### Limit the number of hops for an advert flood message +**Usage:** +- `get flood.max.advert` +- `set flood.max.advert ` + +**Parameters:** +- `value`: Maximum flood hop count (0-64) for an advert packet + +**Default:** `8` --- From d3444e6b0be513f982f3bcb7d2f05a091f16a2fd Mon Sep 17 00:00:00 2001 From: formtapez Date: Fri, 12 Jun 2026 12:14:46 +0200 Subject: [PATCH 06/70] fix formatting --- docs/cli_commands.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index f482cfcb..66a9b77a 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -35,8 +35,8 @@ This document provides an overview of CLI commands that can be sent to MeshCore ### Power-off the node **Usage:** - `poweroff`, or -- `shutdown -` +- `shutdown` + **Note:** No reply is sent. --- @@ -645,7 +645,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Parameters:** - `value`: Maximum flood hop count (0-64) for a packet without a scope (no region set) -**Default:** `64` - indicates it hasn't been set, will track flood.max until it is. +**Default:** `64` - (`0xFF` indicates it hasn't been set, will track flood.max until it is.) **Note:** An alternative to `region denyf *`, setting `flood.max.unscoped` to a lower value such as `3` would allow for local unscoped messages to propagate, while preventing noisy neighbors from flooding a local region. From b8b6598dd3ab47489767167aa85ce727c5823054 Mon Sep 17 00:00:00 2001 From: UltimateCodeWarrior Date: Mon, 22 Jun 2026 23:30:35 -0700 Subject: [PATCH 07/70] Fix blank OLED on T-Beam Supreme V3 (I2C bus + 0x3D address) The SH1106 OLED on the LilyGo T-Beam Supreme V3 never lights up because: 1. The primary I2C bus (Wire) that the OLED, BME280 and magnetometer share is never initialized. The PMU/RTC use Wire1 (via XPowersLib); nothing brings up Wire on PIN_BOARD_SDA/PIN_BOARD_SCL, so the display driver talks on the wrong default pins. 2. The OLED is at I2C address 0x3D on this board (0x3C is taken by the QMC6310 magnetometer), but SH1106Display::begin() only tried 0x3C. Initialize Wire for TBEAM_SUPREME_SX1262, and have SH1106Display::begin() prefer 0x3D (only ever used by the OLED) before falling back to 0x3C. Backwards-compatible for boards with the OLED at 0x3C. Closes #2609 --- src/helpers/esp32/TBeamBoard.cpp | 10 ++++++++++ src/helpers/ui/SH1106Display.cpp | 12 +++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/helpers/esp32/TBeamBoard.cpp b/src/helpers/esp32/TBeamBoard.cpp index 5f708d71..d9f6e022 100644 --- a/src/helpers/esp32/TBeamBoard.cpp +++ b/src/helpers/esp32/TBeamBoard.cpp @@ -16,6 +16,16 @@ void TBeamBoard::begin() { ESP32Board::begin(); +#ifdef TBEAM_SUPREME_SX1262 + // On the T-Beam S3 Supreme the PMU + RTC sit on Wire1 (GPIO 42/41, brought + // up by XPowersLib), while the SH1106 OLED and the BME280/QMC6310 sensors + // sit on the primary bus, Wire (GPIO PIN_BOARD_SDA/PIN_BOARD_SCL). Nothing + // else initialises Wire on this board, so the display driver would + // otherwise talk on the wrong (default) pins and display.begin() fails, + // leaving the screen blank. Bring the OLED bus up here. + Wire.begin(PIN_BOARD_SDA, PIN_BOARD_SCL); +#endif + power_init(); //Configure user button diff --git a/src/helpers/ui/SH1106Display.cpp b/src/helpers/ui/SH1106Display.cpp index f383bb00..86c6089b 100644 --- a/src/helpers/ui/SH1106Display.cpp +++ b/src/helpers/ui/SH1106Display.cpp @@ -11,7 +11,17 @@ bool SH1106Display::i2c_probe(TwoWire &wire, uint8_t addr) bool SH1106Display::begin() { - return display.begin(DISPLAY_ADDRESS, true) && i2c_probe(Wire, DISPLAY_ADDRESS); + // Address selection: on some board revisions (notably the LilyGo T-Beam + // Supreme V3) the OLED lives at 0x3D because 0x3C is occupied by a + // magnetometer (QMC6310N). 0x3D is only ever used by the OLED, so prefer it + // when present, otherwise fall back to the standard 0x3C (DISPLAY_ADDRESS). + uint8_t addr = 0; + if (i2c_probe(Wire, 0x3D)) { + addr = 0x3D; + } else if (i2c_probe(Wire, DISPLAY_ADDRESS)) { + addr = DISPLAY_ADDRESS; + } + return addr && display.begin(addr, true); } void SH1106Display::turnOn() From 3af5ffe11923eff8da35a14baf505f207fc33d2d Mon Sep 17 00:00:00 2001 From: Naomi Smith Date: Fri, 3 Jul 2026 11:55:06 -0700 Subject: [PATCH 08/70] Fix GPS and LEDs on ThinkNode M3 GPS support was implemented incorrectly for the ThinkNode M3. The reset pin was being driven when it should be left floating for this unit. The enable pin also wasn't being picked up by `MicroNMEALocationProvider` because of a mismatch in constant naming conventions. I did a general cleanup of the GPS and ThinkNode M3 bring-up code so that constant names line up and "*_ACTIVE" constants are used consistently vs hardcoding `HIGH`/`LOW`. After making these changes, serial data immediately starts streaming in from the NMEA on boot and GPS detection just works. LED handling was also not quite right for the ThinkNode M3. The LoRa TX LED was being driven high to turn it on when it should actually be driven low. I changed the code to use the `LED_STATE_ON` constant and also added a little code to the shutdown path to properly make sure that all LEDs are turned off. Tested and confirmed working on real hardware. Resolves both #1864 and #2879. --- .../sensors/MicroNMEALocationProvider.h | 32 +++++++++++-------- variants/thinknode_m3/ThinkNodeM3Board.h | 12 +++---- variants/thinknode_m3/platformio.ini | 2 -- variants/thinknode_m3/variant.cpp | 8 +++-- variants/thinknode_m3/variant.h | 9 +++--- 5 files changed, 34 insertions(+), 29 deletions(-) diff --git a/src/helpers/sensors/MicroNMEALocationProvider.h b/src/helpers/sensors/MicroNMEALocationProvider.h index eec466d3..9d697fed 100644 --- a/src/helpers/sensors/MicroNMEALocationProvider.h +++ b/src/helpers/sensors/MicroNMEALocationProvider.h @@ -13,8 +13,12 @@ #endif #endif -#ifndef PIN_GPS_EN_ACTIVE - #define PIN_GPS_EN_ACTIVE HIGH +#ifndef GPS_EN_ACTIVE + #ifdef PIN_GPS_EN_ACTIVE + #define GPS_EN_ACTIVE PIN_GPS_EN_ACTIVE + #else + #define GPS_EN_ACTIVE HIGH + #endif #endif #ifndef GPS_RESET @@ -25,11 +29,11 @@ #endif #endif -#ifndef GPS_RESET_FORCE +#ifndef GPS_RESET_ACTIVE #ifdef PIN_GPS_RESET_ACTIVE - #define GPS_RESET_FORCE PIN_GPS_RESET_ACTIVE + #define GPS_RESET_ACTIVE PIN_GPS_RESET_ACTIVE #else - #define GPS_RESET_FORCE LOW + #define GPS_RESET_ACTIVE LOW #endif #endif @@ -52,11 +56,11 @@ public : _gps_serial(&ser), nmea(_nmeaBuffer, sizeof(_nmeaBuffer)), _pin_reset(pin_reset), _pin_en(pin_en), _clock(clock), _peripher_power(peripher_power) { if (_pin_reset != -1) { pinMode(_pin_reset, OUTPUT); - digitalWrite(_pin_reset, GPS_RESET_FORCE); + digitalWrite(_pin_reset, GPS_RESET_ACTIVE); } if (_pin_en != -1) { pinMode(_pin_en, OUTPUT); - digitalWrite(_pin_en, LOW); + digitalWrite(_pin_en, !GPS_EN_ACTIVE); } } @@ -76,27 +80,27 @@ public : void begin() override { claim(); if (_pin_en != -1) { - digitalWrite(_pin_en, PIN_GPS_EN_ACTIVE); + digitalWrite(_pin_en, GPS_EN_ACTIVE); } if (_pin_reset != -1) { - digitalWrite(_pin_reset, !GPS_RESET_FORCE); + digitalWrite(_pin_reset, !GPS_RESET_ACTIVE); } } void reset() override { if (_pin_reset != -1) { - digitalWrite(_pin_reset, GPS_RESET_FORCE); + digitalWrite(_pin_reset, GPS_RESET_ACTIVE); delay(10); - digitalWrite(_pin_reset, !GPS_RESET_FORCE); + digitalWrite(_pin_reset, !GPS_RESET_ACTIVE); } } void stop() override { if (_pin_en != -1) { - digitalWrite(_pin_en, !PIN_GPS_EN_ACTIVE); + digitalWrite(_pin_en, !GPS_EN_ACTIVE); } if (_pin_reset != -1) { - digitalWrite(_pin_reset, GPS_RESET_FORCE); + digitalWrite(_pin_reset, GPS_RESET_ACTIVE); } release(); } @@ -105,7 +109,7 @@ public : // directly read the enable pin if present as gps can be // activated/deactivated outside of here ... if (_pin_en != -1) { - return digitalRead(_pin_en) == PIN_GPS_EN_ACTIVE; + return digitalRead(_pin_en) == GPS_EN_ACTIVE; } else { return true; // no enable so must be active } diff --git a/variants/thinknode_m3/ThinkNodeM3Board.h b/variants/thinknode_m3/ThinkNodeM3Board.h index 1435d31d..63b14116 100644 --- a/variants/thinknode_m3/ThinkNodeM3Board.h +++ b/variants/thinknode_m3/ThinkNodeM3Board.h @@ -18,12 +18,12 @@ public: void begin(); uint16_t getBattMilliVolts() override; -#if defined(P_LORA_TX_LED) +#ifdef P_LORA_TX_LED void onBeforeTransmit() override { - digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on + digitalWrite(P_LORA_TX_LED, LED_STATE_ON); // turn TX LED on } void onAfterTransmit() override { - digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED off + digitalWrite(P_LORA_TX_LED, !LED_STATE_ON); // turn TX LED off } #endif @@ -44,9 +44,9 @@ public: 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 + digitalWrite(PIN_LED_BLUE, !LED_STATE_ON); + digitalWrite(PIN_LED_GREEN, !LED_STATE_ON); + digitalWrite(PIN_LED_RED, !LED_STATE_ON); // power off board sd_power_system_off(); diff --git a/variants/thinknode_m3/platformio.ini b/variants/thinknode_m3/platformio.ini index 0a3d4eda..0004f4c7 100644 --- a/variants/thinknode_m3/platformio.ini +++ b/variants/thinknode_m3/platformio.ini @@ -24,7 +24,6 @@ build_flags = ${nrf52_base.build_flags} -D P_LORA_MOSI=46 -D P_LORA_RESET=42 -D P_LORA_TX_LED=PIN_LED_BLUE - -D P_LORA_TX_LED_ON=LOW -D LR11X0_DIO_AS_RF_SWITCH=true -D LR11X0_DIO3_TCXO_VOLTAGE=3.3 -D MESH_DEBUG=1 @@ -105,7 +104,6 @@ build_flags = ${ThinkNode_M3.build_flags} -D BLE_TX_POWER=0 ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 - -D GPS_NMEA_DEBUG -D OFFLINE_QUEUE_SIZE=256 -D DISPLAY_CLASS=NullDisplayDriver -D PIN_BUZZER=23 diff --git a/variants/thinknode_m3/variant.cpp b/variants/thinknode_m3/variant.cpp index dad0f3f5..b47b8354 100644 --- a/variants/thinknode_m3/variant.cpp +++ b/variants/thinknode_m3/variant.cpp @@ -80,16 +80,18 @@ void initVariant() digitalWrite(LED_POWER, HIGH); pinMode(PIN_LED_BLUE, OUTPUT); + digitalWrite(PIN_LED_BLUE, !LED_STATE_ON); pinMode(PIN_LED_GREEN, OUTPUT); + digitalWrite(PIN_LED_GREEN, !LED_STATE_ON); pinMode(PIN_LED_RED, OUTPUT); + digitalWrite(PIN_LED_RED, !LED_STATE_ON); pinMode(BUTTON_PIN, INPUT_PULLUP); pinMode(PIN_GPS_POWER, OUTPUT); pinMode(PIN_GPS_EN, OUTPUT); - pinMode(PIN_GPS_RESET, OUTPUT); // Power on gps but in standby - digitalWrite(PIN_GPS_EN, LOW); - digitalWrite(PIN_GPS_POWER, HIGH); + digitalWrite(PIN_GPS_EN, !GPS_EN_ACTIVE); + digitalWrite(PIN_GPS_POWER, GPS_POWER_ACTIVE); } diff --git a/variants/thinknode_m3/variant.h b/variants/thinknode_m3/variant.h index 02ed78a8..78dfab85 100644 --- a/variants/thinknode_m3/variant.h +++ b/variants/thinknode_m3/variant.h @@ -32,7 +32,7 @@ #define EXT_CHRG_DETECT (32) // P1.3 #define EXT_PWR_DETECT (31) // P0.5 -#define PIN_VBAT_READ (5) +#define PIN_VBAT_READ (5) #define AREF_VOLTAGE (2.4f) #define ADC_MULTIPLIER (2.0) //(1.75f) // 2.0 gives more coherent value, 4.2V when charged, needs tweaking @@ -92,18 +92,19 @@ // GPS #define HAS_GPS 1 -#define PIN_GPS_RX (22) +#define PIN_GPS_RX (22) #define PIN_GPS_TX (20) #define PIN_GPS_POWER (14) #define PIN_GPS_EN (21) // STANDBY #define PIN_GPS_RESET (25) // REINIT -#define GPS_RESET_ACTIVE LOW +#define GPS_POWER_ACTIVE HIGH #define GPS_EN_ACTIVE HIGH +#define GPS_RESET (-1) #define GPS_BAUDRATE 9600 //////////////////////////////////////////////////////////////////////////////// // Buzzer #define BUZZER_EN (37) // P1.5 -#define BUZZER_PIN (25) // P0.25 \ No newline at end of file +#define BUZZER_PIN (25) // P0.25 From 57563ab8f92be3a6396b3eead4898b36f186a44b Mon Sep 17 00:00:00 2001 From: liamcottle Date: Mon, 6 Jul 2026 02:04:56 +1200 Subject: [PATCH 09/70] update qr code docs --- docs/qr_codes.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/qr_codes.md b/docs/qr_codes.md index 364efa8a..3516a76a 100644 --- a/docs/qr_codes.md +++ b/docs/qr_codes.md @@ -12,8 +12,10 @@ meshcore://channel/add?name=Public&secret=8b3387e9c5cdea6ac9e5edbaa115cd72 **Parameters**: -- `name`: Channel name (URL-encoded if needed) +- `name`: Channel name (URL-encoded) - `secret`: 16-byte secret represented as 32 hex characters +- `region_scope`: Region Scope (optional, URL-encoded if provided) + - Supported by MeshCore App v1.47.0+ ## Add Contact From 54234b58371d2ecbf3031d807156277e1afff3a1 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Mon, 6 Jul 2026 22:18:49 +1200 Subject: [PATCH 10/70] implement matrix builds for faster firmware releases --- .../setup-build-environment/action.yml | 13 ++- .../workflows/build-companion-firmwares.yml | 35 ++------ .../workflows/build-repeater-firmwares.yml | 35 ++------ .../workflows/build-room-server-firmwares.yml | 35 ++------ .github/workflows/firmware-builder.yml | 90 +++++++++++++++++++ build.sh | 10 +++ 6 files changed, 126 insertions(+), 92 deletions(-) create mode 100644 .github/workflows/firmware-builder.yml diff --git a/.github/actions/setup-build-environment/action.yml b/.github/actions/setup-build-environment/action.yml index 02aaf424..cba2d2e5 100644 --- a/.github/actions/setup-build-environment/action.yml +++ b/.github/actions/setup-build-environment/action.yml @@ -25,5 +25,14 @@ runs: - name: Extract Version from Git Tag shell: bash run: | - GIT_TAG_NAME="${GITHUB_REF#refs/tags/}" - echo "GIT_TAG_VERSION=${GIT_TAG_NAME##*-}" >> $GITHUB_ENV + if [[ "${GITHUB_REF}" == refs/tags/* ]]; then + # triggered by a tag push (e.g: refs/tags/companion-v1.2.3) + GIT_TAG_NAME="${GITHUB_REF#refs/tags/}" + VERSION_STRING="${GIT_TAG_NAME##*-}" + else + # triggered by a workflow dispatch (e.g: refs/heads/main) + # strip "refs/heads/" prefix and replace any remaining "/" with "-" to protect file paths + BRANCH_NAME="${GITHUB_REF#refs/heads/}" + VERSION_STRING=$(echo "$BRANCH_NAME" | tr '/' '-') + fi + echo "GIT_TAG_VERSION=${VERSION_STRING}" >> $GITHUB_ENV diff --git a/.github/workflows/build-companion-firmwares.yml b/.github/workflows/build-companion-firmwares.yml index 771fa6d5..8fd796f7 100644 --- a/.github/workflows/build-companion-firmwares.yml +++ b/.github/workflows/build-companion-firmwares.yml @@ -10,33 +10,8 @@ on: - 'companion-*' jobs: - - build: - runs-on: ubuntu-latest - steps: - - - name: Clone Repo - uses: actions/checkout@v6 - - - name: Setup Build Environment - uses: ./.github/actions/setup-build-environment - - - name: Build Firmwares - env: - FIRMWARE_VERSION: ${{ env.GIT_TAG_VERSION }} - run: /usr/bin/env bash build.sh build-companion-firmwares - - - name: Upload Workflow Artifacts - uses: actions/upload-artifact@v7 - with: - name: companion-firmwares - path: out - - - name: Create Release - uses: softprops/action-gh-release@v3 - if: startsWith(github.ref, 'refs/tags/') - with: - name: Companion Firmware ${{ env.GIT_TAG_VERSION }} - body: "" - draft: true - files: out/* \ No newline at end of file + build-companion-firmwares: + uses: ./.github/workflows/firmware-builder.yml + with: + firmware_type: 'companion' + release_title_prefix: 'Companion Firmware' diff --git a/.github/workflows/build-repeater-firmwares.yml b/.github/workflows/build-repeater-firmwares.yml index 3185d4b2..46b9076c 100644 --- a/.github/workflows/build-repeater-firmwares.yml +++ b/.github/workflows/build-repeater-firmwares.yml @@ -10,33 +10,8 @@ on: - 'repeater-*' jobs: - - build: - runs-on: ubuntu-latest - steps: - - - name: Clone Repo - uses: actions/checkout@v6 - - - name: Setup Build Environment - uses: ./.github/actions/setup-build-environment - - - name: Build Firmwares - env: - FIRMWARE_VERSION: ${{ env.GIT_TAG_VERSION }} - run: /usr/bin/env bash build.sh build-repeater-firmwares - - - name: Upload Workflow Artifacts - uses: actions/upload-artifact@v7 - with: - name: repeater-firmwares - path: out - - - name: Create Release - uses: softprops/action-gh-release@v3 - if: startsWith(github.ref, 'refs/tags/') - with: - name: Repeater Firmware ${{ env.GIT_TAG_VERSION }} - body: "" - draft: true - files: out/* \ No newline at end of file + build-repeater-firmwares: + uses: ./.github/workflows/firmware-builder.yml + with: + firmware_type: 'repeater' + release_title_prefix: 'Repeater Firmware' diff --git a/.github/workflows/build-room-server-firmwares.yml b/.github/workflows/build-room-server-firmwares.yml index 127095a8..c8bd19f7 100644 --- a/.github/workflows/build-room-server-firmwares.yml +++ b/.github/workflows/build-room-server-firmwares.yml @@ -10,33 +10,8 @@ on: - 'room-server-*' jobs: - - build: - runs-on: ubuntu-latest - steps: - - - name: Clone Repo - uses: actions/checkout@v6 - - - name: Setup Build Environment - uses: ./.github/actions/setup-build-environment - - - name: Build Firmwares - env: - FIRMWARE_VERSION: ${{ env.GIT_TAG_VERSION }} - run: /usr/bin/env bash build.sh build-room-server-firmwares - - - name: Upload Workflow Artifacts - uses: actions/upload-artifact@v7 - with: - name: room-server-firmwares - path: out - - - name: Create Release - uses: softprops/action-gh-release@v3 - if: startsWith(github.ref, 'refs/tags/') - with: - name: Room Server Firmware ${{ env.GIT_TAG_VERSION }} - body: "" - draft: true - files: out/* \ No newline at end of file + build-room-server-firmwares: + uses: ./.github/workflows/firmware-builder.yml + with: + firmware_type: 'room-server' + release_title_prefix: 'Room Server Firmware' diff --git a/.github/workflows/firmware-builder.yml b/.github/workflows/firmware-builder.yml new file mode 100644 index 00000000..adc9e424 --- /dev/null +++ b/.github/workflows/firmware-builder.yml @@ -0,0 +1,90 @@ +name: Firmware Builder + +on: + workflow_call: + inputs: + firmware_type: + required: true + type: string + release_title_prefix: + required: true + type: string + +jobs: + + generate-build-matrix: + runs-on: ubuntu-latest + outputs: + targets: ${{ steps.get-build-targets.outputs.targets }} + steps: + + - name: Clone Repo + uses: actions/checkout@v6 + + - name: Setup Build Environment + uses: ./.github/actions/setup-build-environment + + - name: Get Build Targets + id: get-build-targets + run: | + # get list of firmwares to build + TARGET_LIST=$(/usr/bin/env bash build.sh get-${{ inputs.firmware_type }}-firmwares-to-build) + + # convert targets separated by new line into a json array string + JSON_ARRAY=$(echo "$TARGET_LIST" | jq -R -s -c 'split("\n") | map(select(length > 0))') + + # use json array as targets result + echo "targets=$JSON_ARRAY" >> $GITHUB_OUTPUT + + build: + needs: generate-build-matrix + runs-on: ubuntu-latest + continue-on-error: true # don't fail entire build if one board fails to build + strategy: + matrix: + target: ${{ fromJson(needs.generate-build-matrix.outputs.targets) }} + fail-fast: false # don't cancel other builds if one board fails to build + steps: + + - name: Clone Repo + uses: actions/checkout@v6 + + - name: Setup Build Environment + uses: ./.github/actions/setup-build-environment + + - name: Build Firmware + env: + FIRMWARE_VERSION: ${{ env.GIT_TAG_VERSION }} + run: /usr/bin/env bash build.sh build-firmware ${{ matrix.target }} + + - name: Upload Workflow Artifacts + uses: actions/upload-artifact@v7 + with: + name: "${{ matrix.target }}" + path: out + + create-release: + needs: build + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') # only create release for tagged builds + steps: + + - name: Clone Repo + uses: actions/checkout@v6 + + - name: Setup Build Environment + uses: ./.github/actions/setup-build-environment + + - name: Download All Artifacts + uses: actions/download-artifact@v8 + with: + merge-multiple: true + path: out + + - name: Create Release + uses: softprops/action-gh-release@v3 + with: + name: "${{ inputs.release_title_prefix }} ${{ env.GIT_TAG_VERSION }}" + body: "" + draft: true + files: out/* diff --git a/build.sh b/build.sh index 313c4c47..41719a44 100755 --- a/build.sh +++ b/build.sh @@ -1,5 +1,8 @@ #!/usr/bin/env bash +# exit when any command fails +set -e + global_usage() { cat - < Date: Thu, 9 Jul 2026 11:43:20 -0700 Subject: [PATCH 11/70] Update number_allocations.md Adds a small allocation for groupdata packets for the Meshcore firmware for the StreamSensor product. It was previously a LoRaWAN platform, and we've moved to Meshcore. --- docs/number_allocations.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/number_allocations.md b/docs/number_allocations.md index 857525d4..84b9cba0 100644 --- a/docs/number_allocations.md +++ b/docs/number_allocations.md @@ -17,6 +17,7 @@ Once you have a working app/project, you need to be able to demonstrate it exist | 0000 - 00FF | -reserved for internal use- | | | 0100 | MeshCore Open | zsylvester@monitormx.com — https://github.com/zjs81/meshcore-open | | 0110 - 011F | Ripple | ripple_biz@protonmail.com — https://buymeacoffee.com/ripplebiz | +| 0120 - 012F | StreamSensor | william@housedillon.com - https://housedillon.com/blog/lora-e5-with-seeed-fusion | | FF00 - FFFF | -reserved for testing/dev- | | (add rows, inside the range 0100 - FEFF for custom apps) From d9cd3ea03a41f74a1869210b0c4330d8f92668f2 Mon Sep 17 00:00:00 2001 From: ripplebiz Date: Mon, 13 Jul 2026 19:30:11 +1000 Subject: [PATCH 12/70] Revert "Add StreamSensor allocation for GroupData" --- docs/number_allocations.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/number_allocations.md b/docs/number_allocations.md index 84b9cba0..857525d4 100644 --- a/docs/number_allocations.md +++ b/docs/number_allocations.md @@ -17,7 +17,6 @@ Once you have a working app/project, you need to be able to demonstrate it exist | 0000 - 00FF | -reserved for internal use- | | | 0100 | MeshCore Open | zsylvester@monitormx.com — https://github.com/zjs81/meshcore-open | | 0110 - 011F | Ripple | ripple_biz@protonmail.com — https://buymeacoffee.com/ripplebiz | -| 0120 - 012F | StreamSensor | william@housedillon.com - https://housedillon.com/blog/lora-e5-with-seeed-fusion | | FF00 - FFFF | -reserved for testing/dev- | | (add rows, inside the range 0100 - FEFF for custom apps) From 2af1e6d099435bd2500a87e9a2ee584a00df398e Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Wed, 15 Jul 2026 01:23:57 +1000 Subject: [PATCH 13/70] * first draft of new ConfigSerializer. (simple embedded JSON prefs) --- examples/simple_repeater/MyMesh.cpp | 1 - examples/simple_room_server/MyMesh.cpp | 1 - examples/simple_sensor/SensorMesh.cpp | 1 - src/helpers/CommonCLI.cpp | 84 ++------ src/helpers/CommonCLI.h | 122 ++++++++++- src/helpers/ConfigSerializer.cpp | 278 +++++++++++++++++++++++++ src/helpers/ConfigSerializer.h | 67 ++++++ 7 files changed, 485 insertions(+), 69 deletions(-) create mode 100644 src/helpers/ConfigSerializer.cpp create mode 100644 src/helpers/ConfigSerializer.h diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index b66e1952..09f74cbe 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -872,7 +872,6 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc #endif // defaults - memset(&_prefs, 0, sizeof(_prefs)); _prefs.airtime_factor = 1.0; _prefs.rx_delay_base = 0.0f; // turn off by default, was 10.0; _prefs.tx_delay_factor = 0.5f; // was 0.25f diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 36978e80..1a778d4a 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -628,7 +628,6 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc recv_pkt_region = NULL; // defaults - memset(&_prefs, 0, sizeof(_prefs)); _prefs.airtime_factor = 1.0; _prefs.rx_delay_base = 0.0f; // off by default, was 10.0 _prefs.tx_delay_factor = 0.5f; // was 0.25f; diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index 59c9aa09..17f8e323 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -710,7 +710,6 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise set_radio_at = revert_radio_at = 0; // defaults - memset(&_prefs, 0, sizeof(_prefs)); _prefs.airtime_factor = 1.0; _prefs.rx_delay_base = 0.0f; // turn off by default, was 10.0; _prefs.tx_delay_factor = 0.5f; // was 0.25f diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index c95e3e34..56a46d46 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -28,16 +28,21 @@ static bool isValidName(const char *n) { } void CommonCLI::loadPrefs(FILESYSTEM* fs) { - if (fs->exists("/com_prefs")) { - loadPrefsInt(fs, "/com_prefs"); // new filename - } else if (fs->exists("/node_prefs")) { - loadPrefsInt(fs, "/node_prefs"); - savePrefs(fs); // save to new filename - fs->remove("/node_prefs"); // remove old + if (fs->exists("/ser_prefs")) { + File file = fs->open("/ser_prefs"); + if (file) { + _prefs->loadSerial(file); // new Serial prefs + file.close(); + } + } else if (fs->exists("/com_prefs")) { + loadPrefsInt(fs, "/com_prefs"); + if (savePrefs(fs)) { // save to new Serial prefs + // fs->remove("/com_prefs"); // remove old + } } } -void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { +void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { // Legacy prefs loader #if defined(RP2040_PLATFORM) File file = fs->open(filename, "r"); #else @@ -130,70 +135,21 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { } } -void CommonCLI::savePrefs(FILESYSTEM* fs) { +bool CommonCLI::savePrefs(FILESYSTEM* fs) { #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) - fs->remove("/com_prefs"); - File file = fs->open("/com_prefs", FILE_O_WRITE); + fs->remove("/ser_prefs"); + File file = fs->open("/ser_prefs", FILE_O_WRITE); #elif defined(RP2040_PLATFORM) - File file = fs->open("/com_prefs", "w"); + File file = fs->open("/ser_prefs", "w"); #else - File file = fs->open("/com_prefs", "w", true); + File file = fs->open("/ser_prefs", "w", true); #endif if (file) { - uint8_t pad[8]; - memset(pad, 0, sizeof(pad)); - - file.write((uint8_t *)&_prefs->airtime_factor, sizeof(_prefs->airtime_factor)); // 0 - file.write((uint8_t *)&_prefs->node_name, sizeof(_prefs->node_name)); // 4 - file.write(pad, 4); // 36 - file.write((uint8_t *)&_prefs->node_lat, sizeof(_prefs->node_lat)); // 40 - file.write((uint8_t *)&_prefs->node_lon, sizeof(_prefs->node_lon)); // 48 - file.write((uint8_t *)&_prefs->password[0], sizeof(_prefs->password)); // 56 - file.write((uint8_t *)&_prefs->freq, sizeof(_prefs->freq)); // 72 - file.write((uint8_t *)&_prefs->tx_power_dbm, sizeof(_prefs->tx_power_dbm)); // 76 - file.write((uint8_t *)&_prefs->disable_fwd, sizeof(_prefs->disable_fwd)); // 77 - file.write((uint8_t *)&_prefs->advert_interval, sizeof(_prefs->advert_interval)); // 78 - file.write(pad, 1); // 79 : 1 byte unused (rx_boosted_gain moved to end) - file.write((uint8_t *)&_prefs->rx_delay_base, sizeof(_prefs->rx_delay_base)); // 80 - file.write((uint8_t *)&_prefs->tx_delay_factor, sizeof(_prefs->tx_delay_factor)); // 84 - file.write((uint8_t *)&_prefs->guest_password[0], sizeof(_prefs->guest_password)); // 88 - file.write((uint8_t *)&_prefs->direct_tx_delay_factor, sizeof(_prefs->direct_tx_delay_factor)); // 104 - file.write(pad, 4); // 108 : 4 byte unused - file.write((uint8_t *)&_prefs->sf, sizeof(_prefs->sf)); // 112 - file.write((uint8_t *)&_prefs->cr, sizeof(_prefs->cr)); // 113 - file.write((uint8_t *)&_prefs->allow_read_only, sizeof(_prefs->allow_read_only)); // 114 - file.write((uint8_t *)&_prefs->multi_acks, sizeof(_prefs->multi_acks)); // 115 - file.write((uint8_t *)&_prefs->bw, sizeof(_prefs->bw)); // 116 - file.write((uint8_t *)&_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval)); // 120 - file.write((uint8_t *)&_prefs->path_hash_mode, sizeof(_prefs->path_hash_mode)); // 121 - file.write((uint8_t *)&_prefs->loop_detect, sizeof(_prefs->loop_detect)); // 122 - file.write(pad, 1); // 123 - file.write((uint8_t *)&_prefs->flood_max, sizeof(_prefs->flood_max)); // 124 - file.write((uint8_t *)&_prefs->flood_advert_interval, sizeof(_prefs->flood_advert_interval)); // 125 - file.write((uint8_t *)&_prefs->interference_threshold, sizeof(_prefs->interference_threshold)); // 126 - file.write((uint8_t *)&_prefs->bridge_enabled, sizeof(_prefs->bridge_enabled)); // 127 - file.write((uint8_t *)&_prefs->bridge_delay, sizeof(_prefs->bridge_delay)); // 128 - file.write((uint8_t *)&_prefs->bridge_pkt_src, sizeof(_prefs->bridge_pkt_src)); // 130 - file.write((uint8_t *)&_prefs->bridge_baud, sizeof(_prefs->bridge_baud)); // 131 - file.write((uint8_t *)&_prefs->bridge_channel, sizeof(_prefs->bridge_channel)); // 135 - file.write((uint8_t *)&_prefs->bridge_secret, sizeof(_prefs->bridge_secret)); // 136 - file.write((uint8_t *)&_prefs->powersaving_enabled, sizeof(_prefs->powersaving_enabled)); // 152 - file.write(pad, 3); // 153 - file.write((uint8_t *)&_prefs->gps_enabled, sizeof(_prefs->gps_enabled)); // 156 - file.write((uint8_t *)&_prefs->gps_interval, sizeof(_prefs->gps_interval)); // 157 - file.write((uint8_t *)&_prefs->advert_loc_policy, sizeof(_prefs->advert_loc_policy)); // 161 - file.write((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162 - file.write((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166 - file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170 - file.write((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290 - file.write((uint8_t *)&_prefs->flood_max_unscoped, sizeof(_prefs->flood_max_unscoped)); // 291 - file.write((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 292 - file.write((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 293 - file.write((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 294 - // next: 295 - + bool success = _prefs->saveSerial(file); file.close(); + return success; } + return false; } #define MIN_LOCAL_ADVERT_INTERVAL 60 diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index f3abcf47..aab33d11 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -5,6 +5,7 @@ #include #include #include +#include #if defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE) #define WITH_BRIDGE @@ -19,7 +20,9 @@ #define LOOP_DETECT_MODERATE 2 #define LOOP_DETECT_STRICT 3 -struct NodePrefs { // persisted to file +class NodePrefs : public ConfigSerializer { +public: + // in-memory backing data float airtime_factor; char node_name[32]; double node_lat, node_lon; @@ -65,6 +68,121 @@ struct NodePrefs { // persisted to file uint8_t path_hash_mode; // which path mode to use when sending uint8_t loop_detect; uint8_t cad_enabled; // hardware Channel Activity Detection before TX (boolean) + +private: + class RadioPrefs : public ConfigSerializer { + NodePrefs* _parent; + protected: + void structure() override { + def("freq", _parent->freq); + def("bw", _parent->bw); + def("sf", _parent->sf); + def("cr", _parent->cr); + def("cad", _parent->cad_enabled); + def("int_thr", _parent->interference_threshold); + def("rxgain", _parent->rx_boosted_gain); + def("fem_rxgain", _parent->rx_boosted_gain); + def("tx", _parent->tx_power_dbm); + def("af", _parent->airtime_factor); + def("rxdelay", _parent->rx_delay_base); + def("f_txdelay", _parent->tx_delay_factor); + def("d_txdelay", _parent->direct_tx_delay_factor); + def("agc_int", _parent->agc_reset_interval); + def("hash_mode", _parent->path_hash_mode); + def("multi_ack", _parent->multi_acks); + } + public: + RadioPrefs(NodePrefs* parent) : _parent(parent) { } + }; + RadioPrefs radio; + + class BridgePrefs : public ConfigSerializer { + NodePrefs* _parent; + protected: + void structure() override { + def("en", _parent->bridge_enabled); // boolean + def("delay", _parent->bridge_delay); // milliseconds (default 500 ms) + def("src", _parent->bridge_pkt_src); // 0 = logTx, 1 = logRx (default logTx) + def("baud", _parent->bridge_baud); // 9600, 19200, 38400, 57600, 115200 (default 115200) + def("ch", _parent->bridge_channel); // 1-14 (ESP-NOW only) + def("secret", _parent->bridge_secret, sizeof(_parent->bridge_secret)); // for XOR encryption of bridge packets (ESP-NOW only) + } + public: + BridgePrefs(NodePrefs* parent) : _parent(parent) { } + }; + BridgePrefs bridge; + + class GPSPrefs : public ConfigSerializer { + NodePrefs* _parent; + protected: + void structure() override { + def("en", _parent->gps_enabled); // boolean + def("int", _parent->gps_interval); // interval in seconds + def("adv_loc", _parent->advert_loc_policy); + } + public: + GPSPrefs(NodePrefs* parent) : _parent(parent) { } + }; + GPSPrefs gps; + + class PowerPrefs : public ConfigSerializer { + NodePrefs* _parent; + protected: + void structure() override { + def("adc_mult", _parent->adc_multiplier); + def("pwr_sav_en", _parent->powersaving_enabled); + } + public: + PowerPrefs(NodePrefs* parent) : _parent(parent) { } + }; + PowerPrefs power; + + class RepeatPrefs : public ConfigSerializer { + NodePrefs* _parent; + protected: + void structure() override { + def("disable", _parent->disable_fwd); + def("f_max", _parent->flood_max); + def("f_max_uns", _parent->flood_max_unscoped); + def("f_max_adv", _parent->flood_max_advert); + def("loop", _parent->loop_detect); + } + public: + RepeatPrefs(NodePrefs* parent) : _parent(parent) { } + }; + RepeatPrefs repeat; + + class RoomPrefs : public ConfigSerializer { + NodePrefs* _parent; + protected: + void structure() override { + def("rd_only", _parent->allow_read_only); + } + public: + RoomPrefs(NodePrefs* parent) : _parent(parent) { } + }; + RoomPrefs room; + +protected: + void structure() override { + def("name", node_name, sizeof(node_name)); + def("pass", password, sizeof(password)); + def("guest", guest_password, sizeof(guest_password)); + def("owner", owner_info, sizeof(owner_info)); + def("adv_int", advert_interval); + def("f_adv_int", flood_advert_interval); + def("lat", node_lat); + def("lon", node_lon); + def("radio", radio); + def("bridge", bridge); + def("gps", gps); + def("repeat", repeat); + def("room", room); + def("power", power); + } + +public: + NodePrefs() : ConfigSerializer(), bridge(this), gps(this), radio(this), power(this), repeat(this), room(this) { } }; class CommonCLICallbacks { @@ -139,7 +257,7 @@ public: : _board(&board), _rtc(&rtc), _sensors(&sensors), _region_map(®ion_map), _acl(&acl), _prefs(prefs), _callbacks(callbacks) { } void loadPrefs(FILESYSTEM* _fs); - void savePrefs(FILESYSTEM* _fs); + bool savePrefs(FILESYSTEM* _fs); void handleCommand(uint32_t sender_timestamp, char* command, char* reply); uint8_t buildAdvertData(uint8_t node_type, uint8_t* app_data); }; diff --git a/src/helpers/ConfigSerializer.cpp b/src/helpers/ConfigSerializer.cpp new file mode 100644 index 00000000..221fd197 --- /dev/null +++ b/src/helpers/ConfigSerializer.cpp @@ -0,0 +1,278 @@ +#include "ConfigSerializer.h" + +bool ConfigSerializer::saveSerial(Stream& s) { + Context context(&s, OP::WRITE); + _context = &context; // set the context for structure() call + s.print("{"); // root object + _first = true; + structure(); + if (s.print("}") != 1) context.success = false; // failure detect + _context = NULL; + return context.success; +} + +#define TOK_ERROR -1 +#define TOK_EOF 0 +#define TOK_KEY 1 +#define TOK_VALUE 2 +#define TOK_START_OBJ 3 +#define TOK_END_OBJ 4 +#define TOK_WHITESPACE 5 + +static bool is_whitespace(char c) { + return c == ' ' || c == '\t' || c == '\r' || c == '\n'; +} +static bool is_key_char(char c) { + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_'; +} +static bool is_value_char(char c) { + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || c == '-' || c == '.'; +} + +#define EXPECT_OPEN_BRACE 0 +#define EXPECT_KEY 1 +#define EXPECT_VAL_OR_OBJ 2 +#define EXPECT_STRING_VAL 3 +#define EXPECT_STRING_ESCAPE 4 +#define EXPECT_COMMA_OR_CLOSE 5 +#define EXPECT_COMMA_OR_KEY 6 +#define EXPECT_COMMA_OR_KEY_OR_CLOSE 7 + +int ConfigSerializer::Context::readNext() { + char c; + if (pending) { + c = pending; + pending = 0; + } else { + if (_f->available() == 0) return TOK_EOF; + + int n = _f->read(); + if (n < 0) return TOK_EOF; + c = (char)n; + } + + switch (rd_mode) { + case EXPECT_OPEN_BRACE: + if (c == '{') { rd_mode = EXPECT_KEY; return TOK_START_OBJ; } + if (is_whitespace(c)) return TOK_WHITESPACE; + return TOK_ERROR; + + case EXPECT_COMMA_OR_KEY_OR_CLOSE: + if (c == '}') { rd_mode = EXPECT_COMMA_OR_KEY_OR_CLOSE; return TOK_END_OBJ; } + case EXPECT_COMMA_OR_KEY: + if (c == ',') { rd_mode = EXPECT_KEY; return TOK_WHITESPACE; } + case EXPECT_KEY: + if (rd_len > 0 && c == ':') { rd_buf[rd_len] = 0; rd_len = 0; rd_mode = EXPECT_VAL_OR_OBJ; return TOK_KEY; } + if (rd_len == 0 && is_whitespace(c)) return TOK_WHITESPACE; + if (rd_len < CONFIG_MAX_KEYLEN-1 && is_key_char(c)) { rd_buf[rd_len++] = c; return TOK_WHITESPACE; } + return TOK_ERROR; + + case EXPECT_VAL_OR_OBJ: + if (rd_len == 0 && is_whitespace(c)) return TOK_WHITESPACE; + if (rd_len == 0 && c == '"') { rd_mode = EXPECT_STRING_VAL; return TOK_WHITESPACE; } + if (rd_len == 0 && c == '{') { rd_mode = EXPECT_KEY; return TOK_START_OBJ; } + if (is_value_char(c) && rd_len < CONFIG_MAX_TOKEN_LEN-1) { rd_buf[rd_len++] = c; return TOK_WHITESPACE; } + if (rd_len > 0 && (c == ',' || c == '}' || is_whitespace(c))) { pending = c; rd_buf[rd_len] = 0; rd_len = 0; rd_mode = EXPECT_COMMA_OR_CLOSE; return TOK_VALUE; } + return TOK_ERROR; + + case EXPECT_STRING_ESCAPE: + if ((c == '"' || c == '\\' || c == '/') && rd_len < CONFIG_MAX_TOKEN_LEN-1) { rd_buf[rd_len++] = c; rd_mode = EXPECT_STRING_VAL; return TOK_WHITESPACE; } + return TOK_ERROR; // unsupport escape + + case EXPECT_STRING_VAL: + if (c == '"') { rd_buf[rd_len] = 0; rd_len = 0; rd_mode = EXPECT_COMMA_OR_CLOSE; return TOK_VALUE; } + if (c == '\\') { rd_mode = EXPECT_STRING_ESCAPE; return TOK_WHITESPACE; } + if (rd_len < CONFIG_MAX_TOKEN_LEN-1) { rd_buf[rd_len++] = c; return TOK_WHITESPACE; } + return TOK_ERROR; + + case EXPECT_COMMA_OR_CLOSE: + if (c == ',') { rd_mode = EXPECT_KEY; return TOK_WHITESPACE; } + if (c == '}') { rd_mode = EXPECT_COMMA_OR_KEY_OR_CLOSE; return TOK_END_OBJ; } + if (is_whitespace(c)) return TOK_WHITESPACE; + return TOK_ERROR; + } + return TOK_ERROR; // unknown mode +} + +bool ConfigSerializer::loadSerial(Stream& s) { + Context context(&s, OP::READ); + _context = &context; // set the context for structure() call + // parse the Json file + uint8_t sp = 0; + int next_tok; + + while ((next_tok = context.readNext()) > TOK_EOF) { + if (next_tok == TOK_KEY) { + context.setKey(sp, context.getToken()); + } else if (next_tok == TOK_VALUE) { + _depth = 1; // re-run the structure() hierarchy again (looking for specific key, at specific depth) + structure(); + } else if (next_tok == TOK_START_OBJ) { + if (sp < CONFIF_MAX_DEPTH - 1) { + sp++; + } else { + Serial.printf("Error: max nesting reached"); // TODO: debug logging + context.success = false; + break; + } + } else if (next_tok == TOK_END_OBJ) { + if (sp > 0) { + sp--; + } else { + Serial.printf("Error: too many closing '}'"); // TODO: debug logging + context.success = false; + break; + } + } + } + if (sp != 0 || next_tok == TOK_ERROR) { + context.success = false; // unmatched { }, or other parse error + } + _context = NULL; + return context.success; +} + +void ConfigSerializer::writeComma() { + if (_first) { + _first = false; + } else { + _context->file()->print(","); // comma separated properties + } +} + +void ConfigSerializer::def(const char* key, char* value, size_t max_len) { + if (_context->op() == OP::WRITE) { + writeComma(); + _context->file()->print(key); + _context->file()->print(":\""); + _context->file()->print(value); // TODO: escape quotes + _context->file()->print("\""); + } else { + if (_context->keyMatch(_depth, key)) { + strncpy(value, _context->getToken(), max_len - 1); + value[max_len - 1] = 0; + } + } +} + +void ConfigSerializer::def(const char* key, int32_t& value) { + if (_context->op() == OP::WRITE) { + writeComma(); + _context->file()->print(key); + _context->file()->print(":"); + _context->file()->print(value); + } else { + if (_context->keyMatch(_depth, key)) { + value = atol(_context->getToken()); + } + } +} + +void ConfigSerializer::def(const char* key, uint32_t& value) { + if (_context->op() == OP::WRITE) { + writeComma(); + _context->file()->print(key); + _context->file()->print(":"); + _context->file()->print(value); + } else { + if (_context->keyMatch(_depth, key)) { + value = atol(_context->getToken()); + } + } +} + +void ConfigSerializer::def(const char* key, int16_t& value) { + if (_context->op() == OP::WRITE) { + writeComma(); + _context->file()->print(key); + _context->file()->print(":"); + _context->file()->print((int32_t) value, 10); + } else { + if (_context->keyMatch(_depth, key)) { + value = atol(_context->getToken()); + } + } +} + +void ConfigSerializer::def(const char* key, uint16_t& value) { + if (_context->op() == OP::WRITE) { + writeComma(); + _context->file()->print(key); + _context->file()->print(":"); + _context->file()->print((uint32_t) value, 10); + } else { + if (_context->keyMatch(_depth, key)) { + value = atoi(_context->getToken()); + } + } +} + +void ConfigSerializer::def(const char* key, uint8_t& value) { + if (_context->op() == OP::WRITE) { + writeComma(); + _context->file()->print(key); + _context->file()->print(":"); + _context->file()->print((uint32_t) value, 10); + } else { + if (_context->keyMatch(_depth, key)) { + value = atoi(_context->getToken()); + } + } +} + +void ConfigSerializer::def(const char* key, int8_t& value) { + if (_context->op() == OP::WRITE) { + writeComma(); + _context->file()->print(key); + _context->file()->print(":"); + _context->file()->print((int32_t) value, 10); + } else { + if (_context->keyMatch(_depth, key)) { + value = atoi(_context->getToken()); + } + } +} + +void ConfigSerializer::def(const char* key, double& value) { + if (_context->op() == OP::WRITE) { + writeComma(); + _context->file()->print(key); + _context->file()->print(":"); + _context->file()->print(value, 6); // REVISIT: how many dec places? + } else { + if (_context->keyMatch(_depth, key)) { + value = atof(_context->getToken()); + } + } +} + +void ConfigSerializer::def(const char* key, float& value) { + if (_context->op() == OP::WRITE) { + writeComma(); + _context->file()->print(key); + _context->file()->print(":"); + _context->file()->print(value, 4); // REVISIT: how many dec places? + } else { + if (_context->keyMatch(_depth, key)) { + value = (float) atof(_context->getToken()); + } + } +} + +void ConfigSerializer::def(const char* key, ConfigSerializer& sub_obj) { + if (_context->op() == OP::WRITE) { + writeComma(); + _context->file()->print(key); + _context->file()->print(":{"); + sub_obj._context = _context; // inherit the Context + sub_obj._first = true; + sub_obj.structure(); // recurse into sub object + if (_context->file()->print("}") != 1) _context->success = false; // failure detect + } else { + if (_context->keyMatch(_depth, key)) { + sub_obj._context = _context; // inherit the Context + sub_obj._depth = _depth + 1; + sub_obj.structure(); // recurse into sub object + } + } +} diff --git a/src/helpers/ConfigSerializer.h b/src/helpers/ConfigSerializer.h new file mode 100644 index 00000000..2f1d2ae1 --- /dev/null +++ b/src/helpers/ConfigSerializer.h @@ -0,0 +1,67 @@ +#pragma once + +#include + +#ifndef CONFIF_MAX_DEPTH + #define CONFIF_MAX_DEPTH 8 +#endif + +#ifndef CONFIG_MAX_KEYLEN + #define CONFIG_MAX_KEYLEN 16 +#endif + +#ifndef CONFIG_MAX_TOKEN_LEN + #define CONFIG_MAX_TOKEN_LEN 128 +#endif + +class ConfigSerializer { + bool _first; + int8_t _depth; + + enum OP { READ, WRITE }; + + class Context { + Stream* _f; + OP _op; + uint8_t rd_len; + uint8_t rd_mode; + char pending; + char rd_buf[CONFIG_MAX_TOKEN_LEN]; + char _keys[CONFIF_MAX_DEPTH][CONFIG_MAX_KEYLEN]; + + public: + bool success = true; + Context(Stream* f, OP op) : _f(f), _op(op) { rd_buf[rd_len = 0] = 0; rd_mode = 0; pending = 0; } + OP op() const { return _op; } + Stream* file() const { return _f; } + int readNext(); + const char* getToken() const { return rd_buf; } + bool keyMatch(int8_t depth, const char* key) { return strcmp(key, _keys[depth]) == 0; } + void setKey(uint8_t depth, const char* key) { strcpy(_keys[depth], key); } + }; + + Context* _context = NULL; + + void writeComma(); + +protected: + ConfigSerializer() { } + + void def(const char* key, char* value, size_t max_len); // max_len inclusive of null + void def(const char* key, int32_t& value); + void def(const char* key, int16_t& value); + void def(const char* key, int8_t& value); + void def(const char* key, uint32_t& value); + void def(const char* key, uint16_t& value); + void def(const char* key, uint8_t& value); + void def(const char* key, float& value); + void def(const char* key, double& value); + void def(const char* key, bool& value); + void def(const char* key, ConfigSerializer& sub_obj); + + virtual void structure() = 0; + +public: + bool loadSerial(Stream& s); + bool saveSerial(Stream& s); +}; From 63f4b173daa9371f296facd03e2854df5ed85f0d Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Wed, 15 Jul 2026 13:21:44 +1000 Subject: [PATCH 14/70] * misc fixes --- src/helpers/ConfigSerializer.cpp | 46 +++++++++++++++++++++++++++----- src/helpers/ConfigSerializer.h | 6 ++--- 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/src/helpers/ConfigSerializer.cpp b/src/helpers/ConfigSerializer.cpp index 221fd197..76673309 100644 --- a/src/helpers/ConfigSerializer.cpp +++ b/src/helpers/ConfigSerializer.cpp @@ -97,10 +97,10 @@ int ConfigSerializer::Context::readNext() { bool ConfigSerializer::loadSerial(Stream& s) { Context context(&s, OP::READ); _context = &context; // set the context for structure() call - // parse the Json file - uint8_t sp = 0; + uint8_t sp = 0; // object nesting stack pointer int next_tok; + // parse the Json file while ((next_tok = context.readNext()) > TOK_EOF) { if (next_tok == TOK_KEY) { context.setKey(sp, context.getToken()); @@ -108,7 +108,7 @@ bool ConfigSerializer::loadSerial(Stream& s) { _depth = 1; // re-run the structure() hierarchy again (looking for specific key, at specific depth) structure(); } else if (next_tok == TOK_START_OBJ) { - if (sp < CONFIF_MAX_DEPTH - 1) { + if (sp < CONFIG_MAX_DEPTH - 1) { sp++; } else { Serial.printf("Error: max nesting reached"); // TODO: debug logging @@ -145,7 +145,20 @@ void ConfigSerializer::def(const char* key, char* value, size_t max_len) { writeComma(); _context->file()->print(key); _context->file()->print(":\""); - _context->file()->print(value); // TODO: escape quotes + char c; + while ((c = *value++) != 0) { // TODO: handle UTF-8 encoding + if (c == '"') { + _context->file()->print("\\\""); + } else if (c == '\\') { + _context->file()->print("\\\\"); + } else if (c == '\n') { + _context->file()->print("\\n"); + } else if (c == '\r') { + _context->file()->print("\\r"); + } else { + _context->file()->print(c); + } + } _context->file()->print("\""); } else { if (_context->keyMatch(_depth, key)) { @@ -233,12 +246,29 @@ void ConfigSerializer::def(const char* key, int8_t& value) { } } +void ConfigSerializer::def(const char* key, bool& value) { + if (_context->op() == OP::WRITE) { + writeComma(); + _context->file()->print(key); + _context->file()->print(":"); + _context->file()->print(value ? "true" : "false"); + } else { + if (_context->keyMatch(_depth, key)) { + value = strcmp(_context->getToken(), "true") == 0 || atoi(_context->getToken()) != 0; // 'true' or a non-zero number + } + } +} + void ConfigSerializer::def(const char* key, double& value) { if (_context->op() == OP::WRITE) { writeComma(); _context->file()->print(key); _context->file()->print(":"); - _context->file()->print(value, 6); // REVISIT: how many dec places? + if (value == 0.0) { + _context->file()->print("0"); // shorter encoding + } else { + _context->file()->print(value, 6); // REVISIT: how many dec places? + } } else { if (_context->keyMatch(_depth, key)) { value = atof(_context->getToken()); @@ -251,7 +281,11 @@ void ConfigSerializer::def(const char* key, float& value) { writeComma(); _context->file()->print(key); _context->file()->print(":"); - _context->file()->print(value, 4); // REVISIT: how many dec places? + if (value == 0.0f) { + _context->file()->print("0"); // shorter encoding + } else { + _context->file()->print(value, 4); // REVISIT: how many dec places? + } } else { if (_context->keyMatch(_depth, key)) { value = (float) atof(_context->getToken()); diff --git a/src/helpers/ConfigSerializer.h b/src/helpers/ConfigSerializer.h index 2f1d2ae1..5f0dc488 100644 --- a/src/helpers/ConfigSerializer.h +++ b/src/helpers/ConfigSerializer.h @@ -2,8 +2,8 @@ #include -#ifndef CONFIF_MAX_DEPTH - #define CONFIF_MAX_DEPTH 8 +#ifndef CONFIG_MAX_DEPTH + #define CONFIG_MAX_DEPTH 8 #endif #ifndef CONFIG_MAX_KEYLEN @@ -27,7 +27,7 @@ class ConfigSerializer { uint8_t rd_mode; char pending; char rd_buf[CONFIG_MAX_TOKEN_LEN]; - char _keys[CONFIF_MAX_DEPTH][CONFIG_MAX_KEYLEN]; + char _keys[CONFIG_MAX_DEPTH][CONFIG_MAX_KEYLEN]; public: bool success = true; From f515716032e61adbfa4e55178caac5478e9aa89c Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Fri, 17 Jul 2026 16:05:10 +1000 Subject: [PATCH 15/70] * new NodePrefs for companion --- examples/companion_radio/DataStore.cpp | 70 +++++++----------- examples/companion_radio/DataStore.h | 6 +- examples/companion_radio/MyMesh.cpp | 13 ++-- examples/companion_radio/MyMesh.h | 6 +- examples/companion_radio/NodePrefs.h | 99 +++++++++++++++++++++++++- src/helpers/ConfigSerializer.cpp | 17 +++++ src/helpers/ConfigSerializer.h | 1 + 7 files changed, 155 insertions(+), 57 deletions(-) diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index 0f3a0f9c..06c56a7a 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -189,17 +189,22 @@ bool DataStore::saveMainIdentity(const mesh::LocalIdentity &identity) { return identity_store.save("_main", identity); } -void DataStore::loadPrefs(NodePrefs& prefs, double& node_lat, double& node_lon) { - if (_fs->exists("/new_prefs")) { - loadPrefsInt("/new_prefs", prefs, node_lat, node_lon); // new filename - } else if (_fs->exists("/node_prefs")) { - loadPrefsInt("/node_prefs", prefs, node_lat, node_lon); - savePrefs(prefs, node_lat, node_lon); // save to new filename - _fs->remove("/node_prefs"); // remove old +void DataStore::loadPrefs(NodePrefs& prefs) { + if (_fs->exists("/prefs.json")) { + File file = openRead(_fs, "/prefs.json"); + if (file) { + prefs.loadSerial(file); // new Serial prefs + file.close(); + } + } else if (_fs->exists("/new_prefs")) { + loadPrefsInt("/new_prefs", prefs); + if (savePrefs(prefs) ) { // save to new format + //_fs->remove("/new_prefs"); // remove old + } } } -void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& node_lat, double& node_lon) { +void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs) { File file = openRead(_fs, filename); if (file) { uint8_t pad[8]; @@ -207,12 +212,12 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no file.read((uint8_t *)&_prefs.airtime_factor, sizeof(float)); // 0 file.read((uint8_t *)_prefs.node_name, sizeof(_prefs.node_name)); // 4 file.read(pad, 4); // 36 - file.read((uint8_t *)&node_lat, sizeof(node_lat)); // 40 - file.read((uint8_t *)&node_lon, sizeof(node_lon)); // 48 + file.read((uint8_t *)&_prefs.node_lat, sizeof(_prefs.node_lat)); // 40 + file.read((uint8_t *)&_prefs.node_lon, sizeof(_prefs.node_lon)); // 48 file.read((uint8_t *)&_prefs.freq, sizeof(_prefs.freq)); // 56 file.read((uint8_t *)&_prefs.sf, sizeof(_prefs.sf)); // 60 file.read((uint8_t *)&_prefs.cr, sizeof(_prefs.cr)); // 61 - file.read((uint8_t *)&_prefs.client_repeat, sizeof(_prefs.client_repeat)); // 62 + file.read((uint8_t *)&_prefs._client_repeat, sizeof(_prefs._client_repeat)); // 62 file.read((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63 file.read((uint8_t *)&_prefs.bw, sizeof(_prefs.bw)); // 64 file.read((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68 @@ -234,48 +239,21 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no file.read((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 90 file.read((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 121 + // migrate old fields + _prefs.setRepeatEn(_prefs._client_repeat != 0); + file.close(); } } -void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_lon) { - File file = openWrite(_fs, "/new_prefs"); +bool DataStore::savePrefs(NodePrefs& _prefs) { + File file = openWrite(_fs, "/prefs.json"); if (file) { - uint8_t pad[8]; - memset(pad, 0, sizeof(pad)); - - file.write((uint8_t *)&_prefs.airtime_factor, sizeof(float)); // 0 - file.write((uint8_t *)_prefs.node_name, sizeof(_prefs.node_name)); // 4 - file.write(pad, 4); // 36 - file.write((uint8_t *)&node_lat, sizeof(node_lat)); // 40 - file.write((uint8_t *)&node_lon, sizeof(node_lon)); // 48 - file.write((uint8_t *)&_prefs.freq, sizeof(_prefs.freq)); // 56 - file.write((uint8_t *)&_prefs.sf, sizeof(_prefs.sf)); // 60 - file.write((uint8_t *)&_prefs.cr, sizeof(_prefs.cr)); // 61 - file.write((uint8_t *)&_prefs.client_repeat, sizeof(_prefs.client_repeat)); // 62 - file.write((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63 - file.write((uint8_t *)&_prefs.bw, sizeof(_prefs.bw)); // 64 - file.write((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68 - file.write((uint8_t *)&_prefs.telemetry_mode_base, sizeof(_prefs.telemetry_mode_base)); // 69 - file.write((uint8_t *)&_prefs.telemetry_mode_loc, sizeof(_prefs.telemetry_mode_loc)); // 70 - file.write((uint8_t *)&_prefs.telemetry_mode_env, sizeof(_prefs.telemetry_mode_env)); // 71 - file.write((uint8_t *)&_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base)); // 72 - file.write((uint8_t *)&_prefs.advert_loc_policy, sizeof(_prefs.advert_loc_policy)); // 76 - file.write((uint8_t *)&_prefs.multi_acks, sizeof(_prefs.multi_acks)); // 77 - file.write((uint8_t *)&_prefs.path_hash_mode, sizeof(_prefs.path_hash_mode)); // 78 - file.write(pad, 1); // 79 - file.write((uint8_t *)&_prefs.ble_pin, sizeof(_prefs.ble_pin)); // 80 - file.write((uint8_t *)&_prefs.buzzer_quiet, sizeof(_prefs.buzzer_quiet)); // 84 - file.write((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled)); // 85 - file.write((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval)); // 86 - file.write((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); // 87 - file.write((uint8_t *)&_prefs.autoadd_max_hops, sizeof(_prefs.autoadd_max_hops)); // 88 - file.write((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); // 89 - file.write((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 90 - file.write((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 121 - + bool success = _prefs.saveSerial(file); file.close(); + return success; } + return false; } void DataStore::loadContacts(DataStoreHost* host) { diff --git a/examples/companion_radio/DataStore.h b/examples/companion_radio/DataStore.h index af5ee7af..e0a145ca 100644 --- a/examples/companion_radio/DataStore.h +++ b/examples/companion_radio/DataStore.h @@ -19,7 +19,7 @@ class DataStore { mesh::RTCClock* _clock; IdentityStore identity_store; - void loadPrefsInt(const char *filename, NodePrefs& prefs, double& node_lat, double& node_lon); + void loadPrefsInt(const char *filename, NodePrefs& prefs); #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) void checkAdvBlobFile(); #endif @@ -33,8 +33,8 @@ public: FILESYSTEM* getSecondaryFS() const { return _fsExtra; } bool loadMainIdentity(mesh::LocalIdentity &identity); bool saveMainIdentity(const mesh::LocalIdentity &identity); - void loadPrefs(NodePrefs& prefs, double& node_lat, double& node_lon); - void savePrefs(const NodePrefs& prefs, double node_lat, double node_lon); + void loadPrefs(NodePrefs& prefs); + bool savePrefs(NodePrefs& prefs); void loadContacts(DataStoreHost* host); void saveContacts(DataStoreHost* host, bool (*filter)(const ContactInfo& c) = NULL); void loadChannels(DataStoreHost* host); diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index a78fd29a..1423d1ff 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -483,7 +483,7 @@ bool MyMesh::filterRecvFloodPacket(mesh::Packet* packet) { } bool MyMesh::allowPacketForward(const mesh::Packet* packet) { - return _prefs.client_repeat != 0; + return _prefs.isRepeatEn(); } void MyMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis) { @@ -876,7 +876,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe send_unscoped = false; // defaults - memset(&_prefs, 0, sizeof(_prefs)); + //memset(&_prefs, 0, sizeof(_prefs)); _prefs.airtime_factor = 1.0; strcpy(_prefs.node_name, "NONAME"); _prefs.freq = LORA_FREQ; @@ -887,6 +887,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _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 + _prefs.setRepeatEn(false); #if defined(USE_SX1262) || defined(USE_SX1268) #ifdef SX126X_RX_BOOSTED_GAIN _prefs.rx_boosted_gain = SX126X_RX_BOOSTED_GAIN; @@ -931,7 +932,9 @@ void MyMesh::begin(bool has_display) { #endif // load persisted prefs - _store->loadPrefs(_prefs, sensors.node_lat, sensors.node_lon); + _store->loadPrefs(_prefs); + sensors.node_lat = _prefs.node_lat; + sensors.node_lon = _prefs.node_lon; // sanitise bad pref values _prefs.rx_delay_base = constrain(_prefs.rx_delay_base, 0, 20.0f); @@ -1031,7 +1034,7 @@ void MyMesh::handleCmdFrame(size_t len) { i += 40; StrHelper::strzcpy((char *)&out_frame[i], FIRMWARE_VERSION, 20); i += 20; - out_frame[i++] = _prefs.client_repeat; // v9+ + out_frame[i++] = _prefs.isRepeatEn() ? 1 : 0; // v9+ out_frame[i++] = _prefs.path_hash_mode; // v10+ _serial->writeFrame(out_frame, i); } else if (cmd_frame[0] == CMD_APP_START && @@ -1393,7 +1396,7 @@ void MyMesh::handleCmdFrame(size_t len) { _prefs.cr = cr; _prefs.freq = (float)freq / 1000.0; _prefs.bw = (float)bw / 1000.0; - _prefs.client_repeat = repeat; + _prefs.setRepeatEn(repeat != 0); savePrefs(); radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index f4190f30..d95b073f 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -165,7 +165,11 @@ protected: } public: - void savePrefs() { _store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon); } + void savePrefs() { + _prefs.node_lat = sensors.node_lat; + _prefs.node_lon = sensors.node_lon; + _store->savePrefs(_prefs); + } #if ENV_INCLUDE_GPS == 1 void applyGpsPrefs() { diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 48c381ce..521693f3 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -1,5 +1,6 @@ #pragma once #include // For uint8_t, uint32_t +#include #define TELEM_MODE_DENY 0 #define TELEM_MODE_ALLOW_FLAGS 1 // use contact.flags @@ -8,9 +9,11 @@ #define ADVERT_LOC_NONE 0 #define ADVERT_LOC_SHARE 1 -struct NodePrefs { // persisted to file +class NodePrefs : public ConfigSerializer { // persisted to file +public: float airtime_factor; char node_name[32]; + double node_lat, node_lon; float freq; uint8_t sf; uint8_t cr; @@ -29,9 +32,101 @@ struct NodePrefs { // persisted to file uint32_t gps_interval; // GPS read interval in seconds uint8_t autoadd_config; // bitmask for auto-add contacts config uint8_t rx_boosted_gain; // SX126x RX boosted gain mode (0=power saving, 1=boosted) - uint8_t client_repeat; + uint8_t _client_repeat; // DEPRECATED -> use repeat.disable_fwd uint8_t path_hash_mode; // which path mode to use when sending uint8_t autoadd_max_hops; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64) char default_scope_name[31]; uint8_t default_scope_key[16]; + +private: + class RadioPrefs : public ConfigSerializer { // COPIED from CommonCLI (for now) + NodePrefs* _parent; + protected: + void structure() override { + def("freq", _parent->freq); + def("bw", _parent->bw); + def("sf", _parent->sf); + def("cr", _parent->cr); + //def("cad", _parent->cad_enabled); + //def("int_thr", _parent->interference_threshold); + def("rxgain", _parent->rx_boosted_gain); + def("fem_rxgain", _parent->rx_boosted_gain); + def("tx", _parent->tx_power_dbm); + def("af", _parent->airtime_factor); + def("rxdelay", _parent->rx_delay_base); + //def("f_txdelay", _parent->tx_delay_factor); currently hard-coded + //def("d_txdelay", _parent->direct_tx_delay_factor); currently hard-coded + //def("agc_int", _parent->agc_reset_interval); + def("hash_mode", _parent->path_hash_mode); + def("multi_ack", _parent->multi_acks); + } + public: + RadioPrefs(NodePrefs* parent) : _parent(parent) { } + }; + RadioPrefs radio; + + class GPSPrefs : public ConfigSerializer { // COPIED from CommionCLI (for now) + NodePrefs* _parent; + protected: + void structure() override { + def("en", _parent->gps_enabled); // boolean + def("int", _parent->gps_interval); // interval in seconds + def("adv_loc", _parent->advert_loc_policy); + } + public: + GPSPrefs(NodePrefs* parent) : _parent(parent) { } + }; + GPSPrefs gps; + + class RepeatPrefs : public ConfigSerializer { // COPIED from CommionCLI (for now) + public: + uint8_t disable_fwd = 1; + protected: + void structure() override { + def("disable", disable_fwd); + //def("f_max", flood_max); + //def("f_max_uns", flood_max_unscoped); + //def("f_max_adv", flood_max_advert); + //def("loop", loop_detect); + } + }; + RepeatPrefs repeat; + + class CompanionPrefs : public ConfigSerializer { + NodePrefs* _parent; + protected: + void structure() override { + def("auto_max", _parent->autoadd_max_hops); // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64) + def("defs_nm", _parent->default_scope_name, sizeof(_parent->default_scope_name)); + def("defs_key", (void *) _parent->default_scope_key, sizeof(_parent->default_scope_key)); + def("pin", _parent->ble_pin); + def("buzz_q", _parent->buzzer_quiet); + def("auto_add", _parent->autoadd_config); // bitmask for auto-add contacts config + def("man_add", _parent->manual_add_contacts); + def("tel_base", _parent->telemetry_mode_base); + def("tel_loc", _parent->telemetry_mode_loc); + def("tel_env", _parent->telemetry_mode_env); + } + public: + CompanionPrefs(NodePrefs* parent) : _parent(parent) { } + }; + CompanionPrefs companion; + +protected: + void structure() override { + def("name", node_name, sizeof(node_name)); + //def("adv_int", advert_interval); + //def("f_adv_int", flood_advert_interval); + def("lat", node_lat); + def("lon", node_lon); + def("radio", radio); + def("gps", gps); + def("repeat", repeat); + def("comp", companion); + } +public: + NodePrefs() : radio(this), gps(this), companion(this) { } + // new accessor methods + bool isRepeatEn() const { return repeat.disable_fwd == 0; } + void setRepeatEn(bool en) { repeat.disable_fwd = en ? 0 : 1; } }; \ No newline at end of file diff --git a/src/helpers/ConfigSerializer.cpp b/src/helpers/ConfigSerializer.cpp index 76673309..775ef220 100644 --- a/src/helpers/ConfigSerializer.cpp +++ b/src/helpers/ConfigSerializer.cpp @@ -140,6 +140,23 @@ void ConfigSerializer::writeComma() { } } +#include + +void ConfigSerializer::def(const char* key, void* value, size_t len) { + if (_context->op() == OP::WRITE) { + writeComma(); + _context->file()->print(key); + _context->file()->print(":\""); + mesh::Utils::printHex(*_context->file(), (uint8_t*) value, len); + _context->file()->print("\""); + } else { + if (_context->keyMatch(_depth, key)) { + memset(value, 0, len); + mesh::Utils::fromHex((uint8_t *)value, len, _context->getToken()); + } + } +} + void ConfigSerializer::def(const char* key, char* value, size_t max_len) { if (_context->op() == OP::WRITE) { writeComma(); diff --git a/src/helpers/ConfigSerializer.h b/src/helpers/ConfigSerializer.h index 5f0dc488..7e6d6f2a 100644 --- a/src/helpers/ConfigSerializer.h +++ b/src/helpers/ConfigSerializer.h @@ -48,6 +48,7 @@ protected: ConfigSerializer() { } void def(const char* key, char* value, size_t max_len); // max_len inclusive of null + void def(const char* key, void* value, size_t len); // binary blob (encoded in hex) void def(const char* key, int32_t& value); void def(const char* key, int16_t& value); void def(const char* key, int8_t& value); From ae2667ee9eedcb4321d8a45d1f6d8ccbb843375c Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Fri, 17 Jul 2026 20:55:30 +1000 Subject: [PATCH 16/70] * added unit tests for ConfigSerializer --- platformio.ini | 1 + src/helpers/ConfigSerializer.cpp | 4 +- test/mocks/Arduino.h | 3 + test/mocks/Stream.h | 67 ++++++++- .../test_config_serializer.cpp | 136 ++++++++++++++++++ 5 files changed, 206 insertions(+), 5 deletions(-) create mode 100644 test/mocks/Arduino.h create mode 100644 test/test_config_serializer/test_config_serializer.cpp diff --git a/platformio.ini b/platformio.ini index f3ada133..17f6c773 100644 --- a/platformio.ini +++ b/platformio.ini @@ -167,5 +167,6 @@ build_src_filter = -<*> +<../src/Utils.cpp> +<../src/Packet.cpp> + +<../src/helpers/ConfigSerializer.cpp> lib_deps = google/googletest @ 1.17.0 diff --git a/src/helpers/ConfigSerializer.cpp b/src/helpers/ConfigSerializer.cpp index 775ef220..9d1323e4 100644 --- a/src/helpers/ConfigSerializer.cpp +++ b/src/helpers/ConfigSerializer.cpp @@ -111,7 +111,7 @@ bool ConfigSerializer::loadSerial(Stream& s) { if (sp < CONFIG_MAX_DEPTH - 1) { sp++; } else { - Serial.printf("Error: max nesting reached"); // TODO: debug logging + //Serial.printf("Error: max nesting reached"); // TODO: debug logging context.success = false; break; } @@ -119,7 +119,7 @@ bool ConfigSerializer::loadSerial(Stream& s) { if (sp > 0) { sp--; } else { - Serial.printf("Error: too many closing '}'"); // TODO: debug logging + //Serial.printf("Error: too many closing '}'"); // TODO: debug logging context.success = false; break; } diff --git a/test/mocks/Arduino.h b/test/mocks/Arduino.h new file mode 100644 index 00000000..f0821bf4 --- /dev/null +++ b/test/mocks/Arduino.h @@ -0,0 +1,3 @@ +#pragma once + +#include diff --git a/test/mocks/Stream.h b/test/mocks/Stream.h index 195a3029..9b41c54d 100644 --- a/test/mocks/Stream.h +++ b/test/mocks/Stream.h @@ -1,10 +1,71 @@ #pragma once +#include +#include +#include + // Mock Stream class for native testing // Provides minimal interface needed by Utils.h -class Stream { +#define DEC 10 +#define HEX 16 +#define OCT 8 +#define BIN 2 + +class Print +{ public: - virtual void print(char c) {} - virtual void print(const char* str) {} + virtual size_t write(uint8_t b) { return 1; } + size_t write(const char *str) + { + if(str == NULL) { + return 0; + } + return write((const uint8_t *) str, strlen(str)); + } + virtual size_t write(const uint8_t *buffer, size_t size) { + size_t t = 0; + for (int i = 0; i < size; i++) { t += write(buffer[i]); } + return t; + } + size_t write(const char *buffer, size_t size) + { + return write((const uint8_t *) buffer, size); + } + + virtual size_t print(unsigned char b, int r = DEC) { return 0; } + virtual size_t print(int v, int r = DEC) { return 0; } + virtual size_t print(unsigned int v, int r = DEC) { return 0; } + virtual size_t print(long v, int r = DEC) { return 0; } + virtual size_t print(unsigned long v, int r = DEC) { return 0; } + virtual size_t print(long long v, int r = DEC) { return 0; } + virtual size_t print(unsigned long long v, int r = DEC) { return 0; } + virtual size_t print(double v, int p = 2) { return 0; } + + size_t print(char c) { return write(c); } + size_t print(const char* str) { return write(str); } + + //size_t println(void) { return 0; } + + virtual void flush() { /* Empty implementation for backward compatibility */ } }; + +class Stream: public Print +{ +public: + virtual int available() { return 0; } + virtual int read() { return -1; } + virtual int peek() { return 0; } + + virtual size_t readBytes(char *buffer, size_t length) { + size_t i = 0; + while (i < length && available()) { + buffer[i++] = read(); + } + return i; + } + virtual size_t readBytes(uint8_t *buffer, size_t length) + { + return readBytes((char *) buffer, length); + } +}; \ No newline at end of file diff --git a/test/test_config_serializer/test_config_serializer.cpp b/test/test_config_serializer/test_config_serializer.cpp new file mode 100644 index 00000000..d6ce6813 --- /dev/null +++ b/test/test_config_serializer/test_config_serializer.cpp @@ -0,0 +1,136 @@ +#include +#include "helpers/ConfigSerializer.h" + +#define TEST_INT_S "56" +#define TEST_INT 56 +#define TEST_FLOAT_S "-6.123" +#define TEST_FLOAT -6.1230f +#define TEST_DOUBLE_S "12.123456" +#define TEST_DOUBLE 12.123456 + +class MockInputStream : public Stream { + const char* _text; + int pos, len; +public: + MockInputStream(const char* text) : _text(text) { pos = 0; len = strlen(text); } + int available() override { return len - pos; } + int read() override { if (pos < len) { return _text[pos++]; } return -1; } + int peek() override { if (pos < len) { return _text[pos]; } return -1; } +}; + +class MockPrintStream : public Stream { + int len = 0; + uint8_t _buf[1024]; +public: + size_t write(uint8_t b) override { + if (len < sizeof(_buf)) { + _buf[len++] = b; + return 1; + } + return 0; + } + + size_t print(unsigned char b, int r) override { if (b == TEST_INT) return Print::print(TEST_INT_S); return 0; } + size_t print(int v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; } + size_t print(unsigned int v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; } + size_t print(long v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; } + size_t print(unsigned long v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; } + size_t print(long long v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; } + size_t print(unsigned long long v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; } + size_t print(double v, int p = 2) override { + if (p == 6) return Print::print(TEST_DOUBLE_S); + if (p == 4) return Print::print(TEST_FLOAT_S); + return 0; + } + + int getLength() const { return len; } + const uint8_t* getBytes() const { return _buf; } +}; + +class TestStruct : public ConfigSerializer { + protected: + void structure() override { + def("age", age); + def("flags", flags); + def("name", name, sizeof(name)); + } + public: + int32_t age; + char name[16]; + uint8_t flags; +}; + +// ── saveSerial: basic ─────────────────────────────────────────────────────── + +TEST(ConfigSerializer, SaveSerial_Basic) { + MockPrintStream s; + TestStruct data; + + data.age = TEST_INT; + data.flags = TEST_INT; + strcpy(data.name, "Scott"); + + bool success = data.saveSerial(s); + EXPECT_TRUE(success); + + auto l = s.getLength(); + const char* expect = "{age:" TEST_INT_S ",flags:" TEST_INT_S ",name:\"Scott\"}"; + EXPECT_EQ(strlen(expect), l); + + bool match = memcmp(s.getBytes(), expect, l) == 0; + EXPECT_TRUE(match); +} + +// ── loadSerial: basic ─────────────────────────────────────────────────────── + +TEST(ConfigSerializer, LoadSerial_Basic) { + MockInputStream s("{age:" TEST_INT_S ",flags:" TEST_INT_S ",name:\"Scott\"}"); + TestStruct data; + + bool success = data.loadSerial(s); + EXPECT_TRUE(success); + + EXPECT_EQ(TEST_INT, data.age); + EXPECT_EQ(TEST_INT, data.flags); + bool match = strcmp("Scott", data.name) == 0; + EXPECT_TRUE(match); +} + +TEST(ConfigSerializer, LoadSerial_UnmatchedBraces) { + MockInputStream s("{age:" TEST_INT_S ",flags:" TEST_INT_S ",name:\"Scott\""); + TestStruct data; + + bool success = data.loadSerial(s); + EXPECT_FALSE(success); +} + +TEST(ConfigSerializer, LoadSerial_MissingCommas) { + MockInputStream s("{age:" TEST_INT_S " flags:" TEST_INT_S " name:\"Scott\"}"); + TestStruct data; + + bool success = data.loadSerial(s); + EXPECT_FALSE(success); +} + +TEST(ConfigSerializer, LoadSerial_IgnoreUnknowns) { + MockInputStream s("{age:" TEST_INT_S ",xxx:" TEST_INT_S ",name:\"Scott\"}"); + TestStruct data; + data.flags = 1; + + // should ignore the 'xxx' property + bool success = data.loadSerial(s); + EXPECT_TRUE(success); + + EXPECT_EQ(TEST_INT, data.age); + EXPECT_EQ(1, data.flags); // flags should be unmodified + bool match = strcmp("Scott", data.name) == 0; + EXPECT_TRUE(match); +} + + +// ── main ─────────────────────────────────────────────────────── + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} From 975a673efd4fcabc21becb8f92f434b432f43265 Mon Sep 17 00:00:00 2001 From: taco Date: Fri, 17 Jul 2026 21:15:09 +1000 Subject: [PATCH 17/70] add calcMaxPacketMillis --- src/helpers/radiolib/RadioLibWrappers.cpp | 18 ++++++++++++++++++ src/helpers/radiolib/RadioLibWrappers.h | 6 ++++++ 2 files changed, 24 insertions(+) diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 5e72336c..7146e47b 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -228,3 +228,21 @@ float RadioLibWrapper::packetScoreInt(float snr, int sf, int packet_len) { return max(0.0, min(1.0, success_rate_based_on_snr * collision_penalty)); } + +PacketMillis RadioLibWrapper::calcMaxPacketMillis(uint8_t sf, float bw, uint8_t cr, uint8_t preambleSymbols) { + // based on RadioLib's calculateTimeOnAir() + uint32_t tsym_us = ((uint32_t)10000 << sf) / (bw * 10); + uint32_t sfCoeff1_x4 = (sf == 5 || sf == 6) ? 25 : 17; // 6.25 : 4.25, semtech magic numbers to account for sync word + sfd + + // preamble + syncword + sfd + header + uint32_t preamble_us = (((preambleSymbols + 8) * 4 + sfCoeff1_x4) * tsym_us) / 4; + + // airtime for max packet at current radio settings + uint32_t total_us = _radio->getTimeOnAir(MAX_TRANS_UNIT); + // airtime for payload only (no preamble, header or SOF) + uint32_t payload_us = total_us > preamble_us ? total_us - preamble_us : 4000 - preamble_us; // fallback to 4 secs at worst case + // rescale payload_us for max possible CR + if (cr >= 5 && cr < 8) { payload_us = (payload_us * 8) / cr; } + + return PacketMillis {(preamble_us + 999) / 1000, (payload_us + 999) / 1000}; +} \ No newline at end of file diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 3091832f..e7fef910 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -3,6 +3,11 @@ #include #include +struct PacketMillis { + uint32_t preambleMillis; // preamble-detect -> header-valid deadline + uint32_t payloadMillis; // header-valid -> rx-done deadline +}; + class RadioLibWrapper : public mesh::Radio { protected: PhysicalLayer* _radio; @@ -47,6 +52,7 @@ public: virtual uint8_t getSpreadingFactor() const { return LORA_SF; } static uint16_t preambleLengthForSF(uint8_t sf) { return sf <= 8 ? 32 : 16; } void updatePreamble(uint8_t sf) { _preamble_sf = sf; _radio->setPreambleLength(preambleLengthForSF(sf)); } + PacketMillis calcMaxPacketMillis(uint8_t sf, float bw, uint8_t cr, uint8_t preambleSymbols); virtual int16_t performChannelScan(); int getNoiseFloor() const override { return _noise_floor; } From d9f1e12b21dfbac92b90ea7b67dd51a704966d8f Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Fri, 17 Jul 2026 21:23:40 +1000 Subject: [PATCH 18/70] * loadSerial(), support for \n and \r --- src/helpers/ConfigSerializer.cpp | 2 + .../test_config_serializer.cpp | 43 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/helpers/ConfigSerializer.cpp b/src/helpers/ConfigSerializer.cpp index 9d1323e4..adff147f 100644 --- a/src/helpers/ConfigSerializer.cpp +++ b/src/helpers/ConfigSerializer.cpp @@ -76,6 +76,8 @@ int ConfigSerializer::Context::readNext() { return TOK_ERROR; case EXPECT_STRING_ESCAPE: + if ((c == 'n') && rd_len < CONFIG_MAX_TOKEN_LEN-1) { rd_buf[rd_len++] = '\n'; rd_mode = EXPECT_STRING_VAL; return TOK_WHITESPACE; } + if ((c == 'r') && rd_len < CONFIG_MAX_TOKEN_LEN-1) { rd_buf[rd_len++] = '\r'; rd_mode = EXPECT_STRING_VAL; return TOK_WHITESPACE; } if ((c == '"' || c == '\\' || c == '/') && rd_len < CONFIG_MAX_TOKEN_LEN-1) { rd_buf[rd_len++] = c; rd_mode = EXPECT_STRING_VAL; return TOK_WHITESPACE; } return TOK_ERROR; // unsupport escape diff --git a/test/test_config_serializer/test_config_serializer.cpp b/test/test_config_serializer/test_config_serializer.cpp index d6ce6813..c63ea8e8 100644 --- a/test/test_config_serializer/test_config_serializer.cpp +++ b/test/test_config_serializer/test_config_serializer.cpp @@ -81,6 +81,25 @@ TEST(ConfigSerializer, SaveSerial_Basic) { EXPECT_TRUE(match); } +TEST(ConfigSerializer, SaveSerial_EscChars) { + MockPrintStream s; + TestStruct data; + + data.age = TEST_INT; + data.flags = TEST_INT; + strcpy(data.name, "\"Scott\"\n"); + + bool success = data.saveSerial(s); + EXPECT_TRUE(success); + + auto l = s.getLength(); + const char* expect = "{age:" TEST_INT_S ",flags:" TEST_INT_S ",name:\"\\\"Scott\\\"\\n\"}"; + EXPECT_EQ(strlen(expect), l); + + bool match = memcmp(s.getBytes(), expect, l) == 0; + EXPECT_TRUE(match); +} + // ── loadSerial: basic ─────────────────────────────────────────────────────── TEST(ConfigSerializer, LoadSerial_Basic) { @@ -96,6 +115,30 @@ TEST(ConfigSerializer, LoadSerial_Basic) { EXPECT_TRUE(match); } +TEST(ConfigSerializer, LoadSerial_HandleWhitespace) { + MockInputStream s(" { age: " TEST_INT_S " , flags: " TEST_INT_S " , name: \"Scott\" } "); + TestStruct data; + + bool success = data.loadSerial(s); + EXPECT_TRUE(success); + + EXPECT_EQ(TEST_INT, data.age); + EXPECT_EQ(TEST_INT, data.flags); + bool match = strcmp("Scott", data.name) == 0; + EXPECT_TRUE(match); +} + +TEST(ConfigSerializer, LoadSerial_EscChars) { + MockInputStream s("{age:" TEST_INT_S ",flags:" TEST_INT_S ",name:\"\\\"Scott\\\"\\n\"}"); + TestStruct data; + + bool success = data.loadSerial(s); + EXPECT_TRUE(success); + + bool match = strcmp("\"Scott\"\n", data.name) == 0; + EXPECT_TRUE(match); +} + TEST(ConfigSerializer, LoadSerial_UnmatchedBraces) { MockInputStream s("{age:" TEST_INT_S ",flags:" TEST_INT_S ",name:\"Scott\""); TestStruct data; From 79ef74ea8def0195ddbe340441fec7ce5c943051 Mon Sep 17 00:00:00 2001 From: taco Date: Fri, 17 Jul 2026 21:33:57 +1000 Subject: [PATCH 19/70] add IRQ timeout logic for SX1262 --- src/helpers/radiolib/CustomSX1262.h | 51 +++++++++++++++++++--- src/helpers/radiolib/CustomSX1262Wrapper.h | 1 + 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/src/helpers/radiolib/CustomSX1262.h b/src/helpers/radiolib/CustomSX1262.h index ca62fc26..284b79c0 100644 --- a/src/helpers/radiolib/CustomSX1262.h +++ b/src/helpers/radiolib/CustomSX1262.h @@ -3,10 +3,12 @@ #include #include "MeshCore.h" -#define SX126X_IRQ_HEADER_VALID 0b0000010000 // 4 4 valid LoRa header received -#define SX126X_IRQ_PREAMBLE_DETECTED 0x04 - class CustomSX1262 : public SX1262 { + uint32_t _preambleMillis = 66; + uint32_t _maxPayloadMillis = 3934; + uint32_t _activityAt = 0; + bool _headerSeen = false; + public: CustomSX1262(Module *mod) : SX1262(mod) { } @@ -99,9 +101,46 @@ class CustomSX1262 : public SX1262 { } bool isReceiving() { - uint16_t irq = getIrqFlags(); - bool detected = (irq & SX126X_IRQ_HEADER_VALID) || (irq & SX126X_IRQ_PREAMBLE_DETECTED); - return detected; + uint32_t irq = getIrqFlags(); + bool preamble = irq & RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED; // bit 2 + bool header = irq & RADIOLIB_SX126X_IRQ_HEADER_VALID; // bit 4 + bool hdrErr = irq & RADIOLIB_SX126X_IRQ_HEADER_ERR; // bit 5 + uint32_t now = millis(); + if (hdrErr) { + clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED | RADIOLIB_SX126X_IRQ_HEADER_VALID | RADIOLIB_SX126X_IRQ_HEADER_ERR | RADIOLIB_SX126X_IRQ_SYNC_WORD_VALID); + _activityAt = 0; + _headerSeen = false; + return false; + } + if (header) { + if (!_headerSeen) { _headerSeen = true; _activityAt = now; }; + if (now - _activityAt > _maxPayloadMillis) { + MESH_DEBUG_PRINTLN("Clearing header IRQ after %ums", _maxPayloadMillis); + clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED | RADIOLIB_SX126X_IRQ_HEADER_VALID | RADIOLIB_SX126X_IRQ_HEADER_ERR | RADIOLIB_SX126X_IRQ_SYNC_WORD_VALID); + _activityAt = 0; _headerSeen = false; + return false; + } + return true; + } + if (preamble) { + if (_activityAt == 0) _activityAt = now; + if (now - _activityAt > _preambleMillis) { + clearIrqStatus(RADIOLIB_IRQ_PREAMBLE_DETECTED); + _activityAt = 0; + MESH_DEBUG_PRINTLN("Clearing preamble IRQ after %ums", _preambleMillis); + + return false; + } + return true; + } + _activityAt = 0; _headerSeen = false; + return false; + } + + PacketMillis setMaxPacketMillis(PacketMillis maxPacketMillis) { + MESH_DEBUG_PRINTLN("Setting _preambleMillis=%u, _maxPacketMillis=%u", maxPacketMillis.preambleMillis, maxPacketMillis.payloadMillis); + _preambleMillis = maxPacketMillis.preambleMillis; + _maxPayloadMillis = maxPacketMillis.payloadMillis; } bool getRxBoostedGainMode() { diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index 1d103f57..45af4cb4 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -18,6 +18,7 @@ public: ((CustomSX1262 *)_radio)->setBandwidth(bw); ((CustomSX1262 *)_radio)->setCodingRate(cr); updatePreamble(sf); + ((CustomSX1262 *)_radio)->setMaxPacketMillis(calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf))); } bool isReceivingPacket() override { From 0bd871cde6e3256c7de7518c703bb3827d3f9fa2 Mon Sep 17 00:00:00 2001 From: taco Date: Fri, 17 Jul 2026 21:45:31 +1000 Subject: [PATCH 20/70] add IRQ timeout logic for LR11X0 --- src/helpers/radiolib/CustomLR1110.h | 47 ++++++++++++++++++++-- src/helpers/radiolib/CustomLR1110Wrapper.h | 2 + 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/helpers/radiolib/CustomLR1110.h b/src/helpers/radiolib/CustomLR1110.h index 4061c6b1..db0f2af3 100644 --- a/src/helpers/radiolib/CustomLR1110.h +++ b/src/helpers/radiolib/CustomLR1110.h @@ -4,6 +4,10 @@ #include "MeshCore.h" class CustomLR1110 : public LR1110 { + uint32_t _preambleMillis = 66; + uint32_t _maxPayloadMillis = 3934; + uint32_t _activityAt = 0; + bool _headerSeen = false; bool _rx_boosted = false; public: @@ -32,9 +36,46 @@ class CustomLR1110 : public LR1110 { bool getRxBoostedGainMode() const { return _rx_boosted; } bool isReceiving() { - uint16_t irq = getIrqStatus(); - bool detected = ((irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID) || (irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED)); - return detected; + uint32_t irq = getIrqStatus(); + bool preamble = irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED; // bit 4 + bool header = irq & RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID; // bit 5 + bool hdrErr = irq & RADIOLIB_LR11X0_IRQ_HEADER_ERR; // bit 6 + uint32_t now = millis(); + if (hdrErr) { + clearIrqState(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED | RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID | RADIOLIB_LR11X0_IRQ_HEADER_ERR); + _activityAt = 0; + _headerSeen = false; + return false; + } + if (header) { + if (!_headerSeen) { _headerSeen = true; _activityAt = now; }; + if (now - _activityAt > _maxPayloadMillis) { + MESH_DEBUG_PRINTLN("Clearing header IRQ after %ums", _maxPayloadMillis); + clearIrqState(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED | RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID | RADIOLIB_LR11X0_IRQ_HEADER_ERR); + _activityAt = 0; _headerSeen = false; + return false; + } + return true; + } + if (preamble) { + if (_activityAt == 0) _activityAt = now; + if (now - _activityAt > _preambleMillis) { + clearIrqState(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED); + _activityAt = 0; + MESH_DEBUG_PRINTLN("Clearing preamble IRQ after %ums", _preambleMillis); + + return false; + } + return true; + } + _activityAt = 0; _headerSeen = false; + return false; + } + + PacketMillis setMaxPacketMillis(PacketMillis maxPacketMillis) { + MESH_DEBUG_PRINTLN("Setting _preambleMillis=%u, _maxPacketMillis=%u", maxPacketMillis.preambleMillis, maxPacketMillis.payloadMillis); + _preambleMillis = maxPacketMillis.preambleMillis; + _maxPayloadMillis = maxPacketMillis.payloadMillis; } uint8_t getSpreadingFactor() const { return spreadingFactor; } diff --git a/src/helpers/radiolib/CustomLR1110Wrapper.h b/src/helpers/radiolib/CustomLR1110Wrapper.h index c6b1acb4..981faf09 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -14,6 +14,8 @@ public: ((CustomLR1110 *)_radio)->setBandwidth(bw); ((CustomLR1110 *)_radio)->setCodingRate(cr); updatePreamble(sf); + ((CustomLR1110 *)_radio)->setMaxPacketMillis(calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf))); + } void doResetAGC() override { lr11x0ResetAGC((LR11x0 *)_radio, ((CustomLR1110 *)_radio)->getFreqMHz()); } From 2189f2294cabbe0213cab8f506a625808f562cd4 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sat, 18 Jul 2026 19:22:53 +1000 Subject: [PATCH 21/70] * added prefs initialisers to match original zeroed state --- examples/companion_radio/MyMesh.cpp | 1 - examples/companion_radio/NodePrefs.h | 52 +++++++++--------- src/helpers/CommonCLI.h | 80 +++++++++++++++------------- 3 files changed, 71 insertions(+), 62 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 1423d1ff..b8661caf 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -876,7 +876,6 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe send_unscoped = false; // defaults - //memset(&_prefs, 0, sizeof(_prefs)); _prefs.airtime_factor = 1.0; strcpy(_prefs.node_name, "NONAME"); _prefs.freq = LORA_FREQ; diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 521693f3..0c03bd78 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -11,30 +11,30 @@ class NodePrefs : public ConfigSerializer { // persisted to file public: - float airtime_factor; + float airtime_factor = 0; char node_name[32]; - double node_lat, node_lon; - float freq; - uint8_t sf; - uint8_t cr; - uint8_t multi_acks; - uint8_t manual_add_contacts; - float bw; - int8_t tx_power_dbm; - uint8_t telemetry_mode_base; - uint8_t telemetry_mode_loc; - uint8_t telemetry_mode_env; - float rx_delay_base; - uint32_t ble_pin; - uint8_t advert_loc_policy; - uint8_t buzzer_quiet; - uint8_t gps_enabled; // GPS enabled flag (0=disabled, 1=enabled) - uint32_t gps_interval; // GPS read interval in seconds - uint8_t autoadd_config; // bitmask for auto-add contacts config - uint8_t rx_boosted_gain; // SX126x RX boosted gain mode (0=power saving, 1=boosted) - uint8_t _client_repeat; // DEPRECATED -> use repeat.disable_fwd - uint8_t path_hash_mode; // which path mode to use when sending - uint8_t autoadd_max_hops; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64) + double node_lat = 0, node_lon = 0; + float freq = 0; + uint8_t sf = 0; + uint8_t cr = 0; + uint8_t multi_acks = 0; + uint8_t manual_add_contacts = 0; + float bw = 0; + int8_t tx_power_dbm = 0; + uint8_t telemetry_mode_base = 0; + uint8_t telemetry_mode_loc = 0; + uint8_t telemetry_mode_env = 0; + float rx_delay_base = 0; + uint32_t ble_pin = 0; + uint8_t advert_loc_policy = 0; + uint8_t buzzer_quiet = 0; + uint8_t gps_enabled = 0; // GPS enabled flag (0=disabled, 1=enabled) + uint32_t gps_interval = 0; // GPS read interval in seconds + uint8_t autoadd_config = 0; // bitmask for auto-add contacts config + uint8_t rx_boosted_gain = 0; // SX126x RX boosted gain mode (0=power saving, 1=boosted) + uint8_t _client_repeat = 0; // DEPRECATED -> use repeat.disable_fwd + uint8_t path_hash_mode = 0; // which path mode to use when sending + uint8_t autoadd_max_hops = 0; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64) char default_scope_name[31]; uint8_t default_scope_key[16]; @@ -125,7 +125,11 @@ protected: def("comp", companion); } public: - NodePrefs() : radio(this), gps(this), companion(this) { } + NodePrefs() : radio(this), gps(this), companion(this) { + node_name[0] = 0; + default_scope_name[0] = 0; + memset(default_scope_key, 0, sizeof(default_scope_key)); + } // new accessor methods bool isRepeatEn() const { return repeat.disable_fwd == 0; } void setRepeatEn(bool en) { repeat.disable_fwd = en ? 0 : 1; } diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index aab33d11..69fe0315 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -23,51 +23,51 @@ class NodePrefs : public ConfigSerializer { public: // in-memory backing data - float airtime_factor; + float airtime_factor = 0; char node_name[32]; - double node_lat, node_lon; + double node_lat = 0, node_lon = 0; char password[16]; - float freq; - int8_t tx_power_dbm; - uint8_t disable_fwd; - uint8_t advert_interval; // minutes / 2 - uint8_t flood_advert_interval; // hours - float rx_delay_base; - float tx_delay_factor; + float freq = 0; + int8_t tx_power_dbm = 0; + uint8_t disable_fwd = 0; + uint8_t advert_interval = 0; // minutes / 2 + uint8_t flood_advert_interval = 0; // hours + float rx_delay_base = 0; + float tx_delay_factor = 0; char guest_password[16]; - float direct_tx_delay_factor; + float direct_tx_delay_factor = 0; uint32_t guard; - uint8_t sf; - uint8_t cr; - uint8_t allow_read_only; - uint8_t multi_acks; - float bw; - uint8_t flood_max; - uint8_t flood_max_unscoped; - uint8_t flood_max_advert; - uint8_t interference_threshold; - uint8_t agc_reset_interval; // secs / 4 + uint8_t sf = 0; + uint8_t cr = 0; + uint8_t allow_read_only = 0; + uint8_t multi_acks = 0; + float bw = 0; + uint8_t flood_max = 0; + uint8_t flood_max_unscoped = 0; + uint8_t flood_max_advert = 0; + uint8_t interference_threshold = 0; + uint8_t agc_reset_interval = 0; // secs / 4 // Bridge settings - uint8_t bridge_enabled; // boolean - uint16_t bridge_delay; // milliseconds (default 500 ms) - uint8_t bridge_pkt_src; // 0 = logTx, 1 = logRx (default logTx) - uint32_t bridge_baud; // 9600, 19200, 38400, 57600, 115200 (default 115200) - uint8_t bridge_channel; // 1-14 (ESP-NOW only) + uint8_t bridge_enabled = 0; // boolean + uint16_t bridge_delay = 0; // milliseconds (default 500 ms) + uint8_t bridge_pkt_src = 0; // 0 = logTx, 1 = logRx (default logTx) + uint32_t bridge_baud = 0; // 9600, 19200, 38400, 57600, 115200 (default 115200) + uint8_t bridge_channel = 0; // 1-14 (ESP-NOW only) char bridge_secret[16]; // for XOR encryption of bridge packets (ESP-NOW only) // Power setting - uint8_t powersaving_enabled; // boolean + uint8_t powersaving_enabled = 0; // boolean // Gps settings - uint8_t gps_enabled; - uint32_t gps_interval; // in seconds - uint8_t advert_loc_policy; - uint32_t discovery_mod_timestamp; - float adc_multiplier; + uint8_t gps_enabled = 0; + uint32_t gps_interval = 0; // in seconds + uint8_t advert_loc_policy = 0; + uint32_t discovery_mod_timestamp = 0; + float adc_multiplier = 0; char owner_info[120]; - uint8_t rx_boosted_gain; // power settings - uint8_t radio_fem_rxgain; // LoRa FEM RX gain setting - uint8_t path_hash_mode; // which path mode to use when sending - uint8_t loop_detect; - uint8_t cad_enabled; // hardware Channel Activity Detection before TX (boolean) + uint8_t rx_boosted_gain = 0; // power settings + uint8_t radio_fem_rxgain = 0; // LoRa FEM RX gain setting + uint8_t path_hash_mode = 0; // which path mode to use when sending + uint8_t loop_detect = 0; + uint8_t cad_enabled = 0; // hardware Channel Activity Detection before TX (boolean) private: class RadioPrefs : public ConfigSerializer { @@ -182,7 +182,13 @@ protected: } public: - NodePrefs() : ConfigSerializer(), bridge(this), gps(this), radio(this), power(this), repeat(this), room(this) { } + NodePrefs() : ConfigSerializer(), bridge(this), gps(this), radio(this), power(this), repeat(this), room(this) { + node_name[0] = 0; + password[0] = 0; + guest_password[0] = 0; + bridge_secret[0] = 0; + owner_info[0] = 0; + } }; class CommonCLICallbacks { From a0b52be4adde46df63bec79aee1e69eb567b51ef Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sat, 18 Jul 2026 20:12:21 +1000 Subject: [PATCH 22/70] * RP2040 build fixes * new prefs file now "/prefs.json" --- examples/companion_radio/NodePrefs.h | 4 ++-- platformio.ini | 2 +- src/helpers/CommonCLI.cpp | 16 ++++++++++------ 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 0c03bd78..39a5386a 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -65,7 +65,7 @@ private: }; RadioPrefs radio; - class GPSPrefs : public ConfigSerializer { // COPIED from CommionCLI (for now) + class GPSPrefs : public ConfigSerializer { // COPIED from CommonCLI (for now) NodePrefs* _parent; protected: void structure() override { @@ -78,7 +78,7 @@ private: }; GPSPrefs gps; - class RepeatPrefs : public ConfigSerializer { // COPIED from CommionCLI (for now) + class RepeatPrefs : public ConfigSerializer { // COPIED from CommonCLI (for now) public: uint8_t disable_fwd = 1; protected: diff --git a/platformio.ini b/platformio.ini index 17f6c773..fd0afcdf 100644 --- a/platformio.ini +++ b/platformio.ini @@ -100,7 +100,7 @@ lib_deps = extends = arduino_base upload_protocol = picotool board_build.core = earlephilhower -platform = https://github.com/maxgerhardt/platform-raspberrypi.git#4e22a0d ; framework-arduinopico @ 1.50600.0+sha.6a1d13e9 +platform = https://github.com/maxgerhardt/platform-raspberrypi.git ; framework-arduinopico @ 1.50600.0+sha.6a1d13e9 build_flags = ${arduino_base.build_flags} -D RP2040_PLATFORM diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 56a46d46..9d0571d9 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -28,8 +28,12 @@ static bool isValidName(const char *n) { } void CommonCLI::loadPrefs(FILESYSTEM* fs) { - if (fs->exists("/ser_prefs")) { - File file = fs->open("/ser_prefs"); + if (fs->exists("/prefs.json")) { +#if defined(RP2040_PLATFORM) + File file = fs->open("/prefs.json", "r"); +#else + File file = fs->open("/prefs.json"); +#endif if (file) { _prefs->loadSerial(file); // new Serial prefs file.close(); @@ -137,12 +141,12 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { // Legacy bool CommonCLI::savePrefs(FILESYSTEM* fs) { #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) - fs->remove("/ser_prefs"); - File file = fs->open("/ser_prefs", FILE_O_WRITE); + fs->remove("/prefs.json"); + File file = fs->open("/prefs.json", FILE_O_WRITE); #elif defined(RP2040_PLATFORM) - File file = fs->open("/ser_prefs", "w"); + File file = fs->open("/prefs.json", "w"); #else - File file = fs->open("/ser_prefs", "w", true); + File file = fs->open("/prefs.json", "w", true); #endif if (file) { bool success = _prefs->saveSerial(file); From fb2c61f862fcd4c6e08cf0f882175ca260052b13 Mon Sep 17 00:00:00 2001 From: ViezeVingertjes Date: Mon, 22 Jun 2026 16:12:00 +0200 Subject: [PATCH 23/70] fix: prevent ESP32 KISS modem stalls under USB backpressure Switch ESP32-S3 KISS modem environments to HWCDC and move outbound KISS writes to a non-blocking queued frame path so loop() and TX completion keep progressing when the host reads slowly. Add native backpressure regression tests and correct the native_kiss_modem test filter so this suite runs directly with pio test. --- docs/kiss_modem_protocol.md | 14 +- examples/kiss_modem/KissModem.cpp | 199 ++++++++++-- examples/kiss_modem/KissModem.h | 36 ++- examples/kiss_modem/main.cpp | 10 +- platformio.ini | 15 + test/mocks/Arduino.h | 17 + test/mocks/CayenneLPP.h | 11 + test/mocks/Identity.h | 39 +++ test/mocks/Mesh.h | 27 ++ test/mocks/Stream.h | 21 +- test/mocks/Utils.h | 44 +++ test/mocks/helpers/SensorManager.h | 10 + test/test_kiss_modem/test_tx_backpressure.cpp | 294 ++++++++++++++++++ variants/heltec_v4/platformio.ini | 5 + variants/station_g2/platformio.ini | 5 + 15 files changed, 701 insertions(+), 46 deletions(-) create mode 100644 test/mocks/Arduino.h create mode 100644 test/mocks/CayenneLPP.h create mode 100644 test/mocks/Identity.h create mode 100644 test/mocks/Mesh.h create mode 100644 test/mocks/Utils.h create mode 100644 test/mocks/helpers/SensorManager.h create mode 100644 test/test_kiss_modem/test_tx_backpressure.cpp diff --git a/docs/kiss_modem_protocol.md b/docs/kiss_modem_protocol.md index 9a996224..3f4dbf9c 100644 --- a/docs/kiss_modem_protocol.md +++ b/docs/kiss_modem_protocol.md @@ -41,7 +41,7 @@ Maximum unescaped frame size: 512 bytes. | Command | Value | Data | Description | |-------------|--------|--------------------|-------------------------------------------------------------| -| Data | `0x00` | Raw packet | Queue packet for transmission | +| Data | `0x00` | Raw packet | Queue packet for transmission (one pending at a time) | | TXDELAY | `0x01` | Delay (1 byte) | Transmitter keyup delay in 10ms units (default: 50 = 500ms) | | Persistence | `0x02` | P (1 byte) | CSMA persistence parameter 0-255 (default: 63) | | SlotTime | `0x03` | Interval (1 byte) | CSMA slot interval in 10ms units (default: 10 = 100ms) | @@ -58,6 +58,12 @@ Maximum unescaped frame size: 512 bytes. Data frames carry raw packet data only, with no metadata prepended. The Data command payload is limited to 255 bytes to match the MeshCore maximum transmission unit (MAX_TRANS_UNIT); frames larger than 255 bytes are silently dropped. The KISS specification recommends at least 1024 bytes for general-purpose TNCs; this modem is intended for MeshCore packets only, whose protocol MTU is 255 bytes. +Only one packet may be pending for radio transmission at a time. If the host sends a second Data frame before the first has completed, the modem responds with Error (0xF1) and TxBusy (0x07). + +### Host Output Backpressure + +Outbound frames are encoded into a 2-slot queue and flushed when serial output space is available; `loop()` never blocks on writes. Radio TX state advances independently of host read speed. TxDone is retained until it can be queued. If the outbound queue is full, the modem responds with Error (0xF1) and TxBusy (0x07). Hosts should read serial promptly to avoid delayed responses. + ### CSMA Behavior The TNC implements p-persistent CSMA for half-duplex operation: @@ -156,15 +162,15 @@ Response codes use the high-bit convention: `response = command | 0x80`. Generic | MacFailed | `0x04` | MAC verification failed | | UnknownCmd | `0x05` | Unknown sub-command | | EncryptFailed | `0x06` | Encryption failed | -| TxBusy | `0x07` | Transmit busy | +| TxBusy | `0x07` | Radio TX busy, or host output queue full | ### Unsolicited Events The TNC sends these SetHardware frames without a preceding request: -**TxDone (0xF8)**: Sent after a packet has been transmitted. Contains a single byte: 0x01 for success, 0x00 for failure. +**TxDone (0xF8)**: Sent after radio transmission completes. Contains a single byte: 0x01 for success, 0x00 for failure. Delivery to the host may be delayed under serial backpressure but is not dropped. -**RxMeta (0xF9)**: Sent immediately after each standard data frame (type 0x00) with metadata for the received packet. Contains SNR (1 byte, signed, value x4 for 0.25 dB precision) followed by RSSI (1 byte, signed, dBm). Enabled by default; can be toggled with SetSignalReport. Standard KISS clients ignore this frame. +**RxMeta (0xF9)**: Sent after each standard data frame (type 0x00) with SNR (1 byte, signed, value x4) and RSSI (1 byte, signed, dBm). Queued with the data frame; omitted if the data frame cannot be queued. Enabled by default; toggle with SetSignalReport. Standard KISS clients ignore this frame. ## Data Formats diff --git a/examples/kiss_modem/KissModem.cpp b/examples/kiss_modem/KissModem.cpp index eeab1501..7dcfc8d7 100644 --- a/examples/kiss_modem/KissModem.cpp +++ b/examples/kiss_modem/KissModem.cpp @@ -22,6 +22,7 @@ KissModem::KissModem(Stream& serial, mesh::LocalIdentity& identity, mesh::RNG& r _getStatsCallback = nullptr; _config = {0, 0, 0, 0, 0}; _signal_report_enabled = true; + resetOutputQueue(); } void KissModem::begin() { @@ -30,37 +31,168 @@ void KissModem::begin() { _rx_active = false; _has_pending_tx = false; _tx_state = TX_IDLE; + resetOutputQueue(); } -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::resetOutputQueue() { + _tx_frame_head = 0; + _tx_frame_tail = 0; + _tx_frame_count = 0; + _tx_busy_error_pending = false; + _tx_done_pending = false; + _tx_done_result = 0; +} + +void KissModem::popTxFrame() { + _tx_frame_head = (uint8_t)((_tx_frame_head + 1) % KISS_TX_FRAME_QUEUE_DEPTH); + _tx_frame_count--; +} + +uint16_t KissModem::appendEscapedByte(uint8_t* dest, uint16_t idx, uint16_t max_len, uint8_t b) { + if (b == KISS_FEND || b == KISS_FESC) { + if (idx + 2 > max_len) { + return 0; + } + dest[idx++] = KISS_FESC; + dest[idx++] = (b == KISS_FEND) ? KISS_TFEND : KISS_TFESC; + return idx; } + if (idx + 1 > max_len) { + return 0; + } + dest[idx++] = b; + return idx; } -void KissModem::writeFrame(uint8_t type, const uint8_t* data, uint16_t len) { - _serial.write(KISS_FEND); - writeByte(type); +uint16_t KissModem::encodeFrame(uint8_t type, const uint8_t* data, uint16_t len, uint8_t* dest, uint16_t max_len) { + if (max_len < KISS_FRAME_BOUNDARY_BYTES) { + return 0; + } + + uint16_t idx = 0; + dest[idx++] = KISS_FEND; + + idx = appendEscapedByte(dest, idx, max_len, type); + if (idx == 0) { + return 0; + } + for (uint16_t i = 0; i < len; i++) { - writeByte(data[i]); + idx = appendEscapedByte(dest, idx, max_len, data[i]); + if (idx == 0) { + return 0; + } } - _serial.write(KISS_FEND); + + if (idx + 1 > max_len) { + return 0; + } + dest[idx++] = KISS_FEND; + return idx; } -void KissModem::writeHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len) { - _serial.write(KISS_FEND); - writeByte(KISS_CMD_SETHARDWARE); - writeByte(sub_cmd); - for (uint16_t i = 0; i < len; i++) { - writeByte(data[i]); +bool KissModem::tryFlushFrames() { + while (_tx_frame_count > 0) { + const uint8_t idx = _tx_frame_head; + const uint16_t frame_len = _tx_frame_len[idx]; + uint16_t written_len = _tx_frame_written[idx]; + + if (written_len >= frame_len) { + popTxFrame(); + continue; + } + + const int available = _serial.availableForWrite(); + if (available <= 0) { + return false; + } + + const uint16_t remaining = frame_len - written_len; + const uint16_t chunk_len = (available < (int)remaining) ? (uint16_t)available : remaining; + if (chunk_len == 0) { + return false; + } + + size_t chunk_written = _serial.write(_tx_frame_buf[idx] + written_len, chunk_len); + if (chunk_written == 0) { + return false; + } + + written_len += (uint16_t)chunk_written; + _tx_frame_written[idx] = written_len; + + if (written_len < frame_len) { + return false; + } + + popTxFrame(); } - _serial.write(KISS_FEND); + return true; +} + +bool KissModem::queueFrame(uint8_t type, const uint8_t* data, uint16_t len, bool mark_busy_error) { + if (_tx_frame_count >= KISS_TX_FRAME_QUEUE_DEPTH && !tryFlushFrames()) { + if (mark_busy_error) { + _tx_busy_error_pending = true; + } + return false; + } + const uint8_t idx = _tx_frame_tail; + uint16_t frame_len = encodeFrame(type, data, len, _tx_frame_buf[idx], sizeof(_tx_frame_buf[idx])); + if (frame_len == 0) { + return false; + } + + _tx_frame_len[idx] = frame_len; + _tx_frame_written[idx] = 0; + _tx_frame_tail = (uint8_t)((_tx_frame_tail + 1) % KISS_TX_FRAME_QUEUE_DEPTH); + _tx_frame_count++; + tryFlushFrames(); + return true; +} + +bool KissModem::queuePendingBusyError() { + if (!_tx_busy_error_pending) { + return true; + } + const uint8_t err = HW_ERR_TX_BUSY; + if (!queueHardwareFrame(HW_RESP_ERROR, &err, 1, false)) { + return false; + } + _tx_busy_error_pending = false; + return true; +} + +bool KissModem::queueHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len, bool mark_busy_error) { + if (len > KISS_MAX_FRAME_SIZE) { + return false; + } + _tx_hw_payload[0] = sub_cmd; + if (len > 0) { + memcpy(_tx_hw_payload + 1, data, len); + } + return queueFrame(KISS_CMD_SETHARDWARE, _tx_hw_payload, len + 1, mark_busy_error); +} + +bool KissModem::queuePendingTxDone() { + if (!_tx_done_pending) { + return true; + } + if (!queueHardwareFrame(HW_RESP_TX_DONE, &_tx_done_result, 1, false)) { + return false; + } + _tx_done_pending = false; + return true; +} + +void KissModem::setTxDonePending(uint8_t result) { + _tx_done_result = result; + _tx_done_pending = true; + _tx_state = TX_DONE_PENDING; +} + +bool KissModem::writeHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len) { + return queueHardwareFrame(sub_cmd, data, len, true); } void KissModem::writeHardwareError(uint8_t error_code) { @@ -68,6 +200,8 @@ void KissModem::writeHardwareError(uint8_t error_code) { } void KissModem::loop() { + tryFlushFrames(); + while (_serial.available()) { uint8_t b = _serial.read(); @@ -106,6 +240,8 @@ void KissModem::loop() { } processTx(); + tryFlushFrames(); + queuePendingBusyError(); } void KissModem::processFrame() { @@ -295,10 +431,7 @@ void KissModem::processTx() { _tx_timer = millis(); _tx_state = TX_SENDING; } else { - uint8_t result = 0x00; - writeHardwareFrame(HW_RESP_TX_DONE, &result, 1); - _has_pending_tx = false; - _tx_state = TX_IDLE; + setTxDonePending(0x00); } } break; @@ -306,14 +439,15 @@ void KissModem::processTx() { case TX_SENDING: if (_radio.isSendComplete()) { _radio.onSendFinished(); - uint8_t result = 0x01; - writeHardwareFrame(HW_RESP_TX_DONE, &result, 1); - _has_pending_tx = false; - _tx_state = TX_IDLE; + setTxDonePending(0x01); } else if (millis() - _tx_timer >= _radio.getEstAirtimeFor(_pending_tx_len) * KISS_TX_TIMEOUT_FACTOR) { _radio.onSendFinished(); - uint8_t result = 0x00; - writeHardwareFrame(HW_RESP_TX_DONE, &result, 1); + setTxDonePending(0x00); + } + break; + + case TX_DONE_PENDING: + if (queuePendingTxDone()) { _has_pending_tx = false; _tx_state = TX_IDLE; } @@ -322,8 +456,7 @@ void KissModem::processTx() { } void KissModem::onPacketReceived(int8_t snr, int8_t rssi, const uint8_t* packet, uint16_t len) { - writeFrame(KISS_CMD_DATA, packet, len); - if (_signal_report_enabled) { + if (queueFrame(KISS_CMD_DATA, packet, len) && _signal_report_enabled) { uint8_t meta[2] = { (uint8_t)snr, (uint8_t)rssi }; writeHardwareFrame(HW_RESP_RX_META, meta, 2); } diff --git a/examples/kiss_modem/KissModem.h b/examples/kiss_modem/KissModem.h index bbe99d6d..a23e459b 100644 --- a/examples/kiss_modem/KissModem.h +++ b/examples/kiss_modem/KissModem.h @@ -13,6 +13,14 @@ #define KISS_MAX_FRAME_SIZE 512 #define KISS_MAX_PACKET_SIZE 255 +#define KISS_FRAME_BOUNDARY_BYTES 2 +#define KISS_TYPE_BYTES 1 +#define KISS_HW_SUBCMD_BYTES 1 +#define KISS_MAX_ESCAPABLE_BYTES (KISS_MAX_FRAME_SIZE + KISS_TYPE_BYTES + KISS_HW_SUBCMD_BYTES) +#define KISS_MAX_ESCAPED_PAYLOAD_SIZE (2 * KISS_MAX_ESCAPABLE_BYTES) +#define KISS_MAX_ENCODED_FRAME_SIZE (KISS_FRAME_BOUNDARY_BYTES + KISS_MAX_ESCAPED_PAYLOAD_SIZE) +#define KISS_TX_FRAME_QUEUE_DEPTH 2 +#define KISS_HW_MAX_PAYLOAD_SIZE (KISS_MAX_FRAME_SIZE + KISS_HW_SUBCMD_BYTES) #define KISS_CMD_DATA 0x00 #define KISS_CMD_TXDELAY 0x01 @@ -94,7 +102,8 @@ enum TxState { TX_WAIT_CLEAR, TX_SLOT_WAIT, TX_DELAY, - TX_SENDING + TX_SENDING, + TX_DONE_PENDING }; class KissModem { @@ -130,10 +139,28 @@ class KissModem { RadioConfig _config; bool _signal_report_enabled; + uint8_t _tx_frame_buf[KISS_TX_FRAME_QUEUE_DEPTH][KISS_MAX_ENCODED_FRAME_SIZE]; + uint16_t _tx_frame_len[KISS_TX_FRAME_QUEUE_DEPTH]; + uint16_t _tx_frame_written[KISS_TX_FRAME_QUEUE_DEPTH]; + uint8_t _tx_frame_head; + uint8_t _tx_frame_tail; + uint8_t _tx_frame_count; + bool _tx_busy_error_pending; + bool _tx_done_pending; + uint8_t _tx_done_result; + uint8_t _tx_hw_payload[KISS_HW_MAX_PAYLOAD_SIZE]; - void writeByte(uint8_t b); - void writeFrame(uint8_t type, const uint8_t* data, uint16_t len); - void writeHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len); + static uint16_t appendEscapedByte(uint8_t* dest, uint16_t idx, uint16_t max_len, uint8_t b); + static uint16_t encodeFrame(uint8_t type, const uint8_t* data, uint16_t len, uint8_t* dest, uint16_t max_len); + void resetOutputQueue(); + void popTxFrame(); + bool tryFlushFrames(); + bool queueFrame(uint8_t type, const uint8_t* data, uint16_t len, bool mark_busy_error = true); + bool queuePendingBusyError(); + bool queueHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len, bool mark_busy_error); + bool queuePendingTxDone(); + void setTxDonePending(uint8_t result); + bool writeHardwareFrame(uint8_t sub_cmd, const uint8_t* data, uint16_t len); void writeHardwareError(uint8_t error_code); void processFrame(); void handleHardwareCommand(uint8_t sub_cmd, const uint8_t* data, uint16_t len); @@ -182,4 +209,5 @@ public: bool isTxBusy() const { return _tx_state != TX_IDLE; } /** True only when radio is actually transmitting; use to skip recvRaw in main loop. */ bool isActuallyTransmitting() const { return _tx_state == TX_SENDING; } + bool isHostOutputBackedUp() const { return _tx_frame_count > 0 || _tx_busy_error_pending || _tx_done_pending; } }; diff --git a/examples/kiss_modem/main.cpp b/examples/kiss_modem/main.cpp index 7fbcaed1..95a06ee6 100644 --- a/examples/kiss_modem/main.cpp +++ b/examples/kiss_modem/main.cpp @@ -20,6 +20,8 @@ #define NOISE_FLOOR_CALIB_INTERVAL_MS 2000 #define AGC_RESET_INTERVAL_MS 30000 +#define USB_TX_TIMEOUT_MS 50 +#define USB_TX_BUFFER_SIZE 1024 StdRNG rng; mesh::LocalIdentity identity; @@ -111,6 +113,12 @@ void setup() { uint32_t start = millis(); while (!Serial && millis() - start < 3000) delay(10); delay(100); +#if defined(ESP32) + Serial.setTxTimeoutMs(USB_TX_TIMEOUT_MS); +#if ARDUINO_USB_MODE + Serial.setTxBufferSize(USB_TX_BUFFER_SIZE); +#endif +#endif modem = new KissModem(Serial, identity, rng, radio_driver, board, sensors); #endif @@ -126,7 +134,7 @@ void setup() { void loop() { modem->loop(); - if (!modem->isActuallyTransmitting()) { + if (!modem->isActuallyTransmitting() && !modem->isHostOutputBackedUp()) { if (!modem->isTxBusy()) { if ((uint32_t)(millis() - next_agc_reset_ms) >= AGC_RESET_INTERVAL_MS) { radio_driver.resetAGC(); diff --git a/platformio.ini b/platformio.ini index f3ada133..2de71a37 100644 --- a/platformio.ini +++ b/platformio.ini @@ -169,3 +169,18 @@ build_src_filter = +<../src/Packet.cpp> lib_deps = google/googletest @ 1.17.0 + +[env:native_kiss_modem] +platform = native +test_framework = googletest +build_flags = -std=c++17 + -I test/mocks + -I src + -I examples/kiss_modem +test_build_src = yes +test_filter = test_kiss_modem +build_src_filter = + -<*> + +<../examples/kiss_modem/KissModem.cpp> +lib_deps = + google/googletest @ 1.17.0 diff --git a/test/mocks/Arduino.h b/test/mocks/Arduino.h new file mode 100644 index 00000000..77499fe4 --- /dev/null +++ b/test/mocks/Arduino.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include +#include "Stream.h" + +inline uint32_t g_mock_millis = 0; + +using std::isnan; + +inline uint32_t millis() { + return g_mock_millis; +} + +inline void delay(uint32_t ms) { + g_mock_millis += ms; +} diff --git a/test/mocks/CayenneLPP.h b/test/mocks/CayenneLPP.h new file mode 100644 index 00000000..9d51c0fc --- /dev/null +++ b/test/mocks/CayenneLPP.h @@ -0,0 +1,11 @@ +#pragma once + +#include +#include + +class CayenneLPP { +public: + explicit CayenneLPP(size_t) {} + const uint8_t* getBuffer() const { return nullptr; } + uint16_t getSize() const { return 0; } +}; diff --git a/test/mocks/Identity.h b/test/mocks/Identity.h new file mode 100644 index 00000000..5c3d25e9 --- /dev/null +++ b/test/mocks/Identity.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include "Utils.h" + +namespace mesh { + +class Identity { +public: + uint8_t pub_key[PUB_KEY_SIZE]; + + Identity() { + std::memset(pub_key, 0, sizeof(pub_key)); + } + + explicit Identity(const uint8_t* src) { + std::memcpy(pub_key, src, PUB_KEY_SIZE); + } + + bool verify(const uint8_t*, const uint8_t*, int) const { + return true; + } +}; + +class LocalIdentity : public Identity { +public: + LocalIdentity() : Identity() {} + + void sign(uint8_t* sig, const uint8_t*, int) const { + std::memset(sig, 0x5A, SIGNATURE_SIZE); + } + + void calcSharedSecret(uint8_t* secret, const uint8_t*) const { + std::memset(secret, 0x11, PUB_KEY_SIZE); + } +}; + +} diff --git a/test/mocks/Mesh.h b/test/mocks/Mesh.h new file mode 100644 index 00000000..b6c263c1 --- /dev/null +++ b/test/mocks/Mesh.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +namespace mesh { + +class Radio { +public: + virtual ~Radio() = default; + virtual bool isReceiving() { return false; } + virtual uint32_t getEstAirtimeFor(uint16_t) { return 10; } + virtual bool startSendRaw(const uint8_t*, uint16_t) { return true; } + virtual bool isSendComplete() { return true; } + virtual void onSendFinished() {} + virtual int16_t getNoiseFloor() { return -120; } +}; + +class MainBoard { +public: + virtual ~MainBoard() = default; + virtual uint16_t getBattMilliVolts() { return 4200; } + virtual float getMCUTemperature() { return 25.0f; } + virtual const char* getManufacturerName() { return "mock-board"; } + virtual void reboot() {} +}; + +} diff --git a/test/mocks/Stream.h b/test/mocks/Stream.h index 195a3029..cf33785d 100644 --- a/test/mocks/Stream.h +++ b/test/mocks/Stream.h @@ -1,10 +1,23 @@ #pragma once -// Mock Stream class for native testing -// Provides minimal interface needed by Utils.h +#include +#include class Stream { public: - virtual void print(char c) {} - virtual void print(const char* str) {} + virtual ~Stream() = default; + virtual size_t write(uint8_t) { return 1; } + virtual size_t write(const uint8_t* buffer, size_t size) { + size_t total = 0; + for (size_t i = 0; i < size; i++) { + total += write(buffer[i]); + } + return total; + } + virtual int available() { return 0; } + virtual int availableForWrite() { return 0; } + virtual int read() { return -1; } + virtual void flush() {} + virtual void print(char) {} + virtual void print(const char*) {} }; diff --git a/test/mocks/Utils.h b/test/mocks/Utils.h new file mode 100644 index 00000000..9bb6b060 --- /dev/null +++ b/test/mocks/Utils.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include +#include + +#define PUB_KEY_SIZE 32 +#define PRV_KEY_SIZE 64 +#define SIGNATURE_SIZE 64 +#define CIPHER_MAC_SIZE 16 + +namespace mesh { + +class RNG { +public: + virtual ~RNG() = default; + virtual void random(uint8_t* dest, size_t sz) = 0; +}; + +class Utils { +public: + static void sha256(uint8_t* hash, size_t hash_len, const uint8_t*, int) { + std::memset(hash, 0, hash_len); + } + + static int encryptThenMAC(const uint8_t*, uint8_t* dest, const uint8_t* src, int src_len) { + int out_len = src_len + CIPHER_MAC_SIZE; + std::memset(dest, 0xAA, CIPHER_MAC_SIZE); + std::memcpy(dest + CIPHER_MAC_SIZE, src, src_len); + return out_len; + } + + static int MACThenDecrypt(const uint8_t*, uint8_t* dest, const uint8_t* src, int src_len) { + if (src_len < CIPHER_MAC_SIZE) { + return 0; + } + int out_len = src_len - CIPHER_MAC_SIZE; + std::memcpy(dest, src + CIPHER_MAC_SIZE, out_len); + return out_len; + } +}; + +} diff --git a/test/mocks/helpers/SensorManager.h b/test/mocks/helpers/SensorManager.h new file mode 100644 index 00000000..d1a41cb5 --- /dev/null +++ b/test/mocks/helpers/SensorManager.h @@ -0,0 +1,10 @@ +#pragma once + +#include +#include "CayenneLPP.h" + +class SensorManager { +public: + virtual ~SensorManager() = default; + virtual bool querySensors(uint8_t, CayenneLPP&) { return false; } +}; diff --git a/test/test_kiss_modem/test_tx_backpressure.cpp b/test/test_kiss_modem/test_tx_backpressure.cpp new file mode 100644 index 00000000..39a5e3b7 --- /dev/null +++ b/test/test_kiss_modem/test_tx_backpressure.cpp @@ -0,0 +1,294 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "KissModem.h" + +static constexpr int TEST_TX_AVAILABLE_BYTES = 4096; +static constexpr size_t TEST_DEFAULT_MAX_WRITE_CHUNK = SIZE_MAX; +static constexpr size_t TEST_PARTIAL_WRITE_CHUNK = 2; +static constexpr int TEST_PARTIAL_WRITE_FLUSH_LOOPS = 3; +static constexpr uint8_t TEST_SNR = 8; +static constexpr uint8_t TEST_RSSI = 200; + +class BlockingStream : public Stream { +public: + void pushRx(const std::vector& bytes) { + std::lock_guard lock(_mutex); + for (uint8_t b : bytes) { + _rx.push(b); + } + } + + void setBlockWrites(bool blocked) { + { + std::lock_guard lock(_mutex); + _block_writes = blocked; + } + _cv.notify_all(); + } + + bool isWriteBlocked() const { + return _entered_block.load(); + } + + size_t writesCount() const { + std::lock_guard lock(_mutex); + return _writes.size(); + } + + std::vector writesSnapshot() const { + std::lock_guard lock(_mutex); + return _writes; + } + + int availableForWrite() override { + std::lock_guard lock(_mutex); + return _block_writes ? 0 : TEST_TX_AVAILABLE_BYTES; + } + + void setMaxWriteChunk(size_t chunk) { + std::lock_guard lock(_mutex); + _max_write_chunk = chunk; + } + + size_t write(const uint8_t* buffer, size_t size) override { + std::unique_lock lock(_mutex); + while (_block_writes) { + _entered_block.store(true); + _cv.wait(lock); + } + const size_t chunk = (size < _max_write_chunk) ? size : _max_write_chunk; + for (size_t i = 0; i < chunk; i++) { + _writes.push_back(buffer[i]); + } + return chunk; + } + + size_t write(uint8_t b) override { + return write(&b, 1); + } + + int available() override { + std::lock_guard lock(_mutex); + return static_cast(_rx.size()); + } + + int read() override { + std::lock_guard lock(_mutex); + if (_rx.empty()) { + return -1; + } + int b = _rx.front(); + _rx.pop(); + return b; + } + +private: + mutable std::mutex _mutex; + std::condition_variable _cv; + std::queue _rx; + std::vector _writes; + bool _block_writes = false; + std::atomic _entered_block = false; + size_t _max_write_chunk = TEST_DEFAULT_MAX_WRITE_CHUNK; +}; + +class FakeRNG : public mesh::RNG { +public: + void random(uint8_t* dest, size_t sz) override { + for (size_t i = 0; i < sz; i++) { + dest[i] = 0; + } + } +}; + +class FakeRadio : public mesh::Radio { +public: + bool isReceiving() override { return false; } + uint32_t getEstAirtimeFor(uint16_t) override { return 10; } + bool startSendRaw(const uint8_t*, uint16_t) override { + _start_send_count++; + return _start_send_result; + } + bool isSendComplete() override { return _send_complete; } + void onSendFinished() override { _send_finished_count++; } + int16_t getNoiseFloor() override { return -120; } + + void setStartSendResult(bool result) { _start_send_result = result; } + void setSendComplete(bool complete) { _send_complete = complete; } + int startSendCount() const { return _start_send_count; } + int sendFinishedCount() const { return _send_finished_count; } + +private: + bool _start_send_result = true; + bool _send_complete = true; + int _start_send_count = 0; + int _send_finished_count = 0; +}; + +class FakeBoard : public mesh::MainBoard { +public: + uint16_t getBattMilliVolts() override { return 4200; } + float getMCUTemperature() override { return 24.0f; } + const char* getManufacturerName() override { return "test-board"; } + void reboot() override {} +}; + +class FakeSensors : public SensorManager { +public: + bool querySensors(uint8_t, CayenneLPP&) override { return false; } +}; + +class KissModemFixture : public ::testing::Test { +protected: + BlockingStream serial; + mesh::LocalIdentity identity; + FakeRNG rng; + FakeRadio radio; + FakeBoard board; + FakeSensors sensors; + KissModem modem; + + KissModemFixture() + : modem(serial, identity, rng, radio, board, sensors) { + modem.begin(); + } + + static std::vector dataFrame(const std::vector& packet) { + std::vector frame = {KISS_FEND, KISS_CMD_DATA}; + frame.insert(frame.end(), packet.begin(), packet.end()); + frame.push_back(KISS_FEND); + return frame; + } + + void advanceToTxSending() { + modem.loop(); + modem.loop(); + delay((uint32_t)KISS_DEFAULT_TXDELAY * 10); + modem.loop(); + } +}; + +TEST_F(KissModemFixture, PingResponseShouldNotStallLoopUnderTxBackpressure) { + serial.setBlockWrites(true); + serial.pushRx({KISS_FEND, KISS_CMD_SETHARDWARE, HW_CMD_PING, KISS_FEND}); + + auto future = std::async(std::launch::async, [this]() { + modem.loop(); + }); + + auto status = future.wait_for(std::chrono::milliseconds(100)); + EXPECT_EQ(status, std::future_status::ready) << "KissModem::loop blocked in serial write under TX backpressure"; + EXPECT_FALSE(serial.isWriteBlocked()) << "KissModem entered blocking write path"; + + serial.setBlockWrites(false); + future.wait(); + modem.loop(); + EXPECT_GT(serial.writesCount(), 0U) << "KissModem did not flush queued response after backpressure cleared"; +} + +TEST_F(KissModemFixture, PingResponseKeepsStandardKissFraming) { + serial.pushRx({KISS_FEND, KISS_CMD_SETHARDWARE, HW_CMD_PING, KISS_FEND}); + modem.loop(); + + const std::vector expected = {KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP(HW_CMD_PING), KISS_FEND}; + EXPECT_EQ(serial.writesSnapshot(), expected); +} + +TEST_F(KissModemFixture, PingResponseKeepsFramingWithPartialBulkWrites) { + serial.setMaxWriteChunk(TEST_PARTIAL_WRITE_CHUNK); + serial.pushRx({KISS_FEND, KISS_CMD_SETHARDWARE, HW_CMD_PING, KISS_FEND}); + for (int i = 0; i < TEST_PARTIAL_WRITE_FLUSH_LOOPS; i++) { + modem.loop(); + } + + const std::vector expected = {KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP(HW_CMD_PING), KISS_FEND}; + EXPECT_EQ(serial.writesSnapshot(), expected); +} + +TEST_F(KissModemFixture, PacketAndMetaAreQueuedTogetherUnderBackpressure) { + static constexpr uint8_t TEST_PACKET[] = {0x01, 0x02, 0x03}; + + serial.setBlockWrites(true); + modem.onPacketReceived((int8_t)TEST_SNR, (int8_t)TEST_RSSI, TEST_PACKET, sizeof(TEST_PACKET)); + serial.setBlockWrites(false); + modem.loop(); + modem.loop(); + + const std::vector expected = { + KISS_FEND, KISS_CMD_DATA, TEST_PACKET[0], TEST_PACKET[1], TEST_PACKET[2], KISS_FEND, + KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP_RX_META, TEST_SNR, TEST_RSSI, KISS_FEND}; + EXPECT_EQ(serial.writesSnapshot(), expected); +} + +TEST_F(KissModemFixture, RadioTxCompletionAdvancesWhileHostOutputIsBackedUp) { + serial.pushRx(dataFrame({0x42})); + advanceToTxSending(); + ASSERT_EQ(radio.startSendCount(), 1); + + serial.setBlockWrites(true); + modem.loop(); + EXPECT_EQ(radio.sendFinishedCount(), 1); + EXPECT_TRUE(modem.isTxBusy()); + + serial.setBlockWrites(false); + modem.loop(); + + const std::vector expected = {KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP_TX_DONE, 0x01, KISS_FEND}; + EXPECT_EQ(serial.writesSnapshot(), expected); + EXPECT_FALSE(modem.isTxBusy()); +} + +TEST_F(KissModemFixture, QueueFullReportsBusyWithoutDroppingQueuedFrames) { + static constexpr uint8_t TEST_PACKET_ONE[] = {0x11, 0x12}; + static constexpr uint8_t TEST_PACKET_TWO[] = {0x21, 0x22}; + + serial.setBlockWrites(true); + modem.onPacketReceived((int8_t)TEST_SNR, (int8_t)TEST_RSSI, TEST_PACKET_ONE, sizeof(TEST_PACKET_ONE)); + modem.onPacketReceived((int8_t)TEST_SNR, (int8_t)TEST_RSSI, TEST_PACKET_TWO, sizeof(TEST_PACKET_TWO)); + serial.setBlockWrites(false); + modem.loop(); + + const std::vector expected = { + KISS_FEND, KISS_CMD_DATA, TEST_PACKET_ONE[0], TEST_PACKET_ONE[1], KISS_FEND, + KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP_RX_META, TEST_SNR, TEST_RSSI, KISS_FEND, + KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP_ERROR, HW_ERR_TX_BUSY, KISS_FEND}; + EXPECT_EQ(serial.writesSnapshot(), expected); +} + +TEST_F(KissModemFixture, QueuedEncoderEscapesKissSpecialBytes) { + static constexpr uint8_t TEST_PACKET[] = {KISS_FEND, KISS_FESC, 0x01}; + + modem.onPacketReceived((int8_t)TEST_SNR, (int8_t)TEST_RSSI, TEST_PACKET, sizeof(TEST_PACKET)); + + const std::vector expected = { + KISS_FEND, KISS_CMD_DATA, KISS_FESC, KISS_TFEND, KISS_FESC, KISS_TFESC, 0x01, KISS_FEND, + KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP_RX_META, TEST_SNR, TEST_RSSI, KISS_FEND}; + EXPECT_EQ(serial.writesSnapshot(), expected); +} + +TEST_F(KissModemFixture, MaxPacketWorstCaseEscapingFitsQueuedFrame) { + std::vector packet(KISS_MAX_PACKET_SIZE, KISS_FEND); + std::vector expected = {KISS_FEND, KISS_CMD_DATA}; + for (size_t i = 0; i < packet.size(); i++) { + expected.push_back(KISS_FESC); + expected.push_back(KISS_TFEND); + } + expected.push_back(KISS_FEND); + expected.insert(expected.end(), {KISS_FEND, KISS_CMD_SETHARDWARE, HW_RESP_RX_META, TEST_SNR, TEST_RSSI, KISS_FEND}); + + modem.onPacketReceived((int8_t)TEST_SNR, (int8_t)TEST_RSSI, packet.data(), packet.size()); + EXPECT_EQ(serial.writesSnapshot(), expected); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/variants/heltec_v4/platformio.ini b/variants/heltec_v4/platformio.ini index fabf3827..cb76beb3 100644 --- a/variants/heltec_v4/platformio.ini +++ b/variants/heltec_v4/platformio.ini @@ -432,5 +432,10 @@ lib_deps = [env:heltec_v4_kiss_modem] extends = Heltec_lora32_v4 +build_unflags = + -DARDUINO_USB_MODE=0 +build_flags = + ${Heltec_lora32_v4.build_flags} + -DARDUINO_USB_MODE=1 build_src_filter = ${Heltec_lora32_v4.build_src_filter} +<../examples/kiss_modem/> diff --git a/variants/station_g2/platformio.ini b/variants/station_g2/platformio.ini index 6432b523..508ffe4b 100644 --- a/variants/station_g2/platformio.ini +++ b/variants/station_g2/platformio.ini @@ -241,5 +241,10 @@ lib_deps = [env:Station_G2_kiss_modem] extends = Station_G2 +build_unflags = + -DARDUINO_USB_MODE=0 +build_flags = + ${Station_G2.build_flags} + -DARDUINO_USB_MODE=1 build_src_filter = ${Station_G2.build_src_filter} +<../examples/kiss_modem/> From 39ff5b879acadb124ee535fcf2386a83cfff08e0 Mon Sep 17 00:00:00 2001 From: ViezeVingertjes Date: Tue, 23 Jun 2026 15:15:13 +0200 Subject: [PATCH 24/70] fix: run KISS modem unit tests in CI via native_kiss_modem env The new test_kiss_modem suite needs KissModem headers and sources that only native_kiss_modem provides. Scope native to test_utils and run both envs in the unit test workflow. --- .github/workflows/run-unit-tests.yml | 2 +- platformio.ini | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index e3af3aaf..5d48f4c6 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -20,7 +20,7 @@ jobs: uses: ./.github/actions/setup-build-environment - name: Run Unit Tests - run: pio test -e native -vv + run: pio test -e native -e native_kiss_modem -vv - name: Upload Test Results # Upload test results even if the test step failed. diff --git a/platformio.ini b/platformio.ini index 2de71a37..cdd67d27 100644 --- a/platformio.ini +++ b/platformio.ini @@ -163,6 +163,7 @@ build_flags = -std=c++17 -I src -I test/mocks test_build_src = yes +test_filter = test_utils build_src_filter = -<*> +<../src/Utils.cpp> From 3b69e7873bd76b52c3d3469d7496ff64a9048db5 Mon Sep 17 00:00:00 2001 From: ViezeVingertjes Date: Fri, 26 Jun 2026 00:11:33 +0200 Subject: [PATCH 25/70] fix: guard HWCDC serial config behind ARDUINO_USB_MODE setTxTimeoutMs() and setTxBufferSize() are HWCDC-only APIs; calling them on HardwareSerial (UART-bridge boards like T-Beam) fails to compile. Co-Authored-By: Adam Gessaman --- examples/kiss_modem/main.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/kiss_modem/main.cpp b/examples/kiss_modem/main.cpp index 95a06ee6..5836a694 100644 --- a/examples/kiss_modem/main.cpp +++ b/examples/kiss_modem/main.cpp @@ -113,11 +113,9 @@ void setup() { uint32_t start = millis(); while (!Serial && millis() - start < 3000) delay(10); delay(100); -#if defined(ESP32) +#if defined(ESP32) && ARDUINO_USB_MODE Serial.setTxTimeoutMs(USB_TX_TIMEOUT_MS); -#if ARDUINO_USB_MODE Serial.setTxBufferSize(USB_TX_BUFFER_SIZE); -#endif #endif modem = new KissModem(Serial, identity, rng, radio_driver, board, sensors); #endif From b28bf00348c1cf555a88a5988bd0e889958a2517 Mon Sep 17 00:00:00 2001 From: taco Date: Sun, 19 Jul 2026 22:06:36 +1000 Subject: [PATCH 26/70] fix: setMaxPacketMillis() should return void --- src/helpers/radiolib/CustomLR1110.h | 2 +- src/helpers/radiolib/CustomSX1262.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/helpers/radiolib/CustomLR1110.h b/src/helpers/radiolib/CustomLR1110.h index db0f2af3..87a7005f 100644 --- a/src/helpers/radiolib/CustomLR1110.h +++ b/src/helpers/radiolib/CustomLR1110.h @@ -72,7 +72,7 @@ class CustomLR1110 : public LR1110 { return false; } - PacketMillis setMaxPacketMillis(PacketMillis maxPacketMillis) { + void setMaxPacketMillis(PacketMillis maxPacketMillis) { MESH_DEBUG_PRINTLN("Setting _preambleMillis=%u, _maxPacketMillis=%u", maxPacketMillis.preambleMillis, maxPacketMillis.payloadMillis); _preambleMillis = maxPacketMillis.preambleMillis; _maxPayloadMillis = maxPacketMillis.payloadMillis; diff --git a/src/helpers/radiolib/CustomSX1262.h b/src/helpers/radiolib/CustomSX1262.h index 284b79c0..18932488 100644 --- a/src/helpers/radiolib/CustomSX1262.h +++ b/src/helpers/radiolib/CustomSX1262.h @@ -137,7 +137,7 @@ class CustomSX1262 : public SX1262 { return false; } - PacketMillis setMaxPacketMillis(PacketMillis maxPacketMillis) { + void setMaxPacketMillis(PacketMillis maxPacketMillis) { MESH_DEBUG_PRINTLN("Setting _preambleMillis=%u, _maxPacketMillis=%u", maxPacketMillis.preambleMillis, maxPacketMillis.payloadMillis); _preambleMillis = maxPacketMillis.preambleMillis; _maxPayloadMillis = maxPacketMillis.payloadMillis; From aee53a8c3e3176eb9e26fd781774f198d4bb6f30 Mon Sep 17 00:00:00 2001 From: entr0p1 <1475255+entr0p1@users.noreply.github.com> Date: Sun, 19 Jul 2026 23:59:12 +1000 Subject: [PATCH 27/70] NRF52Board - ShutdownPeripherals() housekeeping Improvements: - Add condition to only turn off display if on (avoids wedging the shutdown on some displays) - Reset LoRa radio before calling radio_driver.powerOff() to ensure it is in a known good state (and will accept the command) - Ensure SPI is started so LoRa radio commands can be sent (some shutdowns can happen before this) - Add gate for P_LORA_NSS pin defined before writing to the pin --- src/helpers/NRF52Board.cpp | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index 5fb1e55e..23f7cafc 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -314,14 +314,38 @@ float NRF52Board::getMCUTemperature() { void NRF52Board::shutdownPeripherals() { // Power off the display if any #ifdef DISPLAY_CLASS - display.turnOff(); + if (display.isOn()) { + display.turnOff(); + } #endif - + // Prep LoRa radio for power down + #ifdef P_LORA_RESET + digitalWrite(P_LORA_RESET, HIGH); // preload OUT latch so pinMode can't glitch NRESET low + pinMode(P_LORA_RESET, OUTPUT); + digitalWrite(P_LORA_RESET, LOW); // deliberate hardware reset (datasheet: >=100us) + delayMicroseconds(200); + digitalWrite(P_LORA_RESET, HIGH); + #endif + #if defined(P_LORA_SCLK) && defined(P_LORA_MISO) && defined(P_LORA_MOSI) + SPI.setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI); + SPI.begin(); // SPI may not be started on some shutdown paths, need it to shut down radio + #endif + #ifdef P_LORA_BUSY + pinMode(P_LORA_BUSY, INPUT); + uint32_t started_at = millis(); + while (digitalRead(P_LORA_BUSY) && millis() - started_at < 10) {} //wait for radio to be ready + #endif + #ifdef P_LORA_NSS + pinMode(P_LORA_NSS, OUTPUT); + digitalWrite(P_LORA_NSS, HIGH); + #endif // Power off LoRa radio_driver.powerOff(); // Keep LoRa inactive during deepsleep - digitalWrite(P_LORA_NSS, HIGH); + #ifdef P_LORA_NSS + digitalWrite(P_LORA_NSS, HIGH); + #endif // Power off GPS if any if(sensors.getLocationProvider() != NULL) { From cc221330ba1dbac71d2214dbcdb3699d74b02e83 Mon Sep 17 00:00:00 2001 From: Ryan Gregg Date: Sun, 19 Jul 2026 10:17:08 -0700 Subject: [PATCH 28/70] Fix nRF52 builds: guard SerialEthernetInterface with ETHERNET_ENABLED SerialEthernetInterface.cpp is compiled for every nRF52 target because PlatformIO builds all .cpp files under src/. It includes SerialEthernetInterface.h, which unconditionally pulls in -- a library only present in the RAK4631 Ethernet env's lib_deps. As a result any other nRF52 board (e.g. Heltec T114) fails to build with 'RAK13800_W5100S.h: No such file or directory'. Wrap the contents of both files in '#ifdef ETHERNET_ENABLED' so they compile to empty translation units on non-Ethernet builds. RAK4631 Ethernet envs define ETHERNET_ENABLED and are unaffected. Fixes #2985 --- src/helpers/nrf52/SerialEthernetInterface.cpp | 4 ++++ src/helpers/nrf52/SerialEthernetInterface.h | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/helpers/nrf52/SerialEthernetInterface.cpp b/src/helpers/nrf52/SerialEthernetInterface.cpp index 70891023..36a998a4 100644 --- a/src/helpers/nrf52/SerialEthernetInterface.cpp +++ b/src/helpers/nrf52/SerialEthernetInterface.cpp @@ -1,3 +1,5 @@ +#ifdef ETHERNET_ENABLED + #include "SerialEthernetInterface.h" #include "EthernetMac.h" #include @@ -262,3 +264,5 @@ bool SerialEthernetInterface::isConnected() const { void SerialEthernetInterface::loop() { Ethernet.maintain(); } + +#endif // ETHERNET_ENABLED diff --git a/src/helpers/nrf52/SerialEthernetInterface.h b/src/helpers/nrf52/SerialEthernetInterface.h index 95ce8a52..b8b4e94b 100644 --- a/src/helpers/nrf52/SerialEthernetInterface.h +++ b/src/helpers/nrf52/SerialEthernetInterface.h @@ -1,5 +1,7 @@ #pragma once +#ifdef ETHERNET_ENABLED + #include "helpers/BaseSerialInterface.h" #include #include @@ -76,3 +78,5 @@ class SerialEthernetInterface : public BaseSerialInterface { #define ETHERNET_DEBUG_PRINTLN(...) {} #define ETHERNET_DEBUG_PRINT_IP(...) {} #endif + +#endif // ETHERNET_ENABLED From ae48072ff059c53c94803cd9e6583c763a13a703 Mon Sep 17 00:00:00 2001 From: taco Date: Mon, 20 Jul 2026 19:40:49 +1000 Subject: [PATCH 29/70] SX1262: add setter methods for preambleMillis and payloadMillis --- src/helpers/radiolib/CustomSX1262.h | 11 +++++++---- src/helpers/radiolib/CustomSX1262Wrapper.h | 4 +++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/helpers/radiolib/CustomSX1262.h b/src/helpers/radiolib/CustomSX1262.h index 18932488..103f5167 100644 --- a/src/helpers/radiolib/CustomSX1262.h +++ b/src/helpers/radiolib/CustomSX1262.h @@ -137,10 +137,13 @@ class CustomSX1262 : public SX1262 { return false; } - void setMaxPacketMillis(PacketMillis maxPacketMillis) { - MESH_DEBUG_PRINTLN("Setting _preambleMillis=%u, _maxPacketMillis=%u", maxPacketMillis.preambleMillis, maxPacketMillis.payloadMillis); - _preambleMillis = maxPacketMillis.preambleMillis; - _maxPayloadMillis = maxPacketMillis.payloadMillis; + void setPreambleMillis(uint32_t preambleMillis) { + _preambleMillis = preambleMillis; + MESH_DEBUG_PRINTLN("Set _preambleMillis=%u", _preambleMillis); + } + void setMaxPayloadMillis(uint32_t payloadMillis) { + _maxPayloadMillis = payloadMillis; + MESH_DEBUG_PRINTLN("Set _maxPayloadMillis=%u", _maxPayloadMillis); } bool getRxBoostedGainMode() { diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index 45af4cb4..bfea50ec 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -18,7 +18,9 @@ public: ((CustomSX1262 *)_radio)->setBandwidth(bw); ((CustomSX1262 *)_radio)->setCodingRate(cr); updatePreamble(sf); - ((CustomSX1262 *)_radio)->setMaxPacketMillis(calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf))); + PacketMillis pm = calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf)); + ((CustomSX1262 *)_radio)->setPreambleMillis(pm.preambleMillis); + ((CustomSX1262 *)_radio)->setMaxPayloadMillis(pm.payloadMillis); } bool isReceivingPacket() override { From 121b72712b361eaa5685f8c90144e5fccad5690d Mon Sep 17 00:00:00 2001 From: taco Date: Mon, 20 Jul 2026 19:42:22 +1000 Subject: [PATCH 30/70] LR1110: add setter methods for preambleMillis and payloadMillis --- src/helpers/radiolib/CustomLR1110.h | 13 ++++++++----- src/helpers/radiolib/CustomLR1110Wrapper.h | 5 +++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/helpers/radiolib/CustomLR1110.h b/src/helpers/radiolib/CustomLR1110.h index 87a7005f..0470e48f 100644 --- a/src/helpers/radiolib/CustomLR1110.h +++ b/src/helpers/radiolib/CustomLR1110.h @@ -71,11 +71,14 @@ class CustomLR1110 : public LR1110 { _activityAt = 0; _headerSeen = false; return false; } - - void setMaxPacketMillis(PacketMillis maxPacketMillis) { - MESH_DEBUG_PRINTLN("Setting _preambleMillis=%u, _maxPacketMillis=%u", maxPacketMillis.preambleMillis, maxPacketMillis.payloadMillis); - _preambleMillis = maxPacketMillis.preambleMillis; - _maxPayloadMillis = maxPacketMillis.payloadMillis; + + void setPreambleMillis(uint32_t preambleMillis) { + _preambleMillis = preambleMillis; + MESH_DEBUG_PRINTLN("Set _preambleMillis=%u", _preambleMillis); + } + void setMaxPayloadMillis(uint32_t payloadMillis) { + _maxPayloadMillis = payloadMillis; + MESH_DEBUG_PRINTLN("Set _maxPayloadMillis=%u", _maxPayloadMillis); } uint8_t getSpreadingFactor() const { return spreadingFactor; } diff --git a/src/helpers/radiolib/CustomLR1110Wrapper.h b/src/helpers/radiolib/CustomLR1110Wrapper.h index 981faf09..44230c61 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -14,8 +14,9 @@ public: ((CustomLR1110 *)_radio)->setBandwidth(bw); ((CustomLR1110 *)_radio)->setCodingRate(cr); updatePreamble(sf); - ((CustomLR1110 *)_radio)->setMaxPacketMillis(calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf))); - + PacketMillis pm = calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf)); + ((CustomLR1110 *)_radio)->setPreambleMillis(pm.preambleMillis); + ((CustomLR1110 *)_radio)->setMaxPayloadMillis(pm.payloadMillis); } void doResetAGC() override { lr11x0ResetAGC((LR11x0 *)_radio, ((CustomLR1110 *)_radio)->getFreqMHz()); } From 91e65af9dd8514627b320f19a442100bafb868d7 Mon Sep 17 00:00:00 2001 From: taco Date: Mon, 20 Jul 2026 20:28:38 +1000 Subject: [PATCH 31/70] fix: heltec t1: add missing bodmer TFT_eSPI library --- variants/heltec_t1/platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/heltec_t1/platformio.ini b/variants/heltec_t1/platformio.ini index d5c3caf1..eaa8af37 100644 --- a/variants/heltec_t1/platformio.ini +++ b/variants/heltec_t1/platformio.ini @@ -23,7 +23,7 @@ build_src_filter = ${nrf52_base.build_src_filter} lib_deps = ${nrf52_base.lib_deps} ${sensor_base.lib_deps} - adafruit/Adafruit ST7735 and ST7789 Library @ ^1.11.0 + bodmer/TFT_eSPI @ ^2.4.31 debug_tool = jlink upload_protocol = nrfutil From ab5c45f51612c268b7fa4dca423d5b3dab88df8e Mon Sep 17 00:00:00 2001 From: taco Date: Mon, 20 Jul 2026 21:05:12 +1000 Subject: [PATCH 32/70] fix: clear the correct IRQ flag for timeout on preamble detected --- src/helpers/radiolib/CustomSX1262.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/radiolib/CustomSX1262.h b/src/helpers/radiolib/CustomSX1262.h index 103f5167..15efed5c 100644 --- a/src/helpers/radiolib/CustomSX1262.h +++ b/src/helpers/radiolib/CustomSX1262.h @@ -125,7 +125,7 @@ class CustomSX1262 : public SX1262 { if (preamble) { if (_activityAt == 0) _activityAt = now; if (now - _activityAt > _preambleMillis) { - clearIrqStatus(RADIOLIB_IRQ_PREAMBLE_DETECTED); + clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED); _activityAt = 0; MESH_DEBUG_PRINTLN("Clearing preamble IRQ after %ums", _preambleMillis); From d2bbe4d8d3629f1d44b27b8af0264799ce54c63e Mon Sep 17 00:00:00 2001 From: entr0p1 <1475255+entr0p1@users.noreply.github.com> Date: Mon, 20 Jul 2026 22:43:25 +1000 Subject: [PATCH 33/70] Fix Xiao nRF52840 repeater compile failure Compile fails due to user_btn not declared in board target. Fixes: - Add MomentaryButton declaration for user_btn in variants/xiao_nrf52/target.cpp - Add momentary button helper include in variants/xiao_nrf52/target.h - Add extern linkage for MomentaryButton -> user_btn in variants/xiao_nrf52/target.h --- variants/xiao_nrf52/XiaoNrf52Board.h | 2 +- variants/xiao_nrf52/target.cpp | 1 + variants/xiao_nrf52/target.h | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/variants/xiao_nrf52/XiaoNrf52Board.h b/variants/xiao_nrf52/XiaoNrf52Board.h index b2638a44..e71e7f65 100644 --- a/variants/xiao_nrf52/XiaoNrf52Board.h +++ b/variants/xiao_nrf52/XiaoNrf52Board.h @@ -47,7 +47,7 @@ public: #ifdef PIN_USER_BTN // configure button press to wake up when in powered off state - nrf_gpio_cfg_sense_input(digitalPinToInterrupt(g_ADigitalPinMap[PIN_USER_BTN]), NRF_GPIO_PIN_NOPULL, NRF_GPIO_PIN_SENSE_LOW); + nrf_gpio_cfg_sense_input(digitalPinToInterrupt(g_ADigitalPinMap[PIN_USER_BTN]), NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW); #endif NRF52Board::powerOff(); diff --git a/variants/xiao_nrf52/target.cpp b/variants/xiao_nrf52/target.cpp index ab6fe279..def1dbbe 100644 --- a/variants/xiao_nrf52/target.cpp +++ b/variants/xiao_nrf52/target.cpp @@ -4,6 +4,7 @@ #ifdef DISPLAY_CLASS DISPLAY_CLASS display; + MomentaryButton user_btn(PIN_USER_BTN, 1000, true, true); #endif XiaoNrf52Board board; diff --git a/variants/xiao_nrf52/target.h b/variants/xiao_nrf52/target.h index bb3d2a81..20093904 100644 --- a/variants/xiao_nrf52/target.h +++ b/variants/xiao_nrf52/target.h @@ -11,7 +11,9 @@ #ifdef DISPLAY_CLASS #include + #include extern DISPLAY_CLASS display; + extern MomentaryButton user_btn; #endif extern XiaoNrf52Board board; From 3885c67c8eaf46ce66e28252338df783ca178a95 Mon Sep 17 00:00:00 2001 From: Alexander Hoffer Date: Mon, 20 Jul 2026 15:47:03 +0100 Subject: [PATCH 34/70] fix: synchronize BLE receive queue --- src/helpers/BoundedQueue.h | 39 +++++++++++++ src/helpers/esp32/SerialBLEInterface.cpp | 41 ++++++++----- src/helpers/esp32/SerialBLEInterface.h | 10 ++-- .../test_bounded_queue/test_bounded_queue.cpp | 58 +++++++++++++++++++ 4 files changed, 129 insertions(+), 19 deletions(-) create mode 100644 src/helpers/BoundedQueue.h create mode 100644 test/test_bounded_queue/test_bounded_queue.cpp diff --git a/src/helpers/BoundedQueue.h b/src/helpers/BoundedQueue.h new file mode 100644 index 00000000..07ecb4ff --- /dev/null +++ b/src/helpers/BoundedQueue.h @@ -0,0 +1,39 @@ +#pragma once + +#include + +namespace mesh { + +template +class BoundedQueue { + static_assert(Capacity > 0, "BoundedQueue capacity must be greater than zero"); + + T _items[Capacity]; + size_t _head = 0; + size_t _size = 0; + +public: + bool push(const T& item) { + if (_size == Capacity) return false; + _items[(_head + _size) % Capacity] = item; + _size++; + return true; + } + + bool pop(T& item) { + if (_size == 0) return false; + item = _items[_head]; + _head = (_head + 1) % Capacity; + _size--; + return true; + } + + void clear() { + _head = 0; + _size = 0; + } + + size_t size() const { return _size; } +}; + +} // namespace mesh diff --git a/src/helpers/esp32/SerialBLEInterface.cpp b/src/helpers/esp32/SerialBLEInterface.cpp index dcfa0e1e..9c81dc7b 100644 --- a/src/helpers/esp32/SerialBLEInterface.cpp +++ b/src/helpers/esp32/SerialBLEInterface.cpp @@ -118,17 +118,30 @@ void SerialBLEInterface::onWrite(BLECharacteristic* pCharacteristic, esp_ble_gat if (len > MAX_FRAME_SIZE) { BLE_DEBUG_PRINTLN("ERROR: onWrite(), frame too big, len=%d", len); - } else if (recv_queue_len >= FRAME_QUEUE_SIZE) { - BLE_DEBUG_PRINTLN("ERROR: onWrite(), recv_queue is full!"); } else { - recv_queue[recv_queue_len].len = len; - memcpy(recv_queue[recv_queue_len].buf, rxValue, len); - recv_queue_len++; + Frame frame = {}; + frame.len = len; + memcpy(frame.buf, rxValue, len); + + portENTER_CRITICAL(&recv_queue_mux); + bool queued = recv_queue.push(frame); + portEXIT_CRITICAL(&recv_queue_mux); + + if (!queued) { + BLE_DEBUG_PRINTLN("ERROR: onWrite(), recv_queue is full!"); + } } } // ---------- public methods +void SerialBLEInterface::clearBuffers() { + portENTER_CRITICAL(&recv_queue_mux); + recv_queue.clear(); + portEXIT_CRITICAL(&recv_queue_mux); + send_queue_len = 0; +} + void SerialBLEInterface::enable() { if (_isEnabled) return; @@ -202,17 +215,15 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) { } } - if (recv_queue_len > 0) { // check recv queue - size_t len = recv_queue[0].len; // take from top of queue - memcpy(dest, recv_queue[0].buf, len); + Frame frame = {}; + portENTER_CRITICAL(&recv_queue_mux); + bool received = recv_queue.pop(frame); + portEXIT_CRITICAL(&recv_queue_mux); - BLE_DEBUG_PRINTLN("readBytes: sz=%d, hdr=%d", len, (uint32_t) dest[0]); - - recv_queue_len--; - for (int i = 0; i < recv_queue_len; i++) { // delete top item from queue - recv_queue[i] = recv_queue[i + 1]; - } - return len; + if (received) { + memcpy(dest, frame.buf, frame.len); + BLE_DEBUG_PRINTLN("readBytes: sz=%d, hdr=%d", (uint32_t) frame.len, (uint32_t) dest[0]); + return frame.len; } if (pServer->getConnectedCount() == 0) deviceConnected = false; diff --git a/src/helpers/esp32/SerialBLEInterface.h b/src/helpers/esp32/SerialBLEInterface.h index 965e90fd..e2d600a0 100644 --- a/src/helpers/esp32/SerialBLEInterface.h +++ b/src/helpers/esp32/SerialBLEInterface.h @@ -1,10 +1,12 @@ #pragma once #include "../BaseSerialInterface.h" +#include "../BoundedQueue.h" #include #include #include #include +#include class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLEServerCallbacks, BLECharacteristicCallbacks { BLEServer *pServer; @@ -24,12 +26,12 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE }; #define FRAME_QUEUE_SIZE 4 - int recv_queue_len; - Frame recv_queue[FRAME_QUEUE_SIZE]; + portMUX_TYPE recv_queue_mux = portMUX_INITIALIZER_UNLOCKED; + mesh::BoundedQueue recv_queue; int send_queue_len; Frame send_queue[FRAME_QUEUE_SIZE]; - void clearBuffers() { recv_queue_len = 0; send_queue_len = 0; } + void clearBuffers(); protected: // BLESecurityCallbacks methods @@ -58,7 +60,7 @@ public: _isEnabled = false; _last_write = 0; last_conn_id = 0; - send_queue_len = recv_queue_len = 0; + send_queue_len = 0; } /** diff --git a/test/test_bounded_queue/test_bounded_queue.cpp b/test/test_bounded_queue/test_bounded_queue.cpp new file mode 100644 index 00000000..3e2e2de7 --- /dev/null +++ b/test/test_bounded_queue/test_bounded_queue.cpp @@ -0,0 +1,58 @@ +#include + +#include + +TEST(BoundedQueue, PreservesOrderAcrossWraparound) { + mesh::BoundedQueue queue; + int value = 0; + + for (int i = 0; i < 4; i++) EXPECT_TRUE(queue.push(i)); + EXPECT_FALSE(queue.push(4)); + + EXPECT_TRUE(queue.pop(value)); + EXPECT_EQ(0, value); + EXPECT_TRUE(queue.pop(value)); + EXPECT_EQ(1, value); + + EXPECT_TRUE(queue.push(4)); + EXPECT_TRUE(queue.push(5)); + + for (int expected = 2; expected < 6; expected++) { + EXPECT_TRUE(queue.pop(value)); + EXPECT_EQ(expected, value); + } + EXPECT_FALSE(queue.pop(value)); +} + +TEST(BoundedQueue, SurvivesRepeatedFillAndDrainCycles) { + mesh::BoundedQueue queue; + int value = 0; + + for (int cycle = 0; cycle < 1000; cycle++) { + for (int i = 0; i < 4; i++) EXPECT_TRUE(queue.push(cycle * 4 + i)); + for (int i = 0; i < 4; i++) { + EXPECT_TRUE(queue.pop(value)); + EXPECT_EQ(cycle * 4 + i, value); + } + } +} + +TEST(BoundedQueue, ClearDropsPendingItems) { + mesh::BoundedQueue queue; + int value = 0; + + EXPECT_TRUE(queue.push(1)); + EXPECT_TRUE(queue.push(2)); + queue.clear(); + + EXPECT_EQ(0u, queue.size()); + EXPECT_FALSE(queue.pop(value)); + EXPECT_TRUE(queue.push(3)); + EXPECT_TRUE(queue.pop(value)); + EXPECT_EQ(3, value); +} + +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} From 60030469c93c592bddb5f1a4123717484a7db857 Mon Sep 17 00:00:00 2001 From: Alexander Hoffer Date: Mon, 20 Jul 2026 15:47:03 +0100 Subject: [PATCH 35/70] fix: enable Heltec V4 USB serial --- boards/heltec_v4.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/boards/heltec_v4.json b/boards/heltec_v4.json index 36cdfc04..0d3a52cd 100644 --- a/boards/heltec_v4.json +++ b/boards/heltec_v4.json @@ -9,7 +9,7 @@ "extra_flags": [ "-DBOARD_HAS_PSRAM", "-DARDUINO_USB_CDC_ON_BOOT=1", - "-DARDUINO_USB_MODE=0", + "-DARDUINO_USB_MODE=1", "-DARDUINO_RUNNING_CORE=1", "-DARDUINO_EVENT_RUNNING_CORE=1" ], @@ -40,4 +40,4 @@ }, "url": "https://heltec.org/", "vendor": "heltec" -} \ No newline at end of file +} From 9fd6b419e5d134811e1775e7e3d129df9b83f12f Mon Sep 17 00:00:00 2001 From: Alexander Hoffer Date: Mon, 20 Jul 2026 15:47:03 +0100 Subject: [PATCH 36/70] fix: preserve bonded BLE reconnects --- src/helpers/esp32/SerialBLEInterface.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/helpers/esp32/SerialBLEInterface.cpp b/src/helpers/esp32/SerialBLEInterface.cpp index dcfa0e1e..c1c04602 100644 --- a/src/helpers/esp32/SerialBLEInterface.cpp +++ b/src/helpers/esp32/SerialBLEInterface.cpp @@ -103,10 +103,9 @@ void SerialBLEInterface::onMtuChanged(BLEServer* pServer, esp_ble_gatts_cb_param void SerialBLEInterface::onDisconnect(BLEServer* pServer) { BLE_DEBUG_PRINTLN("onDisconnect()"); + deviceConnected = false; if (_isEnabled) { adv_restart_time = millis() + ADVERT_RESTART_DELAY; - - // loop() will detect this on next loop, and set deviceConnected to false } } @@ -215,8 +214,6 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) { return len; } - if (pServer->getConnectedCount() == 0) deviceConnected = false; - if (deviceConnected != oldDeviceConnected) { if (!deviceConnected) { // disconnecting clearBuffers(); From 79dc1de6fc4a371d48f677a2de574d250fc0d7b2 Mon Sep 17 00:00:00 2001 From: Alexander Hoffer Date: Mon, 20 Jul 2026 15:47:03 +0100 Subject: [PATCH 37/70] fix: preserve UTF-8 advert names --- docs/cli_commands.md | 2 +- src/helpers/AdvertDataHelpers.cpp | 14 ++--- src/helpers/UTF8Helpers.h | 56 ++++++++++++++++++++ test/test_utf8_helpers/test_utf8_helpers.cpp | 38 +++++++++++++ 4 files changed, 103 insertions(+), 7 deletions(-) create mode 100644 src/helpers/UTF8Helpers.h create mode 100644 test/test_utf8_helpers/test_utf8_helpers.cpp diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 5598dba5..b618ae2b 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -305,7 +305,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore **Default:** Varies by board -**Note:** Max length varies. If a location is set, the max length is 24 bytes; 32 otherwise. Emoji and unicode characters may take more than one byte. +**Note:** Advertised names can use up to 23 bytes when location is included and 31 bytes otherwise. Emoji and Unicode characters may take more than one byte. Names that exceed the available advert space are truncated at a valid UTF-8 code point boundary. --- diff --git a/src/helpers/AdvertDataHelpers.cpp b/src/helpers/AdvertDataHelpers.cpp index 0e05620e..998733ae 100644 --- a/src/helpers/AdvertDataHelpers.cpp +++ b/src/helpers/AdvertDataHelpers.cpp @@ -1,4 +1,5 @@ #include +#include uint8_t AdvertDataBuilder::encodeTo(uint8_t app_data[]) { app_data[0] = _type; @@ -16,11 +17,12 @@ app_data[0] |= ADV_FEAT2_MASK; memcpy(&app_data[i], &_extra2, 2); i += 2; } - if (_name && *_name != 0) { - app_data[0] |= ADV_NAME_MASK; - const char* sp = _name; - while (*sp && i < MAX_ADVERT_DATA_SIZE) { - app_data[i++] = *sp++; + if (_name && *_name != 0) { + size_t name_len = mesh::validUtf8PrefixLength(_name, MAX_ADVERT_DATA_SIZE - i); + if (name_len > 0) { + app_data[0] |= ADV_NAME_MASK; + memcpy(&app_data[i], _name, name_len); + i += name_len; } } return i; @@ -84,4 +86,4 @@ void AdvertTimeHelper::formatRelativeTimeDiff(char dest[], int32_t seconds_from_ } } } -} \ No newline at end of file +} diff --git a/src/helpers/UTF8Helpers.h b/src/helpers/UTF8Helpers.h new file mode 100644 index 00000000..e06cf4a6 --- /dev/null +++ b/src/helpers/UTF8Helpers.h @@ -0,0 +1,56 @@ +#pragma once + +#include +#include + +namespace mesh { + +inline bool isUtf8Continuation(uint8_t byte) { + return (byte & 0xC0) == 0x80; +} + +inline size_t validUtf8PrefixLength(const char* text, size_t max_bytes) { + if (text == nullptr) return 0; + + size_t offset = 0; + while (text[offset] != '\0') { + const uint8_t first = static_cast(text[offset]); + size_t sequence_length = 0; + + if (first <= 0x7F) { + sequence_length = 1; + } else if (first >= 0xC2 && first <= 0xDF) { + sequence_length = 2; + } else if (first >= 0xE0 && first <= 0xEF) { + sequence_length = 3; + } else if (first >= 0xF0 && first <= 0xF4) { + sequence_length = 4; + } else { + break; + } + + if (offset + sequence_length > max_bytes) break; + + bool complete = true; + for (size_t i = 1; i < sequence_length; i++) { + if (text[offset + i] == '\0' || !isUtf8Continuation(static_cast(text[offset + i]))) { + complete = false; + break; + } + } + if (!complete) break; + + if (sequence_length == 3) { + const uint8_t second = static_cast(text[offset + 1]); + if ((first == 0xE0 && second < 0xA0) || (first == 0xED && second > 0x9F)) break; + } else if (sequence_length == 4) { + const uint8_t second = static_cast(text[offset + 1]); + if ((first == 0xF0 && second < 0x90) || (first == 0xF4 && second > 0x8F)) break; + } + + offset += sequence_length; + } + return offset; +} + +} // namespace mesh diff --git a/test/test_utf8_helpers/test_utf8_helpers.cpp b/test/test_utf8_helpers/test_utf8_helpers.cpp new file mode 100644 index 00000000..ee953522 --- /dev/null +++ b/test/test_utf8_helpers/test_utf8_helpers.cpp @@ -0,0 +1,38 @@ +#include + +#include + +TEST(UTF8Helpers, KeepsCompleteNameWithinLimit) { + const char* name = "Example RPT 🔋🇵🇱"; + + EXPECT_EQ(24u, mesh::validUtf8PrefixLength(name, 24)); +} + +TEST(UTF8Helpers, StopsBeforeCodePointCrossingLimit) { + const char* name = "Example RPT 🔋🇵🇱"; + + EXPECT_EQ(20u, mesh::validUtf8PrefixLength(name, 23)); +} + +TEST(UTF8Helpers, RejectsMalformedAndTruncatedSequences) { + const char overlong[] = {'A', static_cast(0xC0), static_cast(0xAF), 0}; + const char surrogate[] = {'A', static_cast(0xED), static_cast(0xA0), static_cast(0x80), 0}; + const char out_of_range[] = {'A', static_cast(0xF4), static_cast(0x90), static_cast(0x80), static_cast(0x80), 0}; + const char truncated[] = {'A', static_cast(0xF0), static_cast(0x9F), 0}; + + EXPECT_EQ(1u, mesh::validUtf8PrefixLength(overlong, sizeof(overlong))); + EXPECT_EQ(1u, mesh::validUtf8PrefixLength(surrogate, sizeof(surrogate))); + EXPECT_EQ(1u, mesh::validUtf8PrefixLength(out_of_range, sizeof(out_of_range))); + EXPECT_EQ(1u, mesh::validUtf8PrefixLength(truncated, sizeof(truncated))); +} + +TEST(UTF8Helpers, RejectsUnexpectedContinuationByte) { + const char invalid[] = {'A', static_cast(0x80), 'B', 0}; + + EXPECT_EQ(1u, mesh::validUtf8PrefixLength(invalid, sizeof(invalid))); +} + +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} From 0de2b4fc5f208489a8523882f050ead033ed3d8b Mon Sep 17 00:00:00 2001 From: =UltimateCodeWarrior Date: Mon, 20 Jul 2026 19:24:38 -0700 Subject: [PATCH 38/70] Move Supreme OLED address to variant config Keep SH1106Display generic by relying on DISPLAY_ADDRESS while the T-Beam Supreme variant declares its 0x3D OLED address. --- src/helpers/ui/SH1106Display.cpp | 15 ++++----------- .../lilygo_tbeam_supreme_SX1262/platformio.ini | 1 + 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/helpers/ui/SH1106Display.cpp b/src/helpers/ui/SH1106Display.cpp index 86c6089b..57ccfc2e 100644 --- a/src/helpers/ui/SH1106Display.cpp +++ b/src/helpers/ui/SH1106Display.cpp @@ -11,17 +11,10 @@ bool SH1106Display::i2c_probe(TwoWire &wire, uint8_t addr) bool SH1106Display::begin() { - // Address selection: on some board revisions (notably the LilyGo T-Beam - // Supreme V3) the OLED lives at 0x3D because 0x3C is occupied by a - // magnetometer (QMC6310N). 0x3D is only ever used by the OLED, so prefer it - // when present, otherwise fall back to the standard 0x3C (DISPLAY_ADDRESS). - uint8_t addr = 0; - if (i2c_probe(Wire, 0x3D)) { - addr = 0x3D; - } else if (i2c_probe(Wire, DISPLAY_ADDRESS)) { - addr = DISPLAY_ADDRESS; - } - return addr && display.begin(addr, true); + // Wire must already be initialised by board.begin() before this is called. + // Boards with non-standard SH1106 addresses should define DISPLAY_ADDRESS + // in their variant/platformio configuration. + return i2c_probe(Wire, DISPLAY_ADDRESS) && display.begin(DISPLAY_ADDRESS, true); } void SH1106Display::turnOn() diff --git a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini index 02946156..8bfc4093 100644 --- a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini +++ b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini @@ -18,6 +18,7 @@ build_flags = -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper -D DISPLAY_CLASS=SH1106Display + -D DISPLAY_ADDRESS=0x3D -D LORA_TX_POWER=22 -D P_LORA_TX_LED=6 -D PIN_BOARD_SDA=17 From 4d4d7c37467ce2ad2255f70e53ea40624834393a Mon Sep 17 00:00:00 2001 From: Alexander Hoffer Date: Tue, 21 Jul 2026 07:17:01 +0100 Subject: [PATCH 39/70] refactor: use FreeRTOS BLE receive queue --- src/helpers/BoundedQueue.h | 39 ------------- src/helpers/esp32/SerialBLEInterface.cpp | 16 +---- src/helpers/esp32/SerialBLEInterface.h | 10 +++- .../test_bounded_queue/test_bounded_queue.cpp | 58 ------------------- 4 files changed, 10 insertions(+), 113 deletions(-) delete mode 100644 src/helpers/BoundedQueue.h delete mode 100644 test/test_bounded_queue/test_bounded_queue.cpp diff --git a/src/helpers/BoundedQueue.h b/src/helpers/BoundedQueue.h deleted file mode 100644 index 07ecb4ff..00000000 --- a/src/helpers/BoundedQueue.h +++ /dev/null @@ -1,39 +0,0 @@ -#pragma once - -#include - -namespace mesh { - -template -class BoundedQueue { - static_assert(Capacity > 0, "BoundedQueue capacity must be greater than zero"); - - T _items[Capacity]; - size_t _head = 0; - size_t _size = 0; - -public: - bool push(const T& item) { - if (_size == Capacity) return false; - _items[(_head + _size) % Capacity] = item; - _size++; - return true; - } - - bool pop(T& item) { - if (_size == 0) return false; - item = _items[_head]; - _head = (_head + 1) % Capacity; - _size--; - return true; - } - - void clear() { - _head = 0; - _size = 0; - } - - size_t size() const { return _size; } -}; - -} // namespace mesh diff --git a/src/helpers/esp32/SerialBLEInterface.cpp b/src/helpers/esp32/SerialBLEInterface.cpp index 9c81dc7b..2dd20b2f 100644 --- a/src/helpers/esp32/SerialBLEInterface.cpp +++ b/src/helpers/esp32/SerialBLEInterface.cpp @@ -123,11 +123,7 @@ void SerialBLEInterface::onWrite(BLECharacteristic* pCharacteristic, esp_ble_gat frame.len = len; memcpy(frame.buf, rxValue, len); - portENTER_CRITICAL(&recv_queue_mux); - bool queued = recv_queue.push(frame); - portEXIT_CRITICAL(&recv_queue_mux); - - if (!queued) { + if (xQueueSend(recv_queue, &frame, 0) != pdTRUE) { BLE_DEBUG_PRINTLN("ERROR: onWrite(), recv_queue is full!"); } } @@ -136,9 +132,7 @@ void SerialBLEInterface::onWrite(BLECharacteristic* pCharacteristic, esp_ble_gat // ---------- public methods void SerialBLEInterface::clearBuffers() { - portENTER_CRITICAL(&recv_queue_mux); - recv_queue.clear(); - portEXIT_CRITICAL(&recv_queue_mux); + xQueueReset(recv_queue); send_queue_len = 0; } @@ -216,11 +210,7 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) { } Frame frame = {}; - portENTER_CRITICAL(&recv_queue_mux); - bool received = recv_queue.pop(frame); - portEXIT_CRITICAL(&recv_queue_mux); - - if (received) { + if (xQueueReceive(recv_queue, &frame, 0) == pdTRUE) { memcpy(dest, frame.buf, frame.len); BLE_DEBUG_PRINTLN("readBytes: sz=%d, hdr=%d", (uint32_t) frame.len, (uint32_t) dest[0]); return frame.len; diff --git a/src/helpers/esp32/SerialBLEInterface.h b/src/helpers/esp32/SerialBLEInterface.h index e2d600a0..9fb6adba 100644 --- a/src/helpers/esp32/SerialBLEInterface.h +++ b/src/helpers/esp32/SerialBLEInterface.h @@ -1,12 +1,12 @@ #pragma once #include "../BaseSerialInterface.h" -#include "../BoundedQueue.h" #include #include #include #include #include +#include class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLEServerCallbacks, BLECharacteristicCallbacks { BLEServer *pServer; @@ -26,8 +26,9 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE }; #define FRAME_QUEUE_SIZE 4 - portMUX_TYPE recv_queue_mux = portMUX_INITIALIZER_UNLOCKED; - mesh::BoundedQueue recv_queue; + StaticQueue_t recv_queue_state; + uint8_t recv_queue_storage[FRAME_QUEUE_SIZE * sizeof(Frame)]; + QueueHandle_t recv_queue; int send_queue_len; Frame send_queue[FRAME_QUEUE_SIZE]; @@ -60,6 +61,9 @@ public: _isEnabled = false; _last_write = 0; last_conn_id = 0; + recv_queue = xQueueCreateStatic( + FRAME_QUEUE_SIZE, sizeof(Frame), recv_queue_storage, &recv_queue_state + ); send_queue_len = 0; } diff --git a/test/test_bounded_queue/test_bounded_queue.cpp b/test/test_bounded_queue/test_bounded_queue.cpp deleted file mode 100644 index 3e2e2de7..00000000 --- a/test/test_bounded_queue/test_bounded_queue.cpp +++ /dev/null @@ -1,58 +0,0 @@ -#include - -#include - -TEST(BoundedQueue, PreservesOrderAcrossWraparound) { - mesh::BoundedQueue queue; - int value = 0; - - for (int i = 0; i < 4; i++) EXPECT_TRUE(queue.push(i)); - EXPECT_FALSE(queue.push(4)); - - EXPECT_TRUE(queue.pop(value)); - EXPECT_EQ(0, value); - EXPECT_TRUE(queue.pop(value)); - EXPECT_EQ(1, value); - - EXPECT_TRUE(queue.push(4)); - EXPECT_TRUE(queue.push(5)); - - for (int expected = 2; expected < 6; expected++) { - EXPECT_TRUE(queue.pop(value)); - EXPECT_EQ(expected, value); - } - EXPECT_FALSE(queue.pop(value)); -} - -TEST(BoundedQueue, SurvivesRepeatedFillAndDrainCycles) { - mesh::BoundedQueue queue; - int value = 0; - - for (int cycle = 0; cycle < 1000; cycle++) { - for (int i = 0; i < 4; i++) EXPECT_TRUE(queue.push(cycle * 4 + i)); - for (int i = 0; i < 4; i++) { - EXPECT_TRUE(queue.pop(value)); - EXPECT_EQ(cycle * 4 + i, value); - } - } -} - -TEST(BoundedQueue, ClearDropsPendingItems) { - mesh::BoundedQueue queue; - int value = 0; - - EXPECT_TRUE(queue.push(1)); - EXPECT_TRUE(queue.push(2)); - queue.clear(); - - EXPECT_EQ(0u, queue.size()); - EXPECT_FALSE(queue.pop(value)); - EXPECT_TRUE(queue.push(3)); - EXPECT_TRUE(queue.pop(value)); - EXPECT_EQ(3, value); -} - -int main(int argc, char **argv) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} From 78240e9477feab86b2ea1d24073ca580f9e59415 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 21 Jul 2026 19:22:09 +1000 Subject: [PATCH 40/70] * optimised: value-init not needed --- src/helpers/esp32/SerialBLEInterface.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/esp32/SerialBLEInterface.cpp b/src/helpers/esp32/SerialBLEInterface.cpp index 7084b7d9..6371cf33 100644 --- a/src/helpers/esp32/SerialBLEInterface.cpp +++ b/src/helpers/esp32/SerialBLEInterface.cpp @@ -208,7 +208,7 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) { } } - Frame frame = {}; + Frame frame; if (xQueueReceive(recv_queue, &frame, 0) == pdTRUE) { memcpy(dest, frame.buf, frame.len); BLE_DEBUG_PRINTLN("readBytes: sz=%d, hdr=%d", (uint32_t) frame.len, (uint32_t) dest[0]); From a71cc238546e36c5c31c196eb4ec0cf528746017 Mon Sep 17 00:00:00 2001 From: agessaman Date: Tue, 21 Jul 2026 12:23:20 -0700 Subject: [PATCH 41/70] fix(room_server): support RX boosted gain The room server never supported RX boosted gain, while the repeater does. Three consequences on SX1262/SX1268 boards: - boosted gain was never applied to the radio at boot, so a room server ran in power-saving RX mode while an identical repeater ran boosted - a real receive-sensitivity difference with nothing pointing at it - _prefs.rx_boosted_gain was never initialised (the prefs are memset to 0 before defaults are set), so it defaulted to off - 'set radio.rxgain on' replied "Error: unsupported" but CommonCLI had already written the value to prefs and saved them, so a stale setting survived reboot and was never applied Mirror the repeater's implementation: initialise the pref default under the same USE_SX1262/USE_SX1268 and SX126X_RX_BOOSTED_GAIN guards, apply it during radio init, and override setRxBoostedGain() so the CLI reports success and takes effect. Built Heltec_v3_room_server (SX1262) and LilyGo_T3S3_sx1276_room_server (guards compile out cleanly). --- examples/simple_room_server/MyMesh.cpp | 13 +++++++++++++ examples/simple_room_server/MyMesh.h | 1 + 2 files changed, 14 insertions(+) diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 36978e80..b6485128 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -658,6 +658,14 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.gps_enabled = 0; _prefs.gps_interval = 0; _prefs.advert_loc_policy = ADVERT_LOC_PREFS; + +#if defined(USE_SX1262) || defined(USE_SX1268) +#ifdef SX126X_RX_BOOSTED_GAIN + _prefs.rx_boosted_gain = SX126X_RX_BOOSTED_GAIN; +#else + _prefs.rx_boosted_gain = 1; // enabled by default; +#endif +#endif _prefs.radio_fem_rxgain = 1; next_post_idx = 0; @@ -700,6 +708,7 @@ void MyMesh::begin(FILESYSTEM *fs) { radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); radio_driver.setTxPower(_prefs.tx_power_dbm); + radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain); board.setLoRaFemLnaEnabled(_prefs.radio_fem_rxgain); updateAdvertTimer(); @@ -807,6 +816,10 @@ void MyMesh::setTxPower(int8_t power_dbm) { radio_driver.setTxPower(power_dbm); } +bool MyMesh::setRxBoostedGain(bool enable) { + return radio_driver.setRxBoostedGainMode(enable); +} + void MyMesh::saveIdentity(const mesh::LocalIdentity &new_id) { #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) IdentityStore store(*_fs, ""); diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index 6bab9dc2..caef6920 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -206,6 +206,7 @@ public: void dumpLogFile() override; void setTxPower(int8_t power_dbm) override; + bool setRxBoostedGain(bool enable) override; void formatNeighborsReply(char *reply) override { strcpy(reply, "not supported"); From b8504e55c6fe000bd4f0f412d04823d3b177e0bd Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Wed, 22 Jul 2026 16:03:33 +1000 Subject: [PATCH 42/70] * refactor of Color mapping in UITasks / DisplayDrivers * color displays now with a new light theme --- examples/companion_radio/ui-new/UITask.cpp | 67 ++++++++++++------- examples/companion_radio/ui-orig/UITask.cpp | 27 ++++---- .../ui-tiny/ScrollingStatusBar.h | 2 +- examples/companion_radio/ui-tiny/UITask.cpp | 31 +++++---- examples/simple_repeater/UITask.cpp | 12 ++-- examples/simple_room_server/UITask.cpp | 8 +-- examples/simple_sensor/UITask.cpp | 8 +-- src/helpers/ui/DisplayDriver.h | 14 +++- src/helpers/ui/E213Display.cpp | 30 ++++++--- src/helpers/ui/E213Display.h | 5 +- src/helpers/ui/E290Display.cpp | 30 ++++++--- src/helpers/ui/E290Display.h | 5 +- src/helpers/ui/GxEPDDisplay.cpp | 29 +++++--- src/helpers/ui/GxEPDDisplay.h | 4 +- src/helpers/ui/LGFXDisplay.cpp | 48 +++++-------- src/helpers/ui/LGFXDisplay.h | 4 +- src/helpers/ui/NV3001BDisplay.cpp | 34 +++++----- src/helpers/ui/NV3001BDisplay.h | 4 +- src/helpers/ui/NullDisplayDriver.h | 4 +- src/helpers/ui/SH1106Display.cpp | 19 ++++-- src/helpers/ui/SH1106Display.h | 4 +- src/helpers/ui/SSD1306Display.cpp | 19 ++++-- src/helpers/ui/SSD1306Display.h | 4 +- src/helpers/ui/ST7735Display.cpp | 46 +++++-------- src/helpers/ui/ST7735Display.h | 4 +- src/helpers/ui/ST7789Display.cpp | 51 +++++--------- src/helpers/ui/ST7789Display.h | 4 +- src/helpers/ui/ST7789LCDDisplay.cpp | 47 +++++-------- src/helpers/ui/ST7789LCDDisplay.h | 4 +- src/helpers/ui/U8g2Display.cpp | 12 ++++ src/helpers/ui/U8g2Display.h | 16 ++--- variants/lilygo_techo_card/platformio.ini | 2 +- 32 files changed, 313 insertions(+), 285 deletions(-) create mode 100644 src/helpers/ui/U8g2Display.cpp diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index a26dc19a..09903db0 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -53,23 +53,24 @@ public: int render(DisplayDriver& display) override { // meshcore logo - display.setColor(DisplayDriver::BLUE); + display.setColor(UIColor::corp_blue); int logoWidth = 128; display.drawXbm((display.width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13); // meshcore website const char* website = "https://meshcore.io"; - display.setColor(DisplayDriver::LIGHT); + display.setColor(UIColor::primary_txt); display.setTextSize(1); uint16_t websiteWidth = display.getTextWidth(website); display.setCursor((display.width() - websiteWidth) / 2, 22); display.print(website); // version info - display.setColor(DisplayDriver::LIGHT); + display.setColor(UIColor::primary_txt); display.setTextSize(1); display.drawTextCentered(display.width()/2, 35, _version_info); + display.setColor(UIColor::secondary_txt); display.setTextSize(1); display.drawTextCentered(display.width()/2, 48, FIRMWARE_BUILD_DATE); @@ -128,7 +129,7 @@ class HomeScreen : public UIScreen { int iconHeight = 10; int iconX = display.width() - iconWidth - 5; // Position the icon near the top-right corner int iconY = 0; - display.setColor(DisplayDriver::GREEN); + display.setColor(UIColor::title_txt); // battery outline display.drawRect(iconX, iconY, iconWidth, iconHeight); @@ -143,7 +144,7 @@ class HomeScreen : public UIScreen { // show muted icon if buzzer is muted #ifdef PIN_BUZZER if (_task->isBuzzerQuiet()) { - display.setColor(DisplayDriver::RED); + display.setColor(UIColor::warning_txt); display.drawXbm(iconX - 9, iconY + 1, muted_icon, 8, 8); } #endif @@ -188,34 +189,37 @@ public: } int render(DisplayDriver& display) override { + display.setColor(UIColor::title_bkg); + display.fillRect(0, 0, display.width(), 12); char tmp[80]; // node name display.setTextSize(1); - display.setColor(DisplayDriver::GREEN); + display.setColor(UIColor::title_txt); char filtered_name[sizeof(_node_prefs->node_name)]; display.translateUTF8ToBlocks(filtered_name, _node_prefs->node_name, sizeof(filtered_name)); - display.setCursor(0, 0); + display.setCursor(0, 2); display.print(filtered_name); // battery voltage renderBatteryIndicator(display, _task->getBattMilliVolts()); // curr page indicator + display.setColor(UIColor::title_bkg); int y = 14; int x = display.width() / 2 - 5 * (HomePage::Count-1); for (uint8_t i = 0; i < HomePage::Count; i++, x += 10) { if (i == _page) { - display.fillRect(x-1, y-1, 3, 3); + display.fillRect(x-1, y-1, 4, 4); } else { - display.fillRect(x, y, 1, 1); + display.fillRect(x, y, 2, 2); } } if (_page == HomePage::FIRST) { - display.setColor(DisplayDriver::YELLOW); + display.setColor(UIColor::primary_txt); display.setTextSize(2); sprintf(tmp, "MSG: %d", _task->getMsgCount()); - display.drawTextCentered(display.width() / 2, 20, tmp); + display.drawTextCentered(display.width() / 2, 22, tmp); #ifdef WIFI_SSID IPAddress ip = WiFi.localIP(); @@ -224,19 +228,19 @@ public: display.drawTextCentered(display.width() / 2, 54, tmp); #endif if (_task->hasConnection()) { - display.setColor(DisplayDriver::GREEN); + display.setColor(UIColor::warning_txt); display.setTextSize(1); display.drawTextCentered(display.width() / 2, 43, "< Connected >"); } else if (the_mesh.getBLEPin() != 0) { // BT pin - display.setColor(DisplayDriver::RED); + display.setColor(UIColor::warning_txt); display.setTextSize(2); sprintf(tmp, "Pin:%d", the_mesh.getBLEPin()); display.drawTextCentered(display.width() / 2, 43, tmp); } } else if (_page == HomePage::RECENT) { the_mesh.getRecentlyHeard(recent, UI_RECENT_LIST_SIZE); - display.setColor(DisplayDriver::GREEN); + display.setColor(UIColor::primary_txt); int y = 20; for (int i = 0; i < UI_RECENT_LIST_SIZE; i++, y += 11) { auto a = &recent[i]; @@ -260,7 +264,7 @@ public: display.print(tmp); } } else if (_page == HomePage::RADIO) { - display.setColor(DisplayDriver::YELLOW); + display.setColor(UIColor::primary_txt); display.setTextSize(1); // freq / sf display.setCursor(0, 20); @@ -279,15 +283,17 @@ public: sprintf(tmp, "Noise floor: %d", radio_driver.getNoiseFloor()); display.print(tmp); } else if (_page == HomePage::BLUETOOTH) { - display.setColor(DisplayDriver::GREEN); + display.setColor(UIColor::corp_blue); display.drawXbm((display.width() - 32) / 2, 18, _task->isSerialEnabled() ? bluetooth_on : bluetooth_off, 32, 32); + display.setColor(UIColor::secondary_txt); display.setTextSize(1); display.drawTextCentered(display.width() / 2, 64 - 11, "toggle: " PRESS_LABEL); } else if (_page == HomePage::ADVERT) { - display.setColor(DisplayDriver::GREEN); + display.setColor(UIColor::corp_blue); display.drawXbm((display.width() - 32) / 2, 18, advert_icon, 32, 32); + display.setColor(UIColor::secondary_txt); display.drawTextCentered(display.width() / 2, 64 - 11, "advert: " PRESS_LABEL); #if ENV_INCLUDE_GPS == 1 } else if (_page == HomePage::GPS) { @@ -305,24 +311,33 @@ public: #else strcpy(buf, gps_state ? "gps on" : "gps off"); #endif + display.setColor(UIColor::primary_txt); display.drawTextLeftAlign(0, y, buf); if (nmea == NULL) { y = y + 12; + display.setColor(UIColor::secondary_txt); display.drawTextLeftAlign(0, y, "Can't access GPS"); } else { + display.setColor(UIColor::primary_txt); strcpy(buf, nmea->isValid()?"fix":"no fix"); display.drawTextRightAlign(display.width()-1, y, buf); y = y + 12; + display.setColor(UIColor::secondary_txt); display.drawTextLeftAlign(0, y, "sat"); + display.setColor(UIColor::primary_txt); sprintf(buf, "%d", nmea->satellitesCount()); display.drawTextRightAlign(display.width()-1, y, buf); y = y + 12; + display.setColor(UIColor::secondary_txt); display.drawTextLeftAlign(0, y, "pos"); + display.setColor(UIColor::primary_txt); sprintf(buf, "%.4f %.4f", nmea->getLatitude()/1000000., nmea->getLongitude()/1000000.); display.drawTextRightAlign(display.width()-1, y, buf); y = y + 12; + display.setColor(UIColor::secondary_txt); display.drawTextLeftAlign(0, y, "alt"); + display.setColor(UIColor::primary_txt); sprintf(buf, "%.2f", nmea->getAltitude()/1000.); display.drawTextRightAlign(display.width()-1, y, buf); y = y + 12; @@ -390,7 +405,9 @@ public: strcpy(name, "unk"); sprintf(buf, ""); } display.setCursor(0, y); + display.setColor(UIColor::secondary_txt); display.print(name); + display.setColor(UIColor::primary_txt); display.setCursor( display.width()-display.getTextWidth(buf)-1, y ); @@ -401,11 +418,13 @@ public: else sensors_scroll_offset = 0; #endif } else if (_page == HomePage::SHUTDOWN) { - display.setColor(DisplayDriver::GREEN); + display.setColor(UIColor::corp_blue); display.setTextSize(1); if (_shutdown_init) { + display.setColor(UIColor::warning_txt); display.drawTextCentered(display.width() / 2, 34, "hibernating..."); } else { + display.setColor(UIColor::secondary_txt); display.drawXbm((display.width() - 32) / 2, 18, power_icon, 32, 32); display.drawTextCentered(display.width() / 2, 64 - 11, "hibernate:" PRESS_LABEL); } @@ -498,7 +517,7 @@ public: char tmp[16]; display.setCursor(0, 0); display.setTextSize(1); - display.setColor(DisplayDriver::GREEN); + display.setColor(UIColor::corp_blue); sprintf(tmp, "Unread: %d", num_unread); display.print(tmp); @@ -518,13 +537,13 @@ public: display.drawRect(0, 11, display.width(), 1); // horiz line display.setCursor(0, 14); - display.setColor(DisplayDriver::YELLOW); + display.setColor(UIColor::secondary_txt); char filtered_origin[sizeof(p->origin)]; display.translateUTF8ToBlocks(filtered_origin, p->origin, sizeof(filtered_origin)); display.print(filtered_origin); display.setCursor(0, 25); - display.setColor(DisplayDriver::LIGHT); + display.setColor(UIColor::primary_txt); char filtered_msg[sizeof(p->msg)]; display.translateUTF8ToBlocks(filtered_msg, p->msg, sizeof(filtered_msg)); display.printWordWrap(filtered_msg, display.width()); @@ -806,9 +825,9 @@ void UITask::loop() { _display->setTextSize(1); int y = _display->height() / 3; int p = _display->height() / 32; - _display->setColor(DisplayDriver::DARK); + _display->setColor(UIColor::popup_bkg); _display->fillRect(p, y, _display->width() - p*2, y); - _display->setColor(DisplayDriver::LIGHT); // draw box border + _display->setColor(UIColor::popup_txt); // draw box border _display->drawRect(p, y, _display->width() - p*2, y); _display->drawTextCentered(_display->width() / 2, y + p*3, _alert); _next_refresh = _alert_expiry; // will need refresh when alert is dismissed @@ -845,7 +864,7 @@ void UITask::loop() { if (_display != NULL) { _display->startFrame(); _display->setTextSize(2); - _display->setColor(DisplayDriver::RED); + _display->setColor(UIColor::warning_txt); _display->drawTextCentered(_display->width() / 2, 20, "Low Battery."); _display->drawTextCentered(_display->width() / 2, 40, "Shutting Down!"); _display->endFrame(); diff --git a/examples/companion_radio/ui-orig/UITask.cpp b/examples/companion_radio/ui-orig/UITask.cpp index b48f6412..09fc8e77 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -167,7 +167,7 @@ void UITask::renderBatteryIndicator(uint16_t batteryMilliVolts) { int iconHeight = 12; int iconX = _display->width() - iconWidth - 5; // Position the icon near the top-right corner int iconY = 0; - _display->setColor(DisplayDriver::GREEN); + _display->setColor(UIColor::primary_txt); // battery outline _display->drawRect(iconX, iconY, iconWidth, iconHeight); @@ -188,7 +188,7 @@ void UITask::renderCurrScreen() { _display->setTextSize(1.4); uint16_t textWidth = _display->getTextWidth(_alert); _display->setCursor((_display->width() - textWidth) / 2, 22); - _display->setColor(DisplayDriver::GREEN); + _display->setColor(UIColor::warning_txt); _display->print(_alert); _alert[0] = 0; _need_refresh = true; @@ -197,30 +197,29 @@ void UITask::renderCurrScreen() { // render message preview _display->setCursor(0, 0); _display->setTextSize(1); - _display->setColor(DisplayDriver::GREEN); + _display->setColor(UIColor::primary_txt); _display->print(_node_prefs->node_name); _display->setCursor(0, 12); - _display->setColor(DisplayDriver::YELLOW); + _display->setColor(UIColor::secondary_txt); _display->print(_origin); _display->setCursor(0, 24); - _display->setColor(DisplayDriver::LIGHT); _display->print(_msg); _display->setCursor(_display->width() - 28, 9); _display->setTextSize(2); - _display->setColor(DisplayDriver::ORANGE); + _display->setColor(UIColor::primary_txt); sprintf(tmp, "%d", _msgcount); _display->print(tmp); - _display->setColor(DisplayDriver::YELLOW); // last color will be kept on T114 + _display->setColor(UIColor::secondary_txt); // last color will be kept on T114 } else if ((millis() - ui_started_at) < BOOT_SCREEN_MILLIS) { // boot screen // meshcore logo - _display->setColor(DisplayDriver::BLUE); + _display->setColor(UIColor::corp_blue); int logoWidth = 128; _display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13); // version info - _display->setColor(DisplayDriver::LIGHT); + _display->setColor(UIColor::primary_txt); _display->setTextSize(1); uint16_t textWidth = _display->getTextWidth(_version_info); _display->setCursor((_display->width() - textWidth) / 2, 22); @@ -229,7 +228,7 @@ void UITask::renderCurrScreen() { // node name _display->setCursor(0, 0); _display->setTextSize(1); - _display->setColor(DisplayDriver::GREEN); + _display->setColor(UIColor::primary_txt); _display->print(_node_prefs->node_name); // battery voltage @@ -237,7 +236,7 @@ void UITask::renderCurrScreen() { // freq / sf _display->setCursor(0, 20); - _display->setColor(DisplayDriver::YELLOW); + _display->setColor(UIColor::secondary_txt); sprintf(tmp, "FREQ: %06.3f SF%d", _node_prefs->freq, _node_prefs->sf); _display->print(tmp); @@ -248,14 +247,14 @@ void UITask::renderCurrScreen() { // BT pin if (!_connected && the_mesh.getBLEPin() != 0) { - _display->setColor(DisplayDriver::RED); + _display->setColor(UIColor::warning_txt); _display->setTextSize(2); _display->setCursor(0, 43); sprintf(tmp, "Pin:%d", the_mesh.getBLEPin()); _display->print(tmp); - _display->setColor(DisplayDriver::GREEN); + _display->setColor(UIColor::primary_txt); } else { - _display->setColor(DisplayDriver::LIGHT); + _display->setColor(UIColor::primary_txt); } } _need_refresh = false; diff --git a/examples/companion_radio/ui-tiny/ScrollingStatusBar.h b/examples/companion_radio/ui-tiny/ScrollingStatusBar.h index f5943c9b..9967489f 100644 --- a/examples/companion_radio/ui-tiny/ScrollingStatusBar.h +++ b/examples/companion_radio/ui-tiny/ScrollingStatusBar.h @@ -104,7 +104,7 @@ public: if (_status[0] == 0) return; display.setTextSize(1); - display.setColor(DisplayDriver::GREEN); + display.setColor(UIColor::primary_txt); // if (_needs_redraw) { // _text_width = display.getTextWidth(_status); diff --git a/examples/companion_radio/ui-tiny/UITask.cpp b/examples/companion_radio/ui-tiny/UITask.cpp index 452c02d4..5b9ebfd3 100644 --- a/examples/companion_radio/ui-tiny/UITask.cpp +++ b/examples/companion_radio/ui-tiny/UITask.cpp @@ -56,24 +56,24 @@ public: int render(DisplayDriver& display) override { if (millis() < version_after) { // meshcore logo - display.setColor(DisplayDriver::BLUE); + display.setColor(UIColor::corp_blue); int logoWidth = 72; display.drawXbm(0, 0, meshcore_logo, 72, 36); } else { // meshcore website const char* website = "meshcore.io"; - display.setColor(DisplayDriver::LIGHT); + display.setColor(UIColor::primary_txt); display.setTextSize(1); uint16_t websiteWidth = display.getTextWidth(website); display.setCursor((display.width() - websiteWidth) / 2, 9); display.print(website); // version info - display.setColor(DisplayDriver::LIGHT); display.setTextSize(1); display.drawTextCentered(display.width()/2, 18, _version_info); + display.setColor(UIColor::secondary_txt); display.setTextSize(1); display.drawTextCentered(display.width()/2, 27, FIRMWARE_BUILD_DATE); } @@ -163,7 +163,7 @@ public: // display.print(filtered_name); - display.setColor(DisplayDriver::YELLOW); + display.setColor(UIColor::primary_txt); display.setTextSize(2); sprintf(tmp, "MSG: %d", _task->getMsgCount()); display.setCursor(0, 10); @@ -180,19 +180,19 @@ public: display.drawTextCentered(display.width() / 2, 54, tmp); #endif if (_task->hasConnection()) { - display.setColor(DisplayDriver::GREEN); + display.setColor(UIColor::warning_txt); display.setTextSize(1); display.drawTextCentered(display.width() / 2, display.height()-8, "< Connected >"); } else if (the_mesh.getBLEPin() != 0) { // BT pin - display.setColor(DisplayDriver::RED); + display.setColor(UIColor::warning_txt); display.setTextSize(2); sprintf(tmp, "Pin:%d", the_mesh.getBLEPin()); display.drawTextCentered(display.width() / 2, display.height()-8, tmp); } } else if (_page == HomePage::RECENT) { the_mesh.getRecentlyHeard(recent, UI_RECENT_LIST_SIZE); - display.setColor(DisplayDriver::GREEN); + display.setColor(UIColor::primary_txt); int y = 8; for (int i = 0; i < UI_RECENT_LIST_SIZE; i++, y += 11) { auto a = &recent[i]; @@ -216,7 +216,7 @@ public: display.print(tmp); } } else if (_page == HomePage::RADIO) { - display.setColor(DisplayDriver::YELLOW); + display.setColor(UIColor::primary_txt); display.setTextSize(1); // frequency and spreading factor display.setCursor(0, 8); @@ -238,14 +238,14 @@ public: display.drawTextRightAlign(display.width(), 26, tmp); } else if (_page == HomePage::BLUETOOTH) { - display.setColor(DisplayDriver::GREEN); + display.setColor(UIColor::corp_blue); display.drawXbm((display.width() - 32) / 2, 8, _task->isSerialEnabled() ? bluetooth_on : bluetooth_off, 32, 32); display.setTextSize(1); // display.drawTextCentered(display.width() / 2, 40 - 11, "toggle: " PRESS_LABEL); } else if (_page == HomePage::ADVERT) { - display.setColor(DisplayDriver::GREEN); + display.setColor(UIColor::corp_blue); display.drawXbm((display.width() - 32) / 2, 8, advert_icon, 32, 32); // display.drawTextCentered(display.width() / 2, 40 - 11, "advert: " PRESS_LABEL); #if ENV_INCLUDE_GPS == 1 @@ -264,9 +264,11 @@ public: #else strcpy(buf, gps_state ? "gps on" : "gps off"); #endif + display.setColor(UIColor::primary_txt); display.drawTextLeftAlign(0, y, buf); if (nmea == NULL) { // y = y + 8; + display.setColor(UIColor::warning_txt); display.drawTextLeftAlign(0, y, "Can't access GPS"); } else { if (!gps_state || !nmea->isValid()) { @@ -274,6 +276,7 @@ public: } else { sprintf(buf, "%d sat", nmea->satellitesCount()); } + display.setColor(UIColor::primary_txt); display.drawTextRightAlign(display.width()-1, y, buf); y = y + 8; sprintf(buf, "lat %.4f", @@ -349,8 +352,10 @@ public: r.skipData(type); strcpy(name, "unk"); sprintf(buf, ""); } + display.setColor(UIColor::secondary_txt); display.setCursor(0, y); display.print(name); + display.setColor(UIColor::primary_txt); display.setCursor( display.width()-display.getTextWidth(buf)-1, y ); @@ -361,7 +366,7 @@ public: else sensors_scroll_offset = 0; #endif } else if (_page == HomePage::SHUTDOWN) { - display.setColor(DisplayDriver::GREEN); + display.setColor(UIColor::secondary_txt); display.setTextSize(1); if (_shutdown_init) { display.drawTextCentered(display.width() / 2, 20, "hibernating..."); @@ -692,9 +697,9 @@ void UITask::loop() { _display->setTextSize(1); int y = _display->height() / 3; int p = _display->height() / 32; - _display->setColor(DisplayDriver::DARK); + _display->setColor(UIColor::popup_bkg); _display->fillRect(p, y, _display->width() - p*2, y); - _display->setColor(DisplayDriver::LIGHT); // draw box border + _display->setColor(UIColor::popup_txt); // draw box border _display->drawRect(p, y, _display->width() - p*2, y); _display->drawTextCentered(_display->width() / 2, y + p*3, _alert); _next_refresh = _alert_expiry; // will need refresh when alert is dismissed diff --git a/examples/simple_repeater/UITask.cpp b/examples/simple_repeater/UITask.cpp index 6751aad6..e7225557 100644 --- a/examples/simple_repeater/UITask.cpp +++ b/examples/simple_repeater/UITask.cpp @@ -57,18 +57,17 @@ void UITask::renderCurrScreen() { char tmp[80]; if (millis() < _started_at + BOOT_SCREEN_MILLIS) { // boot screen // meshcore logo - _display->setColor(DisplayDriver::BLUE); + _display->setColor(UIColor::corp_blue); int logoWidth = 128; _display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13); // meshcore website const char* website = "https://meshcore.io"; - _display->setColor(DisplayDriver::LIGHT); + _display->setColor(UIColor::primary_txt); _display->setTextSize(1); _display->drawTextCentered(_display->width() / 2, 22, website); // version info - _display->setColor(DisplayDriver::LIGHT); _display->setTextSize(1); _display->drawTextCentered(_display->width() / 2, 35, _version_info); @@ -77,13 +76,13 @@ void UITask::renderCurrScreen() { _display->drawTextCentered(_display->width() / 2, 48, node_type); } else if (_powering_off_at > 0) { // meshcore logo - _display->setColor(DisplayDriver::BLUE); + _display->setColor(UIColor::corp_blue); int logoWidth = 128; _display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13); // meshcore website const char* website = "https://meshcore.io"; - _display->setColor(DisplayDriver::LIGHT); + _display->setColor(UIColor::primary_txt); _display->setTextSize(1); _display->drawTextCentered(_display->width()/ 2, 22, website); @@ -95,12 +94,11 @@ void UITask::renderCurrScreen() { } else { _display->setCursor(0, 0); _display->setTextSize(1); - _display->setColor(DisplayDriver::GREEN); + _display->setColor(UIColor::primary_txt); _display->print(_node_prefs->node_name); // freq / sf _display->setCursor(0, 20); - _display->setColor(DisplayDriver::YELLOW); sprintf(tmp, "FREQ: %06.3f SF%d", _node_prefs->freq, _node_prefs->sf); _display->print(tmp); diff --git a/examples/simple_room_server/UITask.cpp b/examples/simple_room_server/UITask.cpp index fb66b819..8e4c2fcf 100644 --- a/examples/simple_room_server/UITask.cpp +++ b/examples/simple_room_server/UITask.cpp @@ -49,20 +49,19 @@ void UITask::renderCurrScreen() { char tmp[80]; if (millis() < BOOT_SCREEN_MILLIS) { // boot screen // meshcore logo - _display->setColor(DisplayDriver::BLUE); + _display->setColor(UIColor::corp_blue); int logoWidth = 128; _display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13); // meshcore website const char* website = "https://meshcore.io"; - _display->setColor(DisplayDriver::LIGHT); + _display->setColor(UIColor::primary_txt); _display->setTextSize(1); uint16_t websiteWidth = _display->getTextWidth(website); _display->setCursor((_display->width() - websiteWidth) / 2, 22); _display->print(website); // version info - _display->setColor(DisplayDriver::LIGHT); _display->setTextSize(1); uint16_t versionWidth = _display->getTextWidth(_version_info); _display->setCursor((_display->width() - versionWidth) / 2, 35); @@ -77,12 +76,11 @@ void UITask::renderCurrScreen() { // node name _display->setCursor(0, 0); _display->setTextSize(1); - _display->setColor(DisplayDriver::GREEN); + _display->setColor(UIColor::primary_txt); _display->print(_node_prefs->node_name); // freq / sf _display->setCursor(0, 20); - _display->setColor(DisplayDriver::YELLOW); sprintf(tmp, "FREQ: %06.3f SF%d", _node_prefs->freq, _node_prefs->sf); _display->print(tmp); diff --git a/examples/simple_sensor/UITask.cpp b/examples/simple_sensor/UITask.cpp index 68a80607..75a0b465 100644 --- a/examples/simple_sensor/UITask.cpp +++ b/examples/simple_sensor/UITask.cpp @@ -49,20 +49,19 @@ void UITask::renderCurrScreen() { char tmp[80]; if (millis() < BOOT_SCREEN_MILLIS) { // boot screen // meshcore logo - _display->setColor(DisplayDriver::BLUE); + _display->setColor(UIColor::corp_blue); int logoWidth = 128; _display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13); // meshcore website const char* website = "https://meshcore.io"; - _display->setColor(DisplayDriver::LIGHT); + _display->setColor(UIColor::primary_txt); _display->setTextSize(1); uint16_t websiteWidth = _display->getTextWidth(website); _display->setCursor((_display->width() - websiteWidth) / 2, 22); _display->print(website); // version info - _display->setColor(DisplayDriver::LIGHT); _display->setTextSize(1); uint16_t versionWidth = _display->getTextWidth(_version_info); _display->setCursor((_display->width() - versionWidth) / 2, 35); @@ -77,12 +76,11 @@ void UITask::renderCurrScreen() { // node name _display->setCursor(0, 0); _display->setTextSize(1); - _display->setColor(DisplayDriver::GREEN); + _display->setColor(UIColor::primary_txt); _display->print(_node_prefs->node_name); // freq / sf _display->setCursor(0, 20); - _display->setColor(DisplayDriver::YELLOW); sprintf(tmp, "FREQ: %06.3f SF%d", _node_prefs->freq, _node_prefs->sf); _display->print(tmp); diff --git a/src/helpers/ui/DisplayDriver.h b/src/helpers/ui/DisplayDriver.h index dcc5fe03..b76a1b6c 100644 --- a/src/helpers/ui/DisplayDriver.h +++ b/src/helpers/ui/DisplayDriver.h @@ -3,12 +3,20 @@ #include #include +using ColorVal = uint16_t; + +class UIColor { +public: + // color definitions (by element _type_) + static ColorVal window_bkg, title_bkg, title_txt, primary_txt, secondary_txt, warning_txt, popup_bkg, popup_txt, corp_blue; +}; + class DisplayDriver { int _w, _h; protected: DisplayDriver(int w, int h) { _w = w; _h = h; } public: - enum Color { DARK=0, LIGHT, RED, GREEN, BLUE, YELLOW, ORANGE }; // on b/w screen, colors will be !=0 synonym of light + //enum Color { DARK=0, LIGHT, RED, GREEN, BLUE, YELLOW, ORANGE }; // on b/w screen, colors will be !=0 synonym of light int width() const { return _w; } int height() const { return _h; } @@ -18,9 +26,9 @@ public: virtual void turnOn() = 0; virtual void turnOff() = 0; virtual void clear() = 0; - virtual void startFrame(Color bkg = DARK) = 0; + virtual void startFrame(ColorVal bkg = UIColor::window_bkg) = 0; virtual void setTextSize(int sz) = 0; - virtual void setColor(Color c) = 0; + virtual void setColor(ColorVal c) = 0; virtual void setCursor(int x, int y) = 0; virtual void print(const char* str) = 0; virtual void printWordWrap(const char* str, int max_width) { print(str); } // fallback to basic print() if no override diff --git a/src/helpers/ui/E213Display.cpp b/src/helpers/ui/E213Display.cpp index 814693a0..ab742976 100644 --- a/src/helpers/ui/E213Display.cpp +++ b/src/helpers/ui/E213Display.cpp @@ -2,6 +2,17 @@ #include "../../MeshCore.h" +// Color scheme +ColorVal UIColor::window_bkg = WHITE; +ColorVal UIColor::title_bkg = BLACK; +ColorVal UIColor::title_txt = WHITE; +ColorVal UIColor::primary_txt = BLACK; +ColorVal UIColor::secondary_txt = BLACK; +ColorVal UIColor::warning_txt = BLACK; +ColorVal UIColor::popup_bkg = BLACK; +ColorVal UIColor::popup_txt = WHITE; +ColorVal UIColor::corp_blue = BLACK; + BaseDisplay* E213Display::detectEInk() { // Test 1: Logic of BUSY pin @@ -108,16 +119,18 @@ void E213Display::clear() { display->clear(); } -void E213Display::startFrame(Color bkg) { +void E213Display::startFrame(ColorVal bkg) { display_crc.reset(); // Fill screen with white first to ensure clean background display->fillRect(0, 0, width(), height(), WHITE); - if (bkg == LIGHT) { + if (bkg == 0) { // Fill with black if light background requested (inverted for e-ink) display->fillRect(0, 0, width(), height(), BLACK); } + _color = UIColor::primary_txt; + display->setTextColor(_color); } void E213Display::setTextSize(int sz) { @@ -126,9 +139,10 @@ void E213Display::setTextSize(int sz) { display->setTextSize(sz); } -void E213Display::setColor(Color c) { - display_crc.update(c); - // implemented in individual display methods +void E213Display::setColor(ColorVal c) { + _color = c; + display_crc.update(c); + display->setTextColor(_color); } void E213Display::setCursor(int x, int y) { @@ -147,7 +161,7 @@ void E213Display::fillRect(int x, int y, int w, int h) { display_crc.update(y); display_crc.update(w); display_crc.update(h); - display->fillRect(x, y, w, h, BLACK); + display->fillRect(x, y, w, h, _color); } void E213Display::drawRect(int x, int y, int w, int h) { @@ -155,7 +169,7 @@ void E213Display::drawRect(int x, int y, int w, int h) { display_crc.update(y); display_crc.update(w); display_crc.update(h); - display->drawRect(x, y, w, h, BLACK); + display->drawRect(x, y, w, h, _color); } void E213Display::drawXbm(int x, int y, const uint8_t *bits, int w, int h) { @@ -179,7 +193,7 @@ void E213Display::drawXbm(int x, int y, const uint8_t *bits, int w, int h) { // If the bit is set, draw the pixel if (bitSet) { - display->drawPixel(x + bx, y + by, BLACK); + display->drawPixel(x + bx, y + by, _color); } } } diff --git a/src/helpers/ui/E213Display.h b/src/helpers/ui/E213Display.h index add8f11b..32567a79 100644 --- a/src/helpers/ui/E213Display.h +++ b/src/helpers/ui/E213Display.h @@ -16,6 +16,7 @@ class E213Display : public DisplayDriver { RefCountedDigitalPin* _periph_power; CRC32 display_crc; uint32_t last_display_crc_value = 0; + uint16_t _color; public: E213Display(RefCountedDigitalPin* periph_power = NULL) : DisplayDriver(250, 122), _periph_power(periph_power) {} @@ -30,9 +31,9 @@ public: void turnOn() override; void turnOff() override; void clear() override; - void startFrame(Color bkg = DARK) override; + void startFrame(ColorVal bkg = UIColor::window_bkg) override; void setTextSize(int sz) override; - void setColor(Color c) override; + void setColor(ColorVal c) override; void setCursor(int x, int y) override; void print(const char *str) override; void fillRect(int x, int y, int w, int h) override; diff --git a/src/helpers/ui/E290Display.cpp b/src/helpers/ui/E290Display.cpp index ef4df05e..ddb449f4 100644 --- a/src/helpers/ui/E290Display.cpp +++ b/src/helpers/ui/E290Display.cpp @@ -2,6 +2,17 @@ #include "../../MeshCore.h" +// Color scheme +ColorVal UIColor::window_bkg = WHITE; +ColorVal UIColor::title_bkg = BLACK; +ColorVal UIColor::title_txt = WHITE; +ColorVal UIColor::primary_txt = BLACK; +ColorVal UIColor::secondary_txt = BLACK; +ColorVal UIColor::warning_txt = BLACK; +ColorVal UIColor::popup_bkg = BLACK; +ColorVal UIColor::popup_txt = WHITE; +ColorVal UIColor::corp_blue = BLACK; + bool E290Display::begin() { if (_init) return true; @@ -62,15 +73,17 @@ void E290Display::clear() { display.clear(); } -void E290Display::startFrame(Color bkg) { +void E290Display::startFrame(ColorVal bkg) { display_crc.reset(); // Fill screen with white first to ensure clean background display.fillRect(0, 0, width(), height(), WHITE); - if (bkg == LIGHT) { + if (bkg == 0) { // Fill with black if light background requested (inverted for e-ink) display.fillRect(0, 0, width(), height(), BLACK); } + _color = UIColor::primary_txt; + display.setTextColor(_color); } void E290Display::setTextSize(int sz) { @@ -79,9 +92,10 @@ void E290Display::setTextSize(int sz) { display.setTextSize(sz); } -void E290Display::setColor(Color c) { - display_crc.update(c); - // implemented in individual display methods +void E290Display::setColor(ColorVal c) { + _color = c; + display_crc.update(c); + display.setTextColor(_color); } void E290Display::setCursor(int x, int y) { @@ -100,7 +114,7 @@ void E290Display::fillRect(int x, int y, int w, int h) { display_crc.update(y); display_crc.update(w); display_crc.update(h); - display.fillRect(x, y, w, h, BLACK); + display.fillRect(x, y, w, h, _color); } void E290Display::drawRect(int x, int y, int w, int h) { @@ -108,7 +122,7 @@ void E290Display::drawRect(int x, int y, int w, int h) { display_crc.update(y); display_crc.update(w); display_crc.update(h); - display.drawRect(x, y, w, h, BLACK); + display.drawRect(x, y, w, h, _color); } void E290Display::drawXbm(int x, int y, const uint8_t *bits, int w, int h) { @@ -132,7 +146,7 @@ void E290Display::drawXbm(int x, int y, const uint8_t *bits, int w, int h) { // If the bit is set, draw the pixel if (bitSet) { - display.drawPixel(x + bx, y + by, BLACK); + display.drawPixel(x + bx, y + by, _color); } } } diff --git a/src/helpers/ui/E290Display.h b/src/helpers/ui/E290Display.h index 88bf34ff..bf6296bb 100644 --- a/src/helpers/ui/E290Display.h +++ b/src/helpers/ui/E290Display.h @@ -16,6 +16,7 @@ class E290Display : public DisplayDriver { RefCountedDigitalPin* _periph_power; CRC32 display_crc; uint32_t last_display_crc_value = 0; + uint16_t _color; public: E290Display(RefCountedDigitalPin* periph_power = NULL) : DisplayDriver(296, 128), _periph_power(periph_power) {} @@ -26,9 +27,9 @@ public: void turnOn() override; void turnOff() override; void clear() override; - void startFrame(Color bkg = DARK) override; + void startFrame(ColorVal bkg = UIColor::window_bkg) override; void setTextSize(int sz) override; - void setColor(Color c) override; + void setColor(ColorVal c) override; void setCursor(int x, int y) override; void print(const char *str) override; void fillRect(int x, int y, int w, int h) override; diff --git a/src/helpers/ui/GxEPDDisplay.cpp b/src/helpers/ui/GxEPDDisplay.cpp index ad47754b..0a5f6a55 100644 --- a/src/helpers/ui/GxEPDDisplay.cpp +++ b/src/helpers/ui/GxEPDDisplay.cpp @@ -14,6 +14,18 @@ SPIClass SPI1 = SPIClass(FSPI); #endif +// Color scheme +ColorVal UIColor::window_bkg = GxEPD_WHITE; +ColorVal UIColor::title_bkg = GxEPD_BLACK; +ColorVal UIColor::title_txt = GxEPD_WHITE; +ColorVal UIColor::primary_txt = GxEPD_BLACK; +ColorVal UIColor::secondary_txt = GxEPD_BLACK; +ColorVal UIColor::warning_txt = GxEPD_BLACK; +ColorVal UIColor::popup_bkg = GxEPD_BLACK; +ColorVal UIColor::popup_txt = GxEPD_WHITE; +ColorVal UIColor::corp_blue = GxEPD_BLACK; + + bool GxEPDDisplay::begin() { display.epd2.selectSPI(SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0)); #ifdef ESP32 @@ -61,9 +73,9 @@ void GxEPDDisplay::clear() { display_crc.reset(); } -void GxEPDDisplay::startFrame(Color bkg) { - display.fillScreen(GxEPD_WHITE); - display.setTextColor(_curr_color = GxEPD_BLACK); +void GxEPDDisplay::startFrame(ColorVal bkg) { + display.fillScreen(bkg); + display.setTextColor(_curr_color = UIColor::primary_txt); display_crc.reset(); } @@ -85,14 +97,9 @@ void GxEPDDisplay::setTextSize(int sz) { } } -void GxEPDDisplay::setColor(Color c) { - display_crc.update (c); - // colours need to be inverted for epaper displays - if (c == DARK) { - display.setTextColor(_curr_color = GxEPD_WHITE); - } else { - display.setTextColor(_curr_color = GxEPD_BLACK); - } +void GxEPDDisplay::setColor(ColorVal c) { + display_crc.update (c); + display.setTextColor(_curr_color = c); } void GxEPDDisplay::setCursor(int x, int y) { diff --git a/src/helpers/ui/GxEPDDisplay.h b/src/helpers/ui/GxEPDDisplay.h index 219b6076..c653eac4 100644 --- a/src/helpers/ui/GxEPDDisplay.h +++ b/src/helpers/ui/GxEPDDisplay.h @@ -51,9 +51,9 @@ public: void turnOn() override; void turnOff() override; void clear() override; - void startFrame(Color bkg = DARK) override; + void startFrame(ColorVal bkg = UIColor::window_bkg) override; void setTextSize(int sz) override; - void setColor(Color c) override; + void setColor(ColorVal c) override; void setCursor(int x, int y) override; void print(const char* str) override; void fillRect(int x, int y, int w, int h) override; diff --git a/src/helpers/ui/LGFXDisplay.cpp b/src/helpers/ui/LGFXDisplay.cpp index a53cbc62..7bdd52a2 100644 --- a/src/helpers/ui/LGFXDisplay.cpp +++ b/src/helpers/ui/LGFXDisplay.cpp @@ -1,5 +1,16 @@ #include "LGFXDisplay.h" +// Color scheme +ColorVal UIColor::window_bkg = 0xFFFF; +ColorVal UIColor::title_bkg = 0x001F; +ColorVal UIColor::title_txt = 0xFFFF; +ColorVal UIColor::primary_txt = 0x0000; +ColorVal UIColor::secondary_txt = (18 << 11) | (36 << 5) | 18; // mid-gray +ColorVal UIColor::warning_txt = 0xFD20; +ColorVal UIColor::popup_bkg = 0x07FF; // CYAN +ColorVal UIColor::popup_txt = 0x0000; +ColorVal UIColor::corp_blue = 0x001A; + bool LGFXDisplay::begin() { turnOn(); display->init(); @@ -35,45 +46,20 @@ void LGFXDisplay::clear() { buffer.clearDisplay(); } -void LGFXDisplay::startFrame(Color bkg) { +void LGFXDisplay::startFrame(ColorVal bkg) { // display->startWrite(); // display->getScanLine(); - buffer.clearDisplay(); - buffer.setTextColor(TFT_WHITE); + _color = bkg; + buffer.fillScreen(_color); + buffer.setTextColor(_color = UIColor::primary_txt); } void LGFXDisplay::setTextSize(int sz) { buffer.setTextSize(sz); } -void LGFXDisplay::setColor(Color c) { - // _color = (c != 0) ? ILI9342_WHITE : ILI9342_BLACK; - switch (c) { - case DARK: - _color = TFT_BLACK; - break; - case LIGHT: - _color = TFT_WHITE; - break; - case RED: - _color = TFT_RED; - break; - case GREEN: - _color = TFT_GREEN; - break; - case BLUE: - _color = TFT_BLUE; - break; - case YELLOW: - _color = TFT_YELLOW; - break; - case ORANGE: - _color = TFT_ORANGE; - break; - default: - _color = TFT_WHITE; - } - buffer.setTextColor(_color); +void LGFXDisplay::setColor(ColorVal c) { + buffer.setTextColor(_color = c); } void LGFXDisplay::setCursor(int x, int y) { diff --git a/src/helpers/ui/LGFXDisplay.h b/src/helpers/ui/LGFXDisplay.h index ad7212ec..a2d660b2 100644 --- a/src/helpers/ui/LGFXDisplay.h +++ b/src/helpers/ui/LGFXDisplay.h @@ -25,9 +25,9 @@ public: void turnOn() override; void turnOff() override; void clear() override; - void startFrame(Color bkg = DARK) override; + void startFrame(ColorVal bkg = UIColor::window_bkg) override; void setTextSize(int sz) override; - void setColor(Color c) override; + void setColor(ColorVal c) override; void setCursor(int x, int y) override; void print(const char* str) override; void fillRect(int x, int y, int w, int h) override; diff --git a/src/helpers/ui/NV3001BDisplay.cpp b/src/helpers/ui/NV3001BDisplay.cpp index 03825cc0..1ff7765c 100644 --- a/src/helpers/ui/NV3001BDisplay.cpp +++ b/src/helpers/ui/NV3001BDisplay.cpp @@ -96,18 +96,16 @@ #define NV3001B_TEXT_SIZE2_SCALE_Y 3 #endif -static uint16_t mapColor(DisplayDriver::Color c) { - switch (c) { - case DisplayDriver::DARK: return 0x0000; - case DisplayDriver::LIGHT: return 0xffff; - case DisplayDriver::RED: return 0xf800; - case DisplayDriver::GREEN: return 0x07e0; - case DisplayDriver::BLUE: return 0x001f; - case DisplayDriver::YELLOW: return 0xffe0; - case DisplayDriver::ORANGE: return 0xfd20; - default: return 0xffff; - } -} +// Color scheme +ColorVal UIColor::window_bkg = 0xFFFF; +ColorVal UIColor::title_bkg = 0x001F; +ColorVal UIColor::title_txt = 0xFFFF; +ColorVal UIColor::primary_txt = 0x0000; +ColorVal UIColor::secondary_txt = (18 << 11) | (36 << 5) | 18; // mid-gray +ColorVal UIColor::warning_txt = 0xFD20; +ColorVal UIColor::popup_bkg = 0x07FF; // CYAN +ColorVal UIColor::popup_txt = 0x0000; +ColorVal UIColor::corp_blue = 0x001A; static int scaleX(int x) { return (int)(x * DISPLAY_SCALE_X); @@ -465,15 +463,15 @@ void NV3001BDisplay::turnOff() { void NV3001BDisplay::clear() { uint16_t saved = color; - color = 0x0000; + color = UIColor::window_bkg; fillPhysicalRect(0, 0, NV3001B_SCREEN_WIDTH, NV3001B_SCREEN_HEIGHT); color = saved; } -void NV3001BDisplay::startFrame(Color bkg) { - color = mapColor(bkg); +void NV3001BDisplay::startFrame(ColorVal bkg) { + color = bkg; fillPhysicalRect(0, 0, NV3001B_SCREEN_WIDTH, NV3001B_SCREEN_HEIGHT); - color = 0xffff; + color = UIColor::primary_txt; text_size = 1; cursor_x = 0; cursor_y = 0; @@ -483,8 +481,8 @@ void NV3001BDisplay::setTextSize(int sz) { text_size = sz < 1 ? 1 : sz; } -void NV3001BDisplay::setColor(Color c) { - color = mapColor(c); +void NV3001BDisplay::setColor(ColorVal c) { + color = c; } void NV3001BDisplay::setCursor(int x, int y) { diff --git a/src/helpers/ui/NV3001BDisplay.h b/src/helpers/ui/NV3001BDisplay.h index 98cdaae8..76505b5d 100644 --- a/src/helpers/ui/NV3001BDisplay.h +++ b/src/helpers/ui/NV3001BDisplay.h @@ -55,9 +55,9 @@ public: void turnOn() override; void turnOff() override; void clear() override; - void startFrame(Color bkg = DARK) override; + void startFrame(ColorVal bkg = UIColor::window_bkg) override; void setTextSize(int sz) override; - void setColor(Color c) override; + void setColor(ColorVal c) override; void setCursor(int x, int y) override; void print(const char* str) override; void fillRect(int x, int y, int w, int h) override; diff --git a/src/helpers/ui/NullDisplayDriver.h b/src/helpers/ui/NullDisplayDriver.h index 2a9670bd..ff214c88 100644 --- a/src/helpers/ui/NullDisplayDriver.h +++ b/src/helpers/ui/NullDisplayDriver.h @@ -11,9 +11,9 @@ public: void turnOn() override { } void turnOff() override { } void clear() override { } - void startFrame(Color bkg = DARK) override { } + void startFrame(ColorVal bkg = UIColor::window_bkg) override { } void setTextSize(int sz) override { } - void setColor(Color c) override { } + void setColor(ColorVal c) override { } void setCursor(int x, int y) override { } void print(const char* str) override { } void fillRect(int x, int y, int w, int h) override { } diff --git a/src/helpers/ui/SH1106Display.cpp b/src/helpers/ui/SH1106Display.cpp index 57ccfc2e..aeb4f300 100644 --- a/src/helpers/ui/SH1106Display.cpp +++ b/src/helpers/ui/SH1106Display.cpp @@ -9,6 +9,17 @@ bool SH1106Display::i2c_probe(TwoWire &wire, uint8_t addr) return (error == 0); } +// Color scheme +ColorVal UIColor::window_bkg = SH110X_BLACK; +ColorVal UIColor::title_bkg = SH110X_WHITE; +ColorVal UIColor::title_txt = SH110X_BLACK; +ColorVal UIColor::primary_txt = SH110X_WHITE; +ColorVal UIColor::secondary_txt = SH110X_WHITE; +ColorVal UIColor::warning_txt = SH110X_WHITE; +ColorVal UIColor::popup_bkg = SH110X_WHITE; +ColorVal UIColor::popup_txt = SH110X_BLACK; +ColorVal UIColor::corp_blue = SH110X_WHITE; + bool SH1106Display::begin() { // Wire must already be initialised by board.begin() before this is called. @@ -35,7 +46,7 @@ void SH1106Display::clear() display.display(); } -void SH1106Display::startFrame(Color bkg) +void SH1106Display::startFrame(ColorVal bkg) { display.clearDisplay(); // TODO: apply 'bkg' _color = SH110X_WHITE; @@ -49,9 +60,9 @@ void SH1106Display::setTextSize(int sz) display.setTextSize(sz); } -void SH1106Display::setColor(Color c) +void SH1106Display::setColor(ColorVal c) { - _color = (c != 0) ? SH110X_WHITE : SH110X_BLACK; + _color = c; display.setTextColor(_color); } @@ -77,7 +88,7 @@ void SH1106Display::drawRect(int x, int y, int w, int h) void SH1106Display::drawXbm(int x, int y, const uint8_t *bits, int w, int h) { - display.drawBitmap(x, y, bits, w, h, SH110X_WHITE); + display.drawBitmap(x, y, bits, w, h, _color); } uint16_t SH1106Display::getTextWidth(const char *str) diff --git a/src/helpers/ui/SH1106Display.h b/src/helpers/ui/SH1106Display.h index b52a6adf..4e269d5e 100644 --- a/src/helpers/ui/SH1106Display.h +++ b/src/helpers/ui/SH1106Display.h @@ -30,9 +30,9 @@ public: void turnOn() override; void turnOff() override; void clear() override; - void startFrame(Color bkg = DARK) override; + void startFrame(ColorVal bkg = UIColor::window_bkg) override; void setTextSize(int sz) override; - void setColor(Color c) override; + void setColor(ColorVal c) override; void setCursor(int x, int y) override; void print(const char *str) override; void fillRect(int x, int y, int w, int h) override; diff --git a/src/helpers/ui/SSD1306Display.cpp b/src/helpers/ui/SSD1306Display.cpp index 464b2642..9b858071 100644 --- a/src/helpers/ui/SSD1306Display.cpp +++ b/src/helpers/ui/SSD1306Display.cpp @@ -6,6 +6,17 @@ bool SSD1306Display::i2c_probe(TwoWire& wire, uint8_t addr) { return (error == 0); } +// Color scheme +ColorVal UIColor::window_bkg = SSD1306_BLACK; +ColorVal UIColor::title_bkg = SSD1306_WHITE; +ColorVal UIColor::title_txt = SSD1306_BLACK; +ColorVal UIColor::primary_txt = SSD1306_WHITE; +ColorVal UIColor::secondary_txt = SSD1306_WHITE; +ColorVal UIColor::warning_txt = SSD1306_WHITE; +ColorVal UIColor::popup_bkg = SSD1306_WHITE; +ColorVal UIColor::popup_txt = SSD1306_BLACK; +ColorVal UIColor::corp_blue = SSD1306_WHITE; + bool SSD1306Display::begin() { if (!_isOn) { if (_peripher_power) _peripher_power->claim(); @@ -44,7 +55,7 @@ void SSD1306Display::clear() { display.display(); } -void SSD1306Display::startFrame(Color bkg) { +void SSD1306Display::startFrame(ColorVal bkg) { display.clearDisplay(); // TODO: apply 'bkg' _color = SSD1306_WHITE; display.setTextColor(_color); @@ -56,8 +67,8 @@ void SSD1306Display::setTextSize(int sz) { display.setTextSize(sz); } -void SSD1306Display::setColor(Color c) { - _color = (c != 0) ? SSD1306_WHITE : SSD1306_BLACK; +void SSD1306Display::setColor(ColorVal c) { + _color = c; display.setTextColor(_color); } @@ -78,7 +89,7 @@ void SSD1306Display::drawRect(int x, int y, int w, int h) { } void SSD1306Display::drawXbm(int x, int y, const uint8_t* bits, int w, int h) { - display.drawBitmap(x, y, bits, w, h, SSD1306_WHITE); + display.drawBitmap(x, y, bits, w, h, _color); } uint16_t SSD1306Display::getTextWidth(const char* str) { diff --git a/src/helpers/ui/SSD1306Display.h b/src/helpers/ui/SSD1306Display.h index d843da85..5dabbc58 100644 --- a/src/helpers/ui/SSD1306Display.h +++ b/src/helpers/ui/SSD1306Display.h @@ -35,9 +35,9 @@ public: void turnOn() override; void turnOff() override; void clear() override; - void startFrame(Color bkg = DARK) override; + void startFrame(ColorVal bkg = UIColor::window_bkg) override; void setTextSize(int sz) override; - void setColor(Color c) override; + void setColor(ColorVal c) override; void setCursor(int x, int y) override; void print(const char* str) override; void fillRect(int x, int y, int w, int h) override; diff --git a/src/helpers/ui/ST7735Display.cpp b/src/helpers/ui/ST7735Display.cpp index 905ff503..344f5b1d 100644 --- a/src/helpers/ui/ST7735Display.cpp +++ b/src/helpers/ui/ST7735Display.cpp @@ -417,6 +417,17 @@ bool ST7735Display::i2c_probe(TwoWire& wire, uint8_t addr) { #define PIN_TFT_LEDA_CTL_ACTIVE HIGH #endif +// Color scheme +ColorVal UIColor::window_bkg = ST77XX_WHITE; +ColorVal UIColor::title_bkg = ST77XX_BLUE; +ColorVal UIColor::title_txt = ST77XX_WHITE; +ColorVal UIColor::primary_txt = ST77XX_BLACK; +ColorVal UIColor::secondary_txt = (18 << 11) | (36 << 5) | 18; // mid-gray +ColorVal UIColor::warning_txt = ST77XX_ORANGE; +ColorVal UIColor::popup_bkg = ST77XX_CYAN; +ColorVal UIColor::popup_txt = ST77XX_BLACK; +ColorVal UIColor::corp_blue = 0x001A; + bool ST7735Display::begin() { if (!sprite) { // alloc offscreen canvas @@ -527,9 +538,9 @@ void ST7735Display::clear() { sprite->fillScreen(ST77XX_BLACK); } -void ST7735Display::startFrame(Color bkg) { - sprite->fillScreen(ST77XX_BLACK); - sprite->setTextColor(curr_color = ST77XX_WHITE); +void ST7735Display::startFrame(ColorVal bkg) { + sprite->fillScreen(bkg); + sprite->setTextColor(curr_color = UIColor::primary_txt); sprite->setFreeFont(); sprite->setTextSize(1); // This one affects size of Please wait... message //sprite->cp437(true); // Use full 256 char 'Code Page 437' font @@ -539,33 +550,8 @@ void ST7735Display::setTextSize(int sz) { sprite->setTextSize(sz); } -void ST7735Display::setColor(Color c) { - switch (c) { - case DisplayDriver::DARK : - curr_color = ST77XX_BLACK; - break; - case DisplayDriver::LIGHT : - curr_color = ST77XX_WHITE; - break; - case DisplayDriver::RED : - curr_color = ST77XX_RED; - break; - case DisplayDriver::GREEN : - curr_color = ST77XX_GREEN; - break; - case DisplayDriver::BLUE : - curr_color = ST77XX_BLUE; - break; - case DisplayDriver::YELLOW : - curr_color = ST77XX_YELLOW; - break; - case DisplayDriver::ORANGE : - curr_color = ST77XX_ORANGE; - break; - default: - curr_color = ST77XX_WHITE; - break; - } +void ST7735Display::setColor(ColorVal c) { + curr_color = c; sprite->setTextColor(curr_color); } diff --git a/src/helpers/ui/ST7735Display.h b/src/helpers/ui/ST7735Display.h index 68c83db2..6e289b21 100644 --- a/src/helpers/ui/ST7735Display.h +++ b/src/helpers/ui/ST7735Display.h @@ -33,9 +33,9 @@ public: void turnOn() override; void turnOff() override; void clear() override; - void startFrame(Color bkg = DARK) override; + void startFrame(ColorVal bkg = UIColor::window_bkg) override; void setTextSize(int sz) override; - void setColor(Color c) override; + void setColor(ColorVal c) override; void setCursor(int x, int y) override; void print(const char* str) override; void fillRect(int x, int y, int w, int h) override; diff --git a/src/helpers/ui/ST7789Display.cpp b/src/helpers/ui/ST7789Display.cpp index 98d69395..fad75194 100644 --- a/src/helpers/ui/ST7789Display.cpp +++ b/src/helpers/ui/ST7789Display.cpp @@ -26,6 +26,17 @@ #define SCALE_Y DISPLAY_SCALE_Y #endif +// Color scheme +ColorVal UIColor::window_bkg = ST77XX_WHITE; +ColorVal UIColor::title_bkg = ST77XX_BLUE; +ColorVal UIColor::title_txt = ST77XX_WHITE; +ColorVal UIColor::primary_txt = ST77XX_BLACK; +ColorVal UIColor::secondary_txt = (18 << 11) | (36 << 5) | 18; // mid-gray +ColorVal UIColor::warning_txt = ST77XX_ORANGE; +ColorVal UIColor::popup_bkg = ST77XX_CYAN; +ColorVal UIColor::popup_txt = ST77XX_BLACK; +ColorVal UIColor::corp_blue = 0x001A; + bool ST7789Display::begin() { if(!_isOn) { pinMode(PIN_TFT_VDD_CTL, OUTPUT); @@ -90,9 +101,9 @@ void ST7789Display::clear() { display.clear(); } -void ST7789Display::startFrame(Color bkg) { - display.clear(); - _color = ST77XX_WHITE; +void ST7789Display::startFrame(ColorVal bkg) { + display.fillRect(0, 0, display.width(), display.height()); + _color = UIColor::primary_txt; display.setRGB(_color); display.setFont(ArialMT_Plain_16); } @@ -110,38 +121,8 @@ void ST7789Display::setTextSize(int sz) { } } -void ST7789Display::setColor(Color c) { - switch (c) { - case DisplayDriver::DARK : - _color = ST77XX_BLACK; - display.setColor(OLEDDISPLAY_COLOR::BLACK); - break; -#if 0 - case DisplayDriver::LIGHT : - _color = ST77XX_WHITE; - break; - case DisplayDriver::RED : - _color = ST77XX_RED; - break; - case DisplayDriver::GREEN : - _color = ST77XX_GREEN; - break; - case DisplayDriver::BLUE : - _color = ST77XX_BLUE; - break; - case DisplayDriver::YELLOW : - _color = ST77XX_YELLOW; - break; - case DisplayDriver::ORANGE : - _color = ST77XX_ORANGE; - break; -#endif - default: - _color = ST77XX_WHITE; - display.setColor(OLEDDISPLAY_COLOR::WHITE); - break; - } - display.setRGB(_color); +void ST7789Display::setColor(ColorVal c) { + display.setRGB(_color = c); } void ST7789Display::setCursor(int x, int y) { diff --git a/src/helpers/ui/ST7789Display.h b/src/helpers/ui/ST7789Display.h index 9822a67d..580ad463 100644 --- a/src/helpers/ui/ST7789Display.h +++ b/src/helpers/ui/ST7789Display.h @@ -27,9 +27,9 @@ public: void turnOn() override; void turnOff() override; void clear() override; - void startFrame(Color bkg = DARK) override; + void startFrame(ColorVal bkg = UIColor::window_bkg) override; void setTextSize(int sz) override; - void setColor(Color c) override; + void setColor(ColorVal c) override; void setCursor(int x, int y) override; void print(const char* str) override; void printWordWrap(const char* str, int max_width) override; diff --git a/src/helpers/ui/ST7789LCDDisplay.cpp b/src/helpers/ui/ST7789LCDDisplay.cpp index 7a02668b..a9f30dd5 100644 --- a/src/helpers/ui/ST7789LCDDisplay.cpp +++ b/src/helpers/ui/ST7789LCDDisplay.cpp @@ -23,6 +23,17 @@ bool ST7789LCDDisplay::i2c_probe(TwoWire& wire, uint8_t addr) { return true; } +// Color scheme +ColorVal UIColor::window_bkg = ST77XX_WHITE; +ColorVal UIColor::title_bkg = ST77XX_BLUE; +ColorVal UIColor::title_txt = ST77XX_WHITE; +ColorVal UIColor::primary_txt = ST77XX_BLACK; +ColorVal UIColor::secondary_txt = (18 << 11) | (36 << 5) | 18; // mid-gray +ColorVal UIColor::warning_txt = ST77XX_ORANGE; +ColorVal UIColor::popup_bkg = ST77XX_CYAN; +ColorVal UIColor::popup_txt = ST77XX_BLACK; +ColorVal UIColor::corp_blue = 0x001A; + bool ST7789LCDDisplay::begin() { if (!_isOn) { if (_peripher_power) _peripher_power->claim(); @@ -78,9 +89,9 @@ void ST7789LCDDisplay::clear() { display.fillScreen(ST77XX_BLACK); } -void ST7789LCDDisplay::startFrame(Color bkg) { - display.fillScreen(ST77XX_BLACK); - display.setTextColor(ST77XX_WHITE); +void ST7789LCDDisplay::startFrame(ColorVal bkg) { + display.fillScreen(bkg); + display.setTextColor(_color = UIColor::primary_txt); display.setTextSize(1 * DISPLAY_SCALE_X); // This one affects size of Please wait... message display.cp437(true); // Use full 256 char 'Code Page 437' font } @@ -89,34 +100,8 @@ void ST7789LCDDisplay::setTextSize(int sz) { display.setTextSize(sz * DISPLAY_SCALE_X); } -void ST7789LCDDisplay::setColor(Color c) { - switch (c) { - case DisplayDriver::DARK : - _color = ST77XX_BLACK; - break; - case DisplayDriver::LIGHT : - _color = ST77XX_WHITE; - break; - case DisplayDriver::RED : - _color = ST77XX_RED; - break; - case DisplayDriver::GREEN : - _color = ST77XX_GREEN; - break; - case DisplayDriver::BLUE : - _color = ST77XX_BLUE; - break; - case DisplayDriver::YELLOW : - _color = ST77XX_YELLOW; - break; - case DisplayDriver::ORANGE : - _color = ST77XX_ORANGE; - break; - default: - _color = ST77XX_WHITE; - break; - } - display.setTextColor(_color); +void ST7789LCDDisplay::setColor(ColorVal c) { + display.setTextColor(_color = c); } void ST7789LCDDisplay::setCursor(int x, int y) { diff --git a/src/helpers/ui/ST7789LCDDisplay.h b/src/helpers/ui/ST7789LCDDisplay.h index 03a6d3f1..b5127d35 100644 --- a/src/helpers/ui/ST7789LCDDisplay.h +++ b/src/helpers/ui/ST7789LCDDisplay.h @@ -47,9 +47,9 @@ public: void turnOn() override; void turnOff() override; void clear() override; - void startFrame(Color bkg = DARK) override; + void startFrame(ColorVal bkg = UIColor::window_bkg) override; void setTextSize(int sz) override; - void setColor(Color c) override; + void setColor(ColorVal c) override; void setCursor(int x, int y) override; void print(const char* str) override; void fillRect(int x, int y, int w, int h) override; diff --git a/src/helpers/ui/U8g2Display.cpp b/src/helpers/ui/U8g2Display.cpp new file mode 100644 index 00000000..72118a0e --- /dev/null +++ b/src/helpers/ui/U8g2Display.cpp @@ -0,0 +1,12 @@ +#include "U8g2Display.h" + +// Color scheme +ColorVal UIColor::window_bkg = 0; +ColorVal UIColor::title_bkg = 1; +ColorVal UIColor::title_txt = 0; +ColorVal UIColor::primary_txt = 1; +ColorVal UIColor::secondary_txt = 1; +ColorVal UIColor::warning_txt = 1; +ColorVal UIColor::popup_bkg = 1; +ColorVal UIColor::popup_txt = 0; +ColorVal UIColor::corp_blue = 1; diff --git a/src/helpers/ui/U8g2Display.h b/src/helpers/ui/U8g2Display.h index 73c58936..a87ee19b 100644 --- a/src/helpers/ui/U8g2Display.h +++ b/src/helpers/ui/U8g2Display.h @@ -72,10 +72,10 @@ public: _u8g2.sendBuffer(); } - void startFrame(Color bkg = DARK) override { - _u8g2.clearBuffer(); - _drawColor = 1; - _u8g2.setDrawColor(1); + void startFrame(ColorVal bkg = UIColor::window_bkg) override { + _u8g2.clearBuffer(); // TODO: apply 'bkg' color + _drawColor = UIColor::primary_txt; + _u8g2.setDrawColor(_drawColor); applyFont(1); } @@ -83,8 +83,8 @@ public: applyFont(sz); } - void setColor(Color c) override { - _drawColor = (c != DARK) ? 1 : 0; + void setColor(ColorVal c) override { + _drawColor = c; _u8g2.setDrawColor(_drawColor); } @@ -94,22 +94,18 @@ public: } void print(const char* str) override { - _u8g2.setDrawColor(_drawColor); _u8g2.drawStr(_cursorX, _cursorY, str); } void fillRect(int x, int y, int w, int h) override { - _u8g2.setDrawColor(_drawColor); _u8g2.drawBox(x, y, w, h); } void drawRect(int x, int y, int w, int h) override { - _u8g2.setDrawColor(_drawColor); _u8g2.drawFrame(x, y, w, h); } void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override { - _u8g2.setDrawColor(1); _u8g2.drawXBM(x, y, w, h, bits); } diff --git a/variants/lilygo_techo_card/platformio.ini b/variants/lilygo_techo_card/platformio.ini index 07bcebfe..c7bb7378 100644 --- a/variants/lilygo_techo_card/platformio.ini +++ b/variants/lilygo_techo_card/platformio.ini @@ -23,7 +23,7 @@ build_src_filter = ${nrf52_base.build_src_filter} + + + - + + + + +<../variants/lilygo_techo_card> lib_deps = From a96a66aa999df0c9a2ded5d99c156319360bb199 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Wed, 22 Jul 2026 18:55:59 +1000 Subject: [PATCH 43/70] * build fixes for NullDisplayDriver --- src/helpers/ui/NullDisplayDriver.cpp | 11 +++++++++ variants/heltec_rc32/platformio.ini | 3 +++ variants/heltec_t114/platformio.ini | 2 ++ variants/heltec_tower_v2/platformio.ini | 2 ++ variants/ikoka_nano_nrf/platformio.ini | 5 ++-- .../minewsemi_me25ls01/NullDisplayDriver.h | 24 ------------------- variants/minewsemi_me25ls01/platformio.ini | 5 ++++ variants/minewsemi_me25ls01/target.h | 2 +- variants/t1000-e/platformio.ini | 2 ++ variants/thinknode_m3/platformio.ini | 2 ++ variants/thinknode_m7/platformio.ini | 2 ++ variants/wio-e5-mini/NullDisplayDriver.h | 24 ------------------- variants/wio-e5-mini/platformio.ini | 1 + variants/wio-e5-mini/target.h | 2 +- 14 files changed, 34 insertions(+), 53 deletions(-) create mode 100644 src/helpers/ui/NullDisplayDriver.cpp delete mode 100644 variants/minewsemi_me25ls01/NullDisplayDriver.h delete mode 100644 variants/wio-e5-mini/NullDisplayDriver.h diff --git a/src/helpers/ui/NullDisplayDriver.cpp b/src/helpers/ui/NullDisplayDriver.cpp new file mode 100644 index 00000000..1500f5c1 --- /dev/null +++ b/src/helpers/ui/NullDisplayDriver.cpp @@ -0,0 +1,11 @@ +#include "NullDisplayDriver.h" + +ColorVal UIColor::window_bkg = 0; +ColorVal UIColor::title_bkg = 0; +ColorVal UIColor::title_txt = 0; +ColorVal UIColor::primary_txt = 0; +ColorVal UIColor::secondary_txt = 0; +ColorVal UIColor::warning_txt = 0; +ColorVal UIColor::popup_bkg = 0; +ColorVal UIColor::popup_txt = 0; +ColorVal UIColor::corp_blue = 0; diff --git a/variants/heltec_rc32/platformio.ini b/variants/heltec_rc32/platformio.ini index d535fb77..6361f61d 100644 --- a/variants/heltec_rc32/platformio.ini +++ b/variants/heltec_rc32/platformio.ini @@ -134,6 +134,7 @@ build_flags = build_src_filter = ${Heltec_RC32.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> @@ -157,6 +158,7 @@ build_flags = build_src_filter = ${Heltec_RC32.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> @@ -180,6 +182,7 @@ build_flags = build_src_filter = ${Heltec_RC32.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> diff --git a/variants/heltec_t114/platformio.ini b/variants/heltec_t114/platformio.ini index 135babb1..48dfe078 100644 --- a/variants/heltec_t114/platformio.ini +++ b/variants/heltec_t114/platformio.ini @@ -109,6 +109,7 @@ build_flags = ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_t114.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> @@ -128,6 +129,7 @@ build_flags = -D MAX_GROUP_CHANNELS=40 build_src_filter = ${Heltec_t114.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = diff --git a/variants/heltec_tower_v2/platformio.ini b/variants/heltec_tower_v2/platformio.ini index f029b7d4..9c703ae1 100644 --- a/variants/heltec_tower_v2/platformio.ini +++ b/variants/heltec_tower_v2/platformio.ini @@ -69,6 +69,7 @@ build_flags = ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_tower_v2.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> @@ -91,6 +92,7 @@ build_flags = ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_tower_v2.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> diff --git a/variants/ikoka_nano_nrf/platformio.ini b/variants/ikoka_nano_nrf/platformio.ini index e72f83ce..7880ea4a 100644 --- a/variants/ikoka_nano_nrf/platformio.ini +++ b/variants/ikoka_nano_nrf/platformio.ini @@ -31,6 +31,8 @@ debug_tool = jlink upload_protocol = nrfutil lib_deps = ${nrf52_base.lib_deps} ${sensor_base.lib_deps} +build_src_filter = ${nrf52_base.build_src_filter} + + [ikoka_nano_nrf_e22_22dbm] extends = ikoka_nano_nrf @@ -42,7 +44,6 @@ build_flags = build_src_filter = ${ikoka_nano_nrf.build_src_filter} + + - + +<../variants/ikoka_nano_nrf> [ikoka_nano_nrf_e22_30dbm] @@ -56,7 +57,6 @@ build_flags = build_src_filter = ${ikoka_nano_nrf.build_src_filter} + + - + +<../variants/ikoka_nano_nrf> [ikoka_nano_nrf_e22_33dbm] @@ -70,7 +70,6 @@ build_flags = build_src_filter = ${ikoka_nano_nrf.build_src_filter} + + - + +<../variants/ikoka_nano_nrf> [ikoka_nano_nrf_companion_radio_ble] diff --git a/variants/minewsemi_me25ls01/NullDisplayDriver.h b/variants/minewsemi_me25ls01/NullDisplayDriver.h deleted file mode 100644 index 38bf93f1..00000000 --- a/variants/minewsemi_me25ls01/NullDisplayDriver.h +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once - -#include - -class NullDisplayDriver : public DisplayDriver { -public: - NullDisplayDriver() : DisplayDriver(128, 64) { } - bool begin() { return false; } // not present - - bool isOn() override { return false; } - void turnOn() override { } - void turnOff() override { } - void clear() override { } - void startFrame(Color bkg = DARK) override { } - void setTextSize(int sz) override { } - void setColor(Color c) override { } - void setCursor(int x, int y) override { } - void print(const char* str) override { } - void fillRect(int x, int y, int w, int h) override { } - void drawRect(int x, int y, int w, int h) override { } - void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override { } - uint16_t getTextWidth(const char* str) override { return 0; } - void endFrame() { } -}; diff --git a/variants/minewsemi_me25ls01/platformio.ini b/variants/minewsemi_me25ls01/platformio.ini index 39d4252d..ac81fa16 100644 --- a/variants/minewsemi_me25ls01/platformio.ini +++ b/variants/minewsemi_me25ls01/platformio.ini @@ -55,6 +55,7 @@ build_flags = ${me25ls01.build_flags} ;-D PIN_BUZZER=25 ;-D PIN_BUZZER_EN=37 build_src_filter = ${me25ls01.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-orig/*.cpp> @@ -79,6 +80,7 @@ build_flags = ${me25ls01.build_flags} -D DISPLAY_CLASS=NullDisplayDriver build_src_filter = ${me25ls01.build_src_filter} +<../examples/simple_repeater> + + [env:Minewsemi_me25ls01_room_server] extends = me25ls01 @@ -101,6 +103,7 @@ build_flags = ${me25ls01.build_flags} -D DISPLAY_CLASS=NullDisplayDriver build_src_filter = ${me25ls01.build_src_filter} +<../examples/simple_room_server> + + [env:Minewsemi_me25ls01_terminal_chat] extends = me25ls01 @@ -123,6 +126,7 @@ build_flags = ${me25ls01.build_flags} -D DISPLAY_CLASS=NullDisplayDriver build_src_filter = ${me25ls01.build_src_filter} +<../examples/simple_secure_chat/main.cpp> + + [env:Minewsemi_me25ls01_companion_radio_usb] extends = me25ls01 @@ -142,6 +146,7 @@ build_flags = ${me25ls01.build_flags} -D DISPLAY_CLASS=NullDisplayDriver build_src_filter = ${me25ls01.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-orig/*.cpp> diff --git a/variants/minewsemi_me25ls01/target.h b/variants/minewsemi_me25ls01/target.h index f8d42863..978e616b 100644 --- a/variants/minewsemi_me25ls01/target.h +++ b/variants/minewsemi_me25ls01/target.h @@ -10,7 +10,7 @@ #include #include #ifdef DISPLAY_CLASS - #include "NullDisplayDriver.h" + #include #endif #ifdef DISPLAY_CLASS diff --git a/variants/t1000-e/platformio.ini b/variants/t1000-e/platformio.ini index 43a3d93f..6f32e54d 100644 --- a/variants/t1000-e/platformio.ini +++ b/variants/t1000-e/platformio.ini @@ -84,6 +84,7 @@ build_flags = ${t1000-e.build_flags} -D PIN_BUZZER_EN=37 ; P1/5 - required for T1000-E build_src_filter = ${t1000-e.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-orig/*.cpp> lib_deps = ${t1000-e.lib_deps} @@ -112,6 +113,7 @@ build_flags = ${t1000-e.build_flags} build_src_filter = ${t1000-e.build_src_filter} + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-orig/*.cpp> lib_deps = ${t1000-e.lib_deps} diff --git a/variants/thinknode_m3/platformio.ini b/variants/thinknode_m3/platformio.ini index 0004f4c7..f80a6161 100644 --- a/variants/thinknode_m3/platformio.ini +++ b/variants/thinknode_m3/platformio.ini @@ -85,6 +85,7 @@ build_flags = ${ThinkNode_M3.build_flags} -D PIN_BUZZER_EN=36 build_src_filter = ${ThinkNode_M3.build_src_filter} + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-orig/*.cpp> lib_deps = ${ThinkNode_M3.lib_deps} @@ -111,6 +112,7 @@ build_flags = ${ThinkNode_M3.build_flags} build_src_filter = ${ThinkNode_M3.build_src_filter} + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-orig/*.cpp> lib_deps = ${ThinkNode_M3.lib_deps} diff --git a/variants/thinknode_m7/platformio.ini b/variants/thinknode_m7/platformio.ini index af5d8ea0..acd98c8c 100644 --- a/variants/thinknode_m7/platformio.ini +++ b/variants/thinknode_m7/platformio.ini @@ -83,6 +83,7 @@ build_flags = build_src_filter = ${ThinkNode_M7.build_src_filter} + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-orig/*.cpp> lib_deps = @@ -122,6 +123,7 @@ build_flags = build_src_filter = ${ThinkNode_M7.build_src_filter} + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-orig/*.cpp> lib_deps = diff --git a/variants/wio-e5-mini/NullDisplayDriver.h b/variants/wio-e5-mini/NullDisplayDriver.h deleted file mode 100644 index 2a9670bd..00000000 --- a/variants/wio-e5-mini/NullDisplayDriver.h +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once - -#include - -class NullDisplayDriver : public DisplayDriver { -public: - NullDisplayDriver() : DisplayDriver(128, 64) { } - bool begin() { return false; } // not present - - bool isOn() override { return false; } - void turnOn() override { } - void turnOff() override { } - void clear() override { } - void startFrame(Color bkg = DARK) override { } - void setTextSize(int sz) override { } - void setColor(Color c) override { } - void setCursor(int x, int y) override { } - void print(const char* str) override { } - void fillRect(int x, int y, int w, int h) override { } - void drawRect(int x, int y, int w, int h) override { } - void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override { } - uint16_t getTextWidth(const char* str) override { return 0; } - void endFrame() { } -}; diff --git a/variants/wio-e5-mini/platformio.ini b/variants/wio-e5-mini/platformio.ini index 82f01331..7cf26198 100644 --- a/variants/wio-e5-mini/platformio.ini +++ b/variants/wio-e5-mini/platformio.ini @@ -45,6 +45,7 @@ build_flags = ${lora_e5_mini.build_flags} -D MAX_GROUP_CHANNELS=8 -D DISPLAY_CLASS=NullDisplayDriver build_src_filter = ${lora_e5_mini.build_src_filter} + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-orig/*.cpp> lib_deps = ${lora_e5_mini.lib_deps} diff --git a/variants/wio-e5-mini/target.h b/variants/wio-e5-mini/target.h index 4807e0f7..f4d6a088 100644 --- a/variants/wio-e5-mini/target.h +++ b/variants/wio-e5-mini/target.h @@ -8,7 +8,7 @@ #include #include #ifdef DISPLAY_CLASS - #include "NullDisplayDriver.h" + #include #endif #include From 56274db4ad7ccff236b7cb908927fe6161df2e9e Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Wed, 22 Jul 2026 19:52:23 +1000 Subject: [PATCH 44/70] * build fixes for old ESP32 boards --- examples/simple_repeater/MyMesh.h | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 0b2e7491..aa7d30b0 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -11,6 +11,7 @@ #include #elif defined(ESP32) #include + using File = fs::File; #endif #ifdef WITH_RS232_BRIDGE From 520fa77f4fedfe0d39eb0c472f7c7346f470df82 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Fri, 24 Jul 2026 15:31:01 +1200 Subject: [PATCH 45/70] increase tbeam sx1276 max group channels from 8 to 40 --- variants/lilygo_tbeam_SX1276/platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/lilygo_tbeam_SX1276/platformio.ini b/variants/lilygo_tbeam_SX1276/platformio.ini index cb25903c..f5f49047 100644 --- a/variants/lilygo_tbeam_SX1276/platformio.ini +++ b/variants/lilygo_tbeam_SX1276/platformio.ini @@ -45,7 +45,7 @@ build_flags = ${LilyGo_TBeam_SX1276.build_flags} -I examples/companion_radio/ui-new -D MAX_CONTACTS=160 - -D MAX_GROUP_CHANNELS=8 + -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 ; -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=128 From 3857f4582643ab99e4210a60f05b5963a728cd0a Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Fri, 24 Jul 2026 15:14:56 +1000 Subject: [PATCH 46/70] * monochrome displays now do _not_ invert titlebar --- src/helpers/ui/E213Display.cpp | 8 ++++---- src/helpers/ui/E290Display.cpp | 8 ++++---- src/helpers/ui/GxEPDDisplay.cpp | 8 ++++---- src/helpers/ui/SH1106Display.cpp | 8 ++++---- src/helpers/ui/SSD1306Display.cpp | 8 ++++---- src/helpers/ui/U8g2Display.cpp | 8 ++++---- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/helpers/ui/E213Display.cpp b/src/helpers/ui/E213Display.cpp index ab742976..daf26989 100644 --- a/src/helpers/ui/E213Display.cpp +++ b/src/helpers/ui/E213Display.cpp @@ -4,13 +4,13 @@ // Color scheme ColorVal UIColor::window_bkg = WHITE; -ColorVal UIColor::title_bkg = BLACK; -ColorVal UIColor::title_txt = WHITE; +ColorVal UIColor::title_bkg = WHITE; +ColorVal UIColor::title_txt = BLACK; ColorVal UIColor::primary_txt = BLACK; ColorVal UIColor::secondary_txt = BLACK; ColorVal UIColor::warning_txt = BLACK; -ColorVal UIColor::popup_bkg = BLACK; -ColorVal UIColor::popup_txt = WHITE; +ColorVal UIColor::popup_bkg = WHITE; +ColorVal UIColor::popup_txt = BLACK; ColorVal UIColor::corp_blue = BLACK; BaseDisplay* E213Display::detectEInk() diff --git a/src/helpers/ui/E290Display.cpp b/src/helpers/ui/E290Display.cpp index ddb449f4..34bbcdbc 100644 --- a/src/helpers/ui/E290Display.cpp +++ b/src/helpers/ui/E290Display.cpp @@ -4,13 +4,13 @@ // Color scheme ColorVal UIColor::window_bkg = WHITE; -ColorVal UIColor::title_bkg = BLACK; -ColorVal UIColor::title_txt = WHITE; +ColorVal UIColor::title_bkg = WHITE; +ColorVal UIColor::title_txt = BLACK; ColorVal UIColor::primary_txt = BLACK; ColorVal UIColor::secondary_txt = BLACK; ColorVal UIColor::warning_txt = BLACK; -ColorVal UIColor::popup_bkg = BLACK; -ColorVal UIColor::popup_txt = WHITE; +ColorVal UIColor::popup_bkg = WHITE; +ColorVal UIColor::popup_txt = BLACK; ColorVal UIColor::corp_blue = BLACK; bool E290Display::begin() { diff --git a/src/helpers/ui/GxEPDDisplay.cpp b/src/helpers/ui/GxEPDDisplay.cpp index 0a5f6a55..13dafa34 100644 --- a/src/helpers/ui/GxEPDDisplay.cpp +++ b/src/helpers/ui/GxEPDDisplay.cpp @@ -16,13 +16,13 @@ // Color scheme ColorVal UIColor::window_bkg = GxEPD_WHITE; -ColorVal UIColor::title_bkg = GxEPD_BLACK; -ColorVal UIColor::title_txt = GxEPD_WHITE; +ColorVal UIColor::title_bkg = GxEPD_WHITE; +ColorVal UIColor::title_txt = GxEPD_BLACK; ColorVal UIColor::primary_txt = GxEPD_BLACK; ColorVal UIColor::secondary_txt = GxEPD_BLACK; ColorVal UIColor::warning_txt = GxEPD_BLACK; -ColorVal UIColor::popup_bkg = GxEPD_BLACK; -ColorVal UIColor::popup_txt = GxEPD_WHITE; +ColorVal UIColor::popup_bkg = GxEPD_WHITE; +ColorVal UIColor::popup_txt = GxEPD_BLACK; ColorVal UIColor::corp_blue = GxEPD_BLACK; diff --git a/src/helpers/ui/SH1106Display.cpp b/src/helpers/ui/SH1106Display.cpp index aeb4f300..c3840c02 100644 --- a/src/helpers/ui/SH1106Display.cpp +++ b/src/helpers/ui/SH1106Display.cpp @@ -11,13 +11,13 @@ bool SH1106Display::i2c_probe(TwoWire &wire, uint8_t addr) // Color scheme ColorVal UIColor::window_bkg = SH110X_BLACK; -ColorVal UIColor::title_bkg = SH110X_WHITE; -ColorVal UIColor::title_txt = SH110X_BLACK; +ColorVal UIColor::title_bkg = SH110X_BLACK; +ColorVal UIColor::title_txt = SH110X_WHITE; ColorVal UIColor::primary_txt = SH110X_WHITE; ColorVal UIColor::secondary_txt = SH110X_WHITE; ColorVal UIColor::warning_txt = SH110X_WHITE; -ColorVal UIColor::popup_bkg = SH110X_WHITE; -ColorVal UIColor::popup_txt = SH110X_BLACK; +ColorVal UIColor::popup_bkg = SH110X_BLACK; +ColorVal UIColor::popup_txt = SH110X_WHITE; ColorVal UIColor::corp_blue = SH110X_WHITE; bool SH1106Display::begin() diff --git a/src/helpers/ui/SSD1306Display.cpp b/src/helpers/ui/SSD1306Display.cpp index 9b858071..ab2d77fb 100644 --- a/src/helpers/ui/SSD1306Display.cpp +++ b/src/helpers/ui/SSD1306Display.cpp @@ -8,13 +8,13 @@ bool SSD1306Display::i2c_probe(TwoWire& wire, uint8_t addr) { // Color scheme ColorVal UIColor::window_bkg = SSD1306_BLACK; -ColorVal UIColor::title_bkg = SSD1306_WHITE; -ColorVal UIColor::title_txt = SSD1306_BLACK; +ColorVal UIColor::title_bkg = SSD1306_BLACK; +ColorVal UIColor::title_txt = SSD1306_WHITE; ColorVal UIColor::primary_txt = SSD1306_WHITE; ColorVal UIColor::secondary_txt = SSD1306_WHITE; ColorVal UIColor::warning_txt = SSD1306_WHITE; -ColorVal UIColor::popup_bkg = SSD1306_WHITE; -ColorVal UIColor::popup_txt = SSD1306_BLACK; +ColorVal UIColor::popup_bkg = SSD1306_BLACK; +ColorVal UIColor::popup_txt = SSD1306_WHITE; ColorVal UIColor::corp_blue = SSD1306_WHITE; bool SSD1306Display::begin() { diff --git a/src/helpers/ui/U8g2Display.cpp b/src/helpers/ui/U8g2Display.cpp index 72118a0e..e8d7fe2f 100644 --- a/src/helpers/ui/U8g2Display.cpp +++ b/src/helpers/ui/U8g2Display.cpp @@ -2,11 +2,11 @@ // Color scheme ColorVal UIColor::window_bkg = 0; -ColorVal UIColor::title_bkg = 1; -ColorVal UIColor::title_txt = 0; +ColorVal UIColor::title_bkg = 0; +ColorVal UIColor::title_txt = 1; ColorVal UIColor::primary_txt = 1; ColorVal UIColor::secondary_txt = 1; ColorVal UIColor::warning_txt = 1; -ColorVal UIColor::popup_bkg = 1; -ColorVal UIColor::popup_txt = 0; +ColorVal UIColor::popup_bkg = 0; +ColorVal UIColor::popup_txt = 1; ColorVal UIColor::corp_blue = 1; From 0c04a473559a3ffa0c8bd7c881ac3820da28fef7 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Fri, 24 Jul 2026 15:31:22 +1000 Subject: [PATCH 47/70] * fixed for mono ST7789 display driver --- src/helpers/ui/ST7789Display.cpp | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/helpers/ui/ST7789Display.cpp b/src/helpers/ui/ST7789Display.cpp index fad75194..7aff92a1 100644 --- a/src/helpers/ui/ST7789Display.cpp +++ b/src/helpers/ui/ST7789Display.cpp @@ -27,15 +27,15 @@ #endif // Color scheme -ColorVal UIColor::window_bkg = ST77XX_WHITE; -ColorVal UIColor::title_bkg = ST77XX_BLUE; -ColorVal UIColor::title_txt = ST77XX_WHITE; -ColorVal UIColor::primary_txt = ST77XX_BLACK; -ColorVal UIColor::secondary_txt = (18 << 11) | (36 << 5) | 18; // mid-gray -ColorVal UIColor::warning_txt = ST77XX_ORANGE; -ColorVal UIColor::popup_bkg = ST77XX_CYAN; -ColorVal UIColor::popup_txt = ST77XX_BLACK; -ColorVal UIColor::corp_blue = 0x001A; +ColorVal UIColor::window_bkg = OLEDDISPLAY_COLOR::WHITE; +ColorVal UIColor::title_bkg = OLEDDISPLAY_COLOR::WHITE; +ColorVal UIColor::title_txt = OLEDDISPLAY_COLOR::BLACK; +ColorVal UIColor::primary_txt = OLEDDISPLAY_COLOR::BLACK; +ColorVal UIColor::secondary_txt = OLEDDISPLAY_COLOR::BLACK; +ColorVal UIColor::warning_txt = OLEDDISPLAY_COLOR::BLACK; +ColorVal UIColor::popup_bkg = OLEDDISPLAY_COLOR::WHITE; +ColorVal UIColor::popup_txt = OLEDDISPLAY_COLOR::BLACK; +ColorVal UIColor::corp_blue = OLEDDISPLAY_COLOR::BLACK; bool ST7789Display::begin() { if(!_isOn) { @@ -102,9 +102,8 @@ void ST7789Display::clear() { } void ST7789Display::startFrame(ColorVal bkg) { - display.fillRect(0, 0, display.width(), display.height()); - _color = UIColor::primary_txt; - display.setRGB(_color); + display.clear(); // TODO: use bkg + setColor(UIColor::primary_txt); display.setFont(ArialMT_Plain_16); } @@ -122,7 +121,9 @@ void ST7789Display::setTextSize(int sz) { } void ST7789Display::setColor(ColorVal c) { - display.setRGB(_color = c); + _color = c; + display.setColor((OLEDDISPLAY_COLOR)_color); + display.setRGB(_color == OLEDDISPLAY_COLOR::WHITE ? ST77XX_WHITE : ST77XX_BLACK); } void ST7789Display::setCursor(int x, int y) { From f83ad03e5e79fcb54eb5b91e3675df748c3434fa Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Fri, 24 Jul 2026 18:35:56 +1000 Subject: [PATCH 48/70] * T114 display fix --- examples/companion_radio/ui-new/UITask.cpp | 6 +++++- src/helpers/ui/ST7789Display.cpp | 18 +++++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 09903db0..50dc91d2 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -204,7 +204,11 @@ public: renderBatteryIndicator(display, _task->getBattMilliVolts()); // curr page indicator - display.setColor(UIColor::title_bkg); + if (UIColor::title_bkg == UIColor::window_bkg) { + display.setColor(UIColor::title_txt); + } else { + display.setColor(UIColor::title_bkg); + } int y = 14; int x = display.width() / 2 - 5 * (HomePage::Count-1); for (uint8_t i = 0; i < HomePage::Count; i++, x += 10) { diff --git a/src/helpers/ui/ST7789Display.cpp b/src/helpers/ui/ST7789Display.cpp index 7aff92a1..7d039a13 100644 --- a/src/helpers/ui/ST7789Display.cpp +++ b/src/helpers/ui/ST7789Display.cpp @@ -27,15 +27,15 @@ #endif // Color scheme -ColorVal UIColor::window_bkg = OLEDDISPLAY_COLOR::WHITE; -ColorVal UIColor::title_bkg = OLEDDISPLAY_COLOR::WHITE; -ColorVal UIColor::title_txt = OLEDDISPLAY_COLOR::BLACK; -ColorVal UIColor::primary_txt = OLEDDISPLAY_COLOR::BLACK; -ColorVal UIColor::secondary_txt = OLEDDISPLAY_COLOR::BLACK; -ColorVal UIColor::warning_txt = OLEDDISPLAY_COLOR::BLACK; -ColorVal UIColor::popup_bkg = OLEDDISPLAY_COLOR::WHITE; -ColorVal UIColor::popup_txt = OLEDDISPLAY_COLOR::BLACK; -ColorVal UIColor::corp_blue = OLEDDISPLAY_COLOR::BLACK; +ColorVal UIColor::window_bkg = OLEDDISPLAY_COLOR::BLACK; +ColorVal UIColor::title_bkg = OLEDDISPLAY_COLOR::BLACK; +ColorVal UIColor::title_txt = OLEDDISPLAY_COLOR::WHITE; +ColorVal UIColor::primary_txt = OLEDDISPLAY_COLOR::WHITE; +ColorVal UIColor::secondary_txt = OLEDDISPLAY_COLOR::WHITE; +ColorVal UIColor::warning_txt = OLEDDISPLAY_COLOR::WHITE; +ColorVal UIColor::popup_bkg = OLEDDISPLAY_COLOR::BLACK; +ColorVal UIColor::popup_txt = OLEDDISPLAY_COLOR::WHITE; +ColorVal UIColor::corp_blue = OLEDDISPLAY_COLOR::WHITE; bool ST7789Display::begin() { if(!_isOn) { From fae480315829ded473ffefc8e7d8988f8830b7d8 Mon Sep 17 00:00:00 2001 From: taco Date: Mon, 20 Jul 2026 20:50:38 +1000 Subject: [PATCH 49/70] SX1262: add PREAMBLE_DETECTED to reported irq flags --- src/helpers/radiolib/CustomSX1262.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/helpers/radiolib/CustomSX1262.h b/src/helpers/radiolib/CustomSX1262.h index 15efed5c..6a7c2144 100644 --- a/src/helpers/radiolib/CustomSX1262.h +++ b/src/helpers/radiolib/CustomSX1262.h @@ -100,6 +100,11 @@ class CustomSX1262 : public SX1262 { return true; // success } + int16_t startReceive() override { + // include the PREAMBLE_DETECTED irq bit in reported flags + return SX1262::startReceive(RADIOLIB_SX126X_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); + } + bool isReceiving() { uint32_t irq = getIrqFlags(); bool preamble = irq & RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED; // bit 2 From ea5d7c8bf5b93b25ecb718b3654a3736a483308b Mon Sep 17 00:00:00 2001 From: taco Date: Sat, 25 Jul 2026 00:46:36 +1000 Subject: [PATCH 50/70] LR1110: add PREAMBLE_DETECTED to reported irq flags --- src/helpers/radiolib/CustomLR1110.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/helpers/radiolib/CustomLR1110.h b/src/helpers/radiolib/CustomLR1110.h index 0470e48f..20aea9ff 100644 --- a/src/helpers/radiolib/CustomLR1110.h +++ b/src/helpers/radiolib/CustomLR1110.h @@ -35,6 +35,11 @@ class CustomLR1110 : public LR1110 { bool getRxBoostedGainMode() const { return _rx_boosted; } + int16_t startReceive() override { + // include the PREAMBLE_DETECTED irq bit in reported flags + return LR1110::startReceive(RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); + } + bool isReceiving() { uint32_t irq = getIrqStatus(); bool preamble = irq & RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED; // bit 4 From c56928347f980ac8aa654ba0c34eed030c990b93 Mon Sep 17 00:00:00 2001 From: taco Date: Sat, 25 Jul 2026 00:18:59 +1000 Subject: [PATCH 51/70] LLCC68: add PREAMBLE_DETECTED to reported irq flags --- src/helpers/radiolib/CustomLLCC68.h | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/helpers/radiolib/CustomLLCC68.h b/src/helpers/radiolib/CustomLLCC68.h index 686b09ec..bde44b6c 100644 --- a/src/helpers/radiolib/CustomLLCC68.h +++ b/src/helpers/radiolib/CustomLLCC68.h @@ -2,9 +2,6 @@ #include -#define SX126X_IRQ_HEADER_VALID 0b0000010000 // 4 4 valid LoRa header received -#define SX126X_IRQ_PREAMBLE_DETECTED 0x04 - class CustomLLCC68 : public LLCC68 { public: CustomLLCC68(Module *mod) : LLCC68(mod) { } @@ -78,9 +75,14 @@ class CustomLLCC68 : public LLCC68 { return true; // success } + int16_t startReceive() override { + // include the PREAMBLE_DETECTED irq bit in reported flags + return LLCC68::startReceive(RADIOLIB_SX126X_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); + } + bool isReceiving() { uint16_t irq = getIrqFlags(); - bool detected = (irq & SX126X_IRQ_HEADER_VALID) || (irq & SX126X_IRQ_PREAMBLE_DETECTED); + bool detected = (irq & RADIOLIB_SX126X_IRQ_HEADER_VALID) || (irq & RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED); return detected; } From 0d2b988f24470ce2f0c6ea3343fc4d2144364627 Mon Sep 17 00:00:00 2001 From: taco Date: Sat, 25 Jul 2026 00:20:02 +1000 Subject: [PATCH 52/70] STM32WLx: add PREAMBLE_DETECTED to reported irq flags --- src/helpers/radiolib/CustomSTM32WLx.h | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/helpers/radiolib/CustomSTM32WLx.h b/src/helpers/radiolib/CustomSTM32WLx.h index cbdd5c8c..01190d06 100644 --- a/src/helpers/radiolib/CustomSTM32WLx.h +++ b/src/helpers/radiolib/CustomSTM32WLx.h @@ -2,16 +2,18 @@ #include -#define SX126X_IRQ_HEADER_VALID 0b0000010000 // 4 4 valid LoRa header received -#define SX126X_IRQ_PREAMBLE_DETECTED 0x04 - class CustomSTM32WLx : public STM32WLx { public: CustomSTM32WLx(STM32WLx_Module *mod) : STM32WLx(mod) { } + int16_t startReceive() override { + // include the PREAMBLE_DETECTED irq bit in reported flags + return STM32WLx::startReceive(RADIOLIB_SX126X_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); + } + bool isReceiving() { uint16_t irq = getIrqFlags(); - bool detected = (irq & SX126X_IRQ_HEADER_VALID) || (irq & SX126X_IRQ_PREAMBLE_DETECTED); + bool detected = (irq & RADIOLIB_SX126X_IRQ_HEADER_VALID) || (irq & RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED); return detected; } }; \ No newline at end of file From 6b83de507d946c01322191233dbabb9b77d7712c Mon Sep 17 00:00:00 2001 From: taco Date: Sat, 25 Jul 2026 00:20:57 +1000 Subject: [PATCH 53/70] SX1268: add PREAMBLE_DETECTED to reported irq flags --- src/helpers/radiolib/CustomSX1268.h | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/helpers/radiolib/CustomSX1268.h b/src/helpers/radiolib/CustomSX1268.h index 0c6f828b..1e71a3fa 100644 --- a/src/helpers/radiolib/CustomSX1268.h +++ b/src/helpers/radiolib/CustomSX1268.h @@ -2,9 +2,6 @@ #include -#define SX126X_IRQ_HEADER_VALID 0b0000010000 // 4 4 valid LoRa header received -#define SX126X_IRQ_PREAMBLE_DETECTED 0x04 - class CustomSX1268 : public SX1268 { public: CustomSX1268(Module *mod) : SX1268(mod) { } @@ -78,9 +75,14 @@ class CustomSX1268 : public SX1268 { return true; // success } + int16_t startReceive() override { + // include the PREAMBLE_DETECTED irq bit in reported flags + return SX1268::startReceive(RADIOLIB_SX126X_RX_TIMEOUT_INF, RADIOLIB_IRQ_RX_DEFAULT_FLAGS | (1UL << RADIOLIB_IRQ_PREAMBLE_DETECTED), RADIOLIB_IRQ_RX_DEFAULT_MASK, 0); + } + bool isReceiving() { uint16_t irq = getIrqFlags(); - bool detected = (irq & SX126X_IRQ_HEADER_VALID) || (irq & SX126X_IRQ_PREAMBLE_DETECTED); + bool detected = (irq & RADIOLIB_SX126X_IRQ_HEADER_VALID) || (irq & RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED); return detected; } From b62b421456fdbd7c86d6eeadbabf8db48bd054cc Mon Sep 17 00:00:00 2001 From: Talia Date: Sat, 25 Jul 2026 20:55:58 -0700 Subject: [PATCH 54/70] docs: increase header levels on payloads page to fix table of contents mkdocs will only consider the first H1 (if any) and subheaders under it for the table of contents this increases the header levels of everything below "important concepts" by 1 so that the table of contents correctly resolves them --- docs/payloads.md | 52 ++++++++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/docs/payloads.md b/docs/payloads.md index 21cb9469..b4a98b38 100644 --- a/docs/payloads.md +++ b/docs/payloads.md @@ -23,7 +23,7 @@ NOTE: all 16 and 32-bit integer fields are Little Endian. * Node hash: the first byte of the node's public key -# Node advertisement +## Node advertisement This kind of payload notifies receivers that a node exists, and gives information about the node | Field | Size (bytes) | Description | @@ -57,7 +57,7 @@ Appdata Flags | `0x40` | has feature 2 | Reserved for future use. | | `0x80` | has name | appdata contains a node name | -# Acknowledgement +## Acknowledgement An acknowledgement that a message was received. Note that for returned path messages, an acknowledgement can be sent in the "extra" payload (see [Returned Path](#returned-path)) instead of as a separate acknowledgement packet. CLI commands do not cause acknowledgement responses, neither discrete nor extra. @@ -66,7 +66,7 @@ An acknowledgement that a message was received. Note that for returned path mess | checksum | 4 | CRC checksum of message timestamp, text, and sender pubkey | -# Returned path, request, response, and plain text message +## Returned path, request, response, and plain text message Returned path, request, response, and plain text messages are all formatted in the same way. See the subsection for more details about the ciphertext's associated plaintext representation. @@ -77,7 +77,7 @@ Returned path, request, response, and plain text messages are all formatted in t | cipher MAC | 2 | MAC for encrypted data in next field | | ciphertext | rest of payload | encrypted message, see subsections below for details | -## Returned path +### Returned path Returned path messages provide a description of the route a packet took from the original author. Receivers will send returned path messages to the author of the original message. @@ -88,7 +88,7 @@ Returned path messages provide a description of the route a packet took from the | extra type | 1 | extra, bundled payload type, eg., acknowledgement or response. Same values as in [Packet Format](./packet_format.md) | | extra | rest of data | extra, bundled payload content, follows same format as main content defined by this document | -## Request +### Request | Field | Size (bytes) | Description | |--------------|-----------------|------------------------------------------| @@ -102,7 +102,7 @@ For the common chat/server helpers in `BaseChatMesh`, the current request type v | `0x01` | get stats | get stats of repeater or room server | | `0x02` | keepalive | keep-alive request used for maintained connections | -### Get stats +#### Get stats Gets information about the node, possibly including the following: @@ -125,32 +125,32 @@ Gets information about the node, possibly including the following: * Number posted (?) * Number of post pushes (?) -### Get telemetry data +#### Get telemetry data Not defined in `BaseChatMesh`. Sensor- and application-specific request payloads may be implemented by higher-level firmware. -### Get Telemetry +#### Get Telemetry Not defined in `BaseChatMesh`. -### Get Min/Max/Ave (Sensor nodes) +#### Get Min/Max/Ave (Sensor nodes) Not defined in `BaseChatMesh`. -### Get Access List +#### Get Access List Not defined in `BaseChatMesh`. -### Get Neighbors +#### Get Neighbors Not defined in `BaseChatMesh`. -### Get Owner Info +#### Get Owner Info Not defined in `BaseChatMesh`. -## Response +### Response | Field | Size (bytes) | Description | |---------|-----------------|-----------------------------------| @@ -158,7 +158,7 @@ Not defined in `BaseChatMesh`. Response contents are opaque application data. There is no single generic response envelope beyond the encrypted payload wrapper shown above. -## Plain text message +### Plain text message | Field | Size (bytes) | Description | |--------------------|-----------------|-----------------------------------------------------------------------------------| @@ -174,7 +174,7 @@ txt_type | `0x01` | CLI command | the command text of the message | | `0x02` | signed plain text message | first four bytes is sender pubkey prefix, followed by plain text message | -# Anonymous request +## Anonymous request | Field | Size (bytes) | Description | |------------------|-----------------|-------------------------------------------| @@ -183,7 +183,7 @@ txt_type | cipher MAC | 2 | MAC for encrypted data in next field | | ciphertext | rest of payload | encrypted message, see below for details | -## Room server login +### Room server login | Field | Size (bytes) | Description | |----------------|-----------------|-------------------------------------------------------------------------------| @@ -191,14 +191,14 @@ txt_type | sync timestamp | 4 | sender's "sync messages SINCE x" timestamp | | password | rest of message | password for room | -## Repeater/Sensor login +### Repeater/Sensor login | Field | Size (bytes) | Description | |----------------|-----------------|-------------------------------------------------------------------------------| | timestamp | 4 | sender time (unix timestamp) | | password | rest of message | password for repeater/sensor | -## Repeater - Regions request +### Repeater - Regions request | Field | Size (bytes) | Description | |----------------|--------------|------------------------------| @@ -207,7 +207,7 @@ txt_type | reply path len | 1 | path len for reply | | reply path | (variable) | reply path | -## Repeater - Owner info request +### Repeater - Owner info request | Field | Size (bytes) | Description | |----------------|--------------|------------------------------| @@ -216,7 +216,7 @@ txt_type | reply path len | 1 | path len for reply | | reply path | (variable) | reply path | -## Repeater - Clock and status request +### Repeater - Clock and status request | Field | Size (bytes) | Description | |----------------|--------------|------------------------------| @@ -226,7 +226,7 @@ txt_type | reply path | (variable) | reply path | -# Group text message +## Group text message | Field | Size (bytes) | Description | |--------------|-----------------|----------------------------------------------| @@ -236,7 +236,7 @@ txt_type The plaintext contained in the ciphertext matches the format described in [plain text message](#plain-text-message). Specifically, it consists of a four byte timestamp, a flags byte, and the message. The flags byte will generally be `0x00` because it is a "plain text message". The message will be of the form `: ` (eg., `user123: I'm on my way`). -# Group datagram +## Group datagram | Field | Size (bytes) | Description | |--------------|-----------------|----------------------------------------------| @@ -253,14 +253,14 @@ The data contained in the ciphertext uses the format below: | data | rest of payload | (depends on data type) | -# Control data +## Control data | Field | Size (bytes) | Description | |--------------|-----------------|--------------------------------------------| | flags | 1 | upper 4 bits is sub_type | | data | rest of payload | typically unencrypted data | -## DISCOVER_REQ (sub_type) +### DISCOVER_REQ (sub_type) | Field | Size (bytes) | Description | |--------------|-----------------|----------------------------------------------| @@ -269,7 +269,7 @@ The data contained in the ciphertext uses the format below: | tag | 4 | randomly generate by sender | | since | 4 | (optional) epoch timestamp (0 by default) | -## DISCOVER_RESP (sub_type) +### DISCOVER_RESP (sub_type) | Field | Size (bytes) | Description | |--------------|-----------------|--------------------------------------------| @@ -279,6 +279,6 @@ The data contained in the ciphertext uses the format below: | pubkey | 8 or 32 | node's ID (or prefix) | -# Custom packet +## Custom packet Custom packets have no defined format. From 5de857f8f830f27abb360cdb5814ae3be07e8b5d Mon Sep 17 00:00:00 2001 From: liamcottle Date: Fri, 17 Jul 2026 23:59:27 +1200 Subject: [PATCH 55/70] initial ch390 ethernet support for thinknode m7 companion --- examples/companion_radio/main.cpp | 5 + src/helpers/ethernet/Ethernet.h | 9 + .../ethernet/ch390/CH390EthernetInterface.cpp | 207 ++++++++++++++++++ .../ethernet/ch390/CH390EthernetInterface.h | 80 +++++++ variants/thinknode_m7/platformio.ini | 36 +++ 5 files changed, 337 insertions(+) create mode 100644 src/helpers/ethernet/Ethernet.h create mode 100644 src/helpers/ethernet/ch390/CH390EthernetInterface.cpp create mode 100644 src/helpers/ethernet/ch390/CH390EthernetInterface.h diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index d39aeef9..2fc00dbd 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -48,6 +48,9 @@ static uint32_t _atoi(const char* sp) { #include ArduinoSerialInterface serial_interface; HardwareSerial companion_serial(1); + #elif defined(ETHERNET_ENABLED) + #include + ETHERNET_CLASS serial_interface; #else #include ArduinoSerialInterface serial_interface; @@ -240,6 +243,8 @@ void setup() { companion_serial.setPins(SERIAL_RX, SERIAL_TX); companion_serial.begin(115200); serial_interface.begin(companion_serial); +#elif defined(ETHERNET_ENABLED) + serial_interface.begin(); #else serial_interface.begin(Serial); #endif diff --git a/src/helpers/ethernet/Ethernet.h b/src/helpers/ethernet/Ethernet.h new file mode 100644 index 00000000..4d889df4 --- /dev/null +++ b/src/helpers/ethernet/Ethernet.h @@ -0,0 +1,9 @@ +#pragma once + +#if defined(ETHERNET_ENABLED) + #if defined(ETHERNET_USE_CH390) + #include "helpers/ethernet/ch390/CH390EthernetInterface.h" + #else + #error "ETHERNET_ENABLED is defined, but no specific driver flag (e.g. ETHERNET_USE_CH390) was provided!" + #endif +#endif diff --git a/src/helpers/ethernet/ch390/CH390EthernetInterface.cpp b/src/helpers/ethernet/ch390/CH390EthernetInterface.cpp new file mode 100644 index 00000000..f7ee5503 --- /dev/null +++ b/src/helpers/ethernet/ch390/CH390EthernetInterface.cpp @@ -0,0 +1,207 @@ +#include "CH390EthernetInterface.h" + +#define RECV_STATE_IDLE 0 +#define RECV_STATE_HDR_FOUND 1 +#define RECV_STATE_LEN1_FOUND 2 +#define RECV_STATE_LEN2_FOUND 3 + +bool CH390EthernetInterface::begin() { + ETHERNET_DEBUG_PRINTLN("Ethernet initializing"); + + // Init CH390 + ch390_config_t config = CH390_DEFAULT_CONFIG(); + config.spi_miso_gpio = ETH_MISO_PIN; + config.spi_mosi_gpio = ETH_MOSI_PIN; + config.spi_sck_gpio = ETH_SCLK_PIN; + config.spi_cs_gpio = ETH_CS_PIN; + config.int_gpio = ETH_INT_PIN; + if (!CH390.begin(config)) { + ETHERNET_DEBUG_PRINTLN("Failed to initialize CH390 hardware."); + return false; + } + + // Setup Static IP if build flags are present + #if defined(ETHERNET_STATIC_IP) && defined(ETHERNET_STATIC_GATEWAY) && defined(ETHERNET_STATIC_SUBNET) + IPAddress ip(ETHERNET_STATIC_IP); + IPAddress gw(ETHERNET_STATIC_GATEWAY); + IPAddress sn(ETHERNET_STATIC_SUBNET); + CH390.config(ip, gw, sn); + #endif + + // Start Server + server.begin(); + ETHERNET_DEBUG_PRINTLN("listening on TCP port: %d", ETHERNET_TCP_PORT); + + return true; +} + +void CH390EthernetInterface::enable() { + if (_isEnabled) return; + _isEnabled = true; + clearBuffers(); +} + +void CH390EthernetInterface::disable() { + _isEnabled = false; +} + +size_t CH390EthernetInterface::writeFrame(const uint8_t src[], size_t len) { + if (len > MAX_FRAME_SIZE) { + ETHERNET_DEBUG_PRINTLN("writeFrame(), frame too big, len=%d\n", len); + return 0; + } + + if (deviceConnected && len > 0) { + if (send_queue_len >= FRAME_QUEUE_SIZE) { + ETHERNET_DEBUG_PRINTLN("writeFrame(), send_queue is full!"); + return 0; + } + + send_queue[send_queue_len].len = len; // add to send queue + memcpy(send_queue[send_queue_len].buf, src, len); + send_queue_len++; + + return len; + } + return 0; +} + +bool CH390EthernetInterface::isWriteBusy() const { + return false; +} + +size_t CH390EthernetInterface::checkRecvFrame(uint8_t dest[]) { + if (server.hasClient()) { + auto newClient = server.available(); + if (newClient) { + IPAddress new_ip = newClient.remoteIP(); + uint16_t new_port = newClient.remotePort(); + ETHERNET_DEBUG_PRINTLN( + "New client accepted %u.%u.%u.%u:%u", + new_ip[0], new_ip[1], new_ip[2], new_ip[3], new_port); + + deviceConnected = false; + if (client) { + ETHERNET_DEBUG_PRINTLN("Closing previous client"); + client.stop(); + } + _state = RECV_STATE_IDLE; + _frame_len = 0; + _rx_len = 0; + client = newClient; + ETHERNET_DEBUG_PRINTLN("Switched to new client"); + } + } + + if (client.connected()) { + if (!deviceConnected) { + ETHERNET_DEBUG_PRINTLN( + "Got connection %u.%u.%u.%u:%u", + client.remoteIP()[0], + client.remoteIP()[1], + client.remoteIP()[2], + client.remoteIP()[3], + client.remotePort()); + deviceConnected = true; + } + } else { + if (deviceConnected) { + deviceConnected = false; + ETHERNET_DEBUG_PRINTLN("Disconnected"); + } + } + + if (deviceConnected) { + if (send_queue_len > 0) { // first, check send queue + + _last_write = millis(); + int len = send_queue[0].len; + +#if ETHERNET_RAW_LINE + ETHERNET_DEBUG_PRINTLN("TX line len=%d", len); + client.write(send_queue[0].buf, len); + client.write("\r\n", 2); +#else + uint8_t pkt[3+len]; // use same header as serial interface so client can delimit frames + pkt[0] = '>'; + pkt[1] = (len & 0xFF); // LSB + pkt[2] = (len >> 8); // MSB + memcpy(&pkt[3], send_queue[0].buf, send_queue[0].len); + ETHERNET_DEBUG_PRINTLN("Sending frame len=%d", len); + #if ETHERNET_DEBUG_LOGGING && ARDUINO + ETHERNET_DEBUG_PRINTLN("TX frame len=%d", len); + #endif + client.write(pkt, 3 + len); +#endif + send_queue_len--; + for (int i = 0; i < send_queue_len; i++) { // delete top item from queue + send_queue[i] = send_queue[i + 1]; + } + } else { + while (client.available()) { + int c = client.read(); + if (c < 0) break; + +#if ETHERNET_RAW_LINE + if (c == '\r' || c == '\n') { + if (_rx_len == 0) { + continue; + } + uint16_t out_len = _rx_len; + if (out_len > MAX_FRAME_SIZE) out_len = MAX_FRAME_SIZE; + memcpy(dest, _rx_buf, out_len); + _rx_len = 0; + return out_len; + } + if (_rx_len < MAX_FRAME_SIZE) { + _rx_buf[_rx_len] = (uint8_t)c; + _rx_len++; + } +#else + switch (_state) { + case RECV_STATE_IDLE: + if (c == '<') { + _state = RECV_STATE_HDR_FOUND; + } + break; + case RECV_STATE_HDR_FOUND: + _frame_len = (uint8_t)c; + _state = RECV_STATE_LEN1_FOUND; + break; + case RECV_STATE_LEN1_FOUND: + _frame_len |= ((uint16_t)c) << 8; + _rx_len = 0; + _state = _frame_len > 0 ? RECV_STATE_LEN2_FOUND : RECV_STATE_IDLE; + break; + default: + if (_rx_len < MAX_FRAME_SIZE) { + _rx_buf[_rx_len] = (uint8_t)c; + } + _rx_len++; + if (_rx_len >= _frame_len) { + if (_frame_len > MAX_FRAME_SIZE) { + _frame_len = MAX_FRAME_SIZE; + } + #if ETHERNET_DEBUG_LOGGING && ARDUINO + ETHERNET_DEBUG_PRINTLN("RX frame len=%d", _frame_len); + #endif + memcpy(dest, _rx_buf, _frame_len); + _state = RECV_STATE_IDLE; + return _frame_len; + } + } +#endif + } + } + } + + return 0; +} + +bool CH390EthernetInterface::isConnected() const { + return deviceConnected; +} + +void CH390EthernetInterface::loop() { + +} diff --git a/src/helpers/ethernet/ch390/CH390EthernetInterface.h b/src/helpers/ethernet/ch390/CH390EthernetInterface.h new file mode 100644 index 00000000..d638319b --- /dev/null +++ b/src/helpers/ethernet/ch390/CH390EthernetInterface.h @@ -0,0 +1,80 @@ +#pragma once + +#include "../../BaseSerialInterface.h" +#include +#include +#include +#include +#include + +#ifndef ETHERNET_TCP_PORT + #define ETHERNET_TCP_PORT 5000 +#endif +// define ETHERNET_RAW_LINE=1 to use raw line-based CLI instead of framed packets + +class CH390EthernetInterface : public BaseSerialInterface { + bool deviceConnected; + bool _isEnabled; + unsigned long _last_write; + uint8_t _state; + uint16_t _frame_len; + uint16_t _rx_len; + uint8_t _rx_buf[MAX_FRAME_SIZE]; + + WiFiServer server; + WiFiClient client; + + struct Frame { + uint8_t len; + uint8_t buf[MAX_FRAME_SIZE]; + }; + + #define FRAME_QUEUE_SIZE 4 + int send_queue_len; + Frame send_queue[FRAME_QUEUE_SIZE]; + + void clearBuffers() { + send_queue_len = 0; + _state = 0; + _frame_len = 0; + _rx_len = 0; + } + + protected: + + public: + CH390EthernetInterface() : server(ETHERNET_TCP_PORT) { + deviceConnected = false; + _isEnabled = false; + _last_write = 0; + send_queue_len = 0; + _state = 0; + _frame_len = 0; + _rx_len = 0; + } + bool begin(); + void loop(); + + // BaseSerialInterface methods + void enable() override; + void disable() override; + bool isEnabled() const override { return _isEnabled; } + + bool isConnected() const override; + bool isWriteBusy() const override; + + size_t writeFrame(const uint8_t src[], size_t len) override; + size_t checkRecvFrame(uint8_t dest[]) override; +}; + + +#if ETHERNET_DEBUG_LOGGING && ARDUINO + #include + #define ETHERNET_DEBUG_PRINT(F, ...) Serial.printf("ETH: " F, ##__VA_ARGS__) + #define ETHERNET_DEBUG_PRINTLN(F, ...) Serial.printf("ETH: " F "\n", ##__VA_ARGS__) + #define ETHERNET_DEBUG_PRINT_IP(name, ip) Serial.printf(name ": %u.%u.%u.%u" "\n", ip[0], ip[1], ip[2], ip[3]) +#else + #define ETHERNET_DEBUG_PRINT(...) {} + #define ETHERNET_DEBUG_PRINTLN(...) {} + #define ETHERNET_DEBUG_PRINT_IP(...) {} +#endif diff --git a/variants/thinknode_m7/platformio.ini b/variants/thinknode_m7/platformio.ini index acd98c8c..43ff61c3 100644 --- a/variants/thinknode_m7/platformio.ini +++ b/variants/thinknode_m7/platformio.ini @@ -34,6 +34,22 @@ build_src_filter = ${esp32_base.build_src_filter} lib_deps = ${esp32_base.lib_deps} stevemarple/MicroNMEA @ ^2.0.6 +[ThinkNode_M7_ethernet] +build_flags = + -D ETHERNET_ENABLED + -D ETHERNET_USE_CH390 + -D ETHERNET_CLASS=CH390EthernetInterface + -D ETH_MISO_PIN=14 + -D ETH_MOSI_PIN=48 + -D ETH_SCLK_PIN=47 + -D ETH_CS_PIN=21 + -D ETH_INT_PIN=45 + -D ETHERNET_DEBUG_LOGGING=1 +build_src_filter = + + +lib_deps = + https://github.com/liamcottle/ESP32-CH390.git#47b401f1de546118b03c18b5689dedec45871f2d + [env:ThinkNode_M7_repeater] extends = ThinkNode_M7 build_src_filter = ${ThinkNode_M7.build_src_filter} @@ -130,6 +146,26 @@ lib_deps = ${ThinkNode_M7.lib_deps} densaugeo/base64 @ ~1.4.0 +[env:ThinkNode_M7_companion_radio_ethernet] +extends = ThinkNode_M7 +build_flags = + ${ThinkNode_M7.build_flags} + ${ThinkNode_M7_ethernet.build_flags} + -I examples/companion_radio/ui-orig + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D DISPLAY_CLASS=NullDisplayDriver + -D OFFLINE_QUEUE_SIZE=256 +build_src_filter = ${ThinkNode_M7.build_src_filter} + ${ThinkNode_M7_ethernet.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-orig/*.cpp> +lib_deps = ${ThinkNode_M7.lib_deps} + ${ThinkNode_M7_ethernet.lib_deps} + densaugeo/base64 @ ~1.4.0 + [env:ThinkNode_M7_kiss_modem] extends = ThinkNode_M7 build_src_filter = ${ThinkNode_M7.build_src_filter} From b4143a4402ebd4f0f5fbcb8bfea7cffbde74f7f7 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Sat, 18 Jul 2026 00:33:35 +1200 Subject: [PATCH 56/70] rename class --- examples/companion_radio/main.cpp | 2 +- src/helpers/ethernet/{Ethernet.h => EthernetInterface.h} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/helpers/ethernet/{Ethernet.h => EthernetInterface.h} (100%) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 2fc00dbd..b6b6d93c 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -49,7 +49,7 @@ static uint32_t _atoi(const char* sp) { ArduinoSerialInterface serial_interface; HardwareSerial companion_serial(1); #elif defined(ETHERNET_ENABLED) - #include + #include ETHERNET_CLASS serial_interface; #else #include diff --git a/src/helpers/ethernet/Ethernet.h b/src/helpers/ethernet/EthernetInterface.h similarity index 100% rename from src/helpers/ethernet/Ethernet.h rename to src/helpers/ethernet/EthernetInterface.h From e672679d6ec5c0c849b60b9c735f386a30c0bade Mon Sep 17 00:00:00 2001 From: liamcottle Date: Sun, 19 Jul 2026 23:28:01 +1200 Subject: [PATCH 57/70] refactored companion interfaces to allow for multiple active connection modes --- examples/companion_radio/AbstractUITask.h | 12 +- examples/companion_radio/main.cpp | 209 ++++++++--------- examples/companion_radio/ui-new/UITask.cpp | 8 +- examples/companion_radio/ui-new/UITask.h | 4 +- examples/companion_radio/ui-orig/UITask.h | 2 +- examples/companion_radio/ui-tiny/UITask.h | 4 +- src/helpers/BaseSerialInterface.h | 1 + src/helpers/MultiSerialInterface.h | 200 ++++++++++++++++ .../ethernet/SerialEthernetInterface.cpp | 140 ++++++++++++ .../ethernet/SerialEthernetInterface.h | 75 ++++++ .../ethernet/ch390/CH390EthernetInterface.cpp | 215 +++++------------- .../ethernet/ch390/CH390EthernetInterface.h | 72 +----- variants/thinknode_m7/platformio.ini | 6 + 13 files changed, 595 insertions(+), 353 deletions(-) create mode 100644 src/helpers/MultiSerialInterface.h create mode 100644 src/helpers/ethernet/SerialEthernetInterface.cpp create mode 100644 src/helpers/ethernet/SerialEthernetInterface.h diff --git a/examples/companion_radio/AbstractUITask.h b/examples/companion_radio/AbstractUITask.h index 0eee45ae..b25b1442 100644 --- a/examples/companion_radio/AbstractUITask.h +++ b/examples/companion_radio/AbstractUITask.h @@ -4,7 +4,7 @@ #include #include #include -#include +#include #include #ifdef PIN_BUZZER @@ -25,10 +25,10 @@ enum class UIEventType { class AbstractUITask { protected: mesh::MainBoard* _board; - BaseSerialInterface* _serial; + MultiSerialInterface* _interfaceManager; bool _connected; - AbstractUITask(mesh::MainBoard* board, BaseSerialInterface* serial) : _board(board), _serial(serial) { + AbstractUITask(mesh::MainBoard* board, MultiSerialInterface* interfaceManager) : _board(board), _interfaceManager(interfaceManager) { _connected = false; } @@ -36,9 +36,9 @@ public: void setHasConnection(bool connected) { _connected = connected; } bool hasConnection() const { return _connected; } uint16_t getBattMilliVolts() const { return _board->getBattMilliVolts(); } - bool isSerialEnabled() const { return _serial->isEnabled(); } - void enableSerial() { _serial->enable(); } - void disableSerial() { _serial->disable(); } + bool isBluetoothEnabled() const { return _interfaceManager->isBluetoothEnabled(); } + void enableBluetooth() { _interfaceManager->enableBluetooth(); } + void disableBluetooth() { _interfaceManager->disableBluetooth(); } virtual void msgRead(int msgcount) = 0; virtual void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) = 0; virtual void notify(UIEventType t = UIEventType::none) = 0; diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index b6b6d93c..7c67d90d 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -12,6 +12,67 @@ static uint32_t _atoi(const char* sp) { return n; } +// interface manager +#include +MultiSerialInterface interface_manager; + +// include bluetooth interface +#if defined(BLE_PIN_CODE) + #ifdef ESP32 + // include esp32 bluetooth interface + #include + SerialBLEInterface bluetooth_interface; + #elif defined(NRF52_PLATFORM) + // include nrf52 bluetooth interface + #include + SerialBLEInterface bluetooth_interface; + #else + #error "SerialBLEInterface is not defined for this platform" + #endif +#endif + +// include wifi interface +#ifdef WIFI_SSID + #ifndef TCP_PORT + #define TCP_PORT 5000 + #endif + #ifdef ESP32 + // include esp32 wifi interface + #include + SerialWifiInterface wifi_interface; + #else + #error "SerialWifiInterface is not defined for this platform" + #endif +#endif + +// include usb interface +#if defined(ENABLE_USB_INTERFACE) + #include + ArduinoSerialInterface usb_serial_interface; +#endif + +// include ethernet interface +#if defined(ETHERNET_ENABLED) + // todo refactor rak/nrf52 SerialEthernetInterface to new EthernetInterface + #if defined(NRF52_PLATFORM) + // include nrf52 ethernet interface + #include + SerialEthernetInterface ethernet_interface; + #else + // include ethernet interface + #include + ETHERNET_CLASS ethernet_interface; + #endif +#endif + +// include hardware serial interface +#if defined(SERIAL_RX) + #include + ArduinoSerialInterface hardware_serial_interface; + HardwareSerial companion_serial(1); +#endif + +// platform file system #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) #include #if defined(QSPIFLASH) @@ -34,67 +95,10 @@ static uint32_t _atoi(const char* sp) { DataStore store(SPIFFS, rtc_clock); #endif -#ifdef ESP32 - #ifdef WIFI_SSID - #include - SerialWifiInterface serial_interface; - #ifndef TCP_PORT - #define TCP_PORT 5000 - #endif - #elif defined(BLE_PIN_CODE) - #include - SerialBLEInterface serial_interface; - #elif defined(SERIAL_RX) - #include - ArduinoSerialInterface serial_interface; - HardwareSerial companion_serial(1); - #elif defined(ETHERNET_ENABLED) - #include - ETHERNET_CLASS serial_interface; - #else - #include - ArduinoSerialInterface serial_interface; - #endif -#elif defined(RP2040_PLATFORM) - //#ifdef WIFI_SSID - // #include - // SerialWifiInterface serial_interface; - // #ifndef TCP_PORT - // #define TCP_PORT 5000 - // #endif - // #elif defined(BLE_PIN_CODE) - // #include - // SerialBLEInterface serial_interface; - #if defined(SERIAL_RX) - #include - ArduinoSerialInterface serial_interface; - HardwareSerial companion_serial(1); - #else - #include - ArduinoSerialInterface serial_interface; - #endif -#elif defined(NRF52_PLATFORM) - #ifdef BLE_PIN_CODE - #include - SerialBLEInterface serial_interface; - #elif defined(ETHERNET_ENABLED) - #include - SerialEthernetInterface serial_interface; - #else - #include - ArduinoSerialInterface serial_interface; - #endif -#elif defined(STM32_PLATFORM) - #include - ArduinoSerialInterface serial_interface; -#else - #error "need to define a serial interface" -#endif - /* GLOBAL OBJECTS */ #ifdef DISPLAY_CLASS #include "UITask.h" - UITask ui_task(&board, &serial_interface); + UITask ui_task(&board, &interface_manager); #endif StdRNG fast_rng; @@ -164,26 +168,6 @@ void setup() { false #endif ); - -#ifdef BLE_PIN_CODE - serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin()); - the_mesh.startInterface(serial_interface); -#elif defined(ETHERNET_ENABLED) - Serial.print("Waiting for serial to connect...\n"); - unsigned long timeout = millis(); - while (!Serial) { - if ((millis() - timeout) < 5000) { delay(100); } else { break; } - } - Serial.println("Initializing Ethernet adapter..."); - if (serial_interface.begin()) { - the_mesh.startInterface(serial_interface); - } else { - Serial.println("ETH: Init failed, continuing without Ethernet (mesh only)"); - } -#else - serial_interface.begin(Serial); - the_mesh.startInterface(serial_interface); -#endif #elif defined(RP2040_PLATFORM) LittleFS.begin(); store.begin(); @@ -194,22 +178,6 @@ void setup() { false #endif ); - - //#ifdef WIFI_SSID - // WiFi.begin(WIFI_SSID, WIFI_PWD); - // serial_interface.begin(TCP_PORT); - // #elif defined(BLE_PIN_CODE) - // char dev_name[32+16]; - // sprintf(dev_name, "%s%s", BLE_NAME_PREFIX, the_mesh.getNodeName()); - // serial_interface.begin(dev_name, the_mesh.getBLEPin()); - #if defined(SERIAL_RX) - companion_serial.setPins(SERIAL_RX, SERIAL_TX); - companion_serial.begin(115200); - serial_interface.begin(companion_serial); - #else - serial_interface.begin(Serial); - #endif - the_mesh.startInterface(serial_interface); #elif defined(ESP32) SPIFFS.begin(true); store.begin(); @@ -220,7 +188,17 @@ void setup() { false #endif ); +#else + #error "need to define filesystem" +#endif +// add bluetooth interface +#if defined(BLE_PIN_CODE) + bluetooth_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin()); + interface_manager.addInterface(InterfaceType::Bluetooth, &bluetooth_interface); +#endif + +// add wifi interface #ifdef WIFI_SSID board.setInhibitSleep(true); // prevent sleep when WiFi is active WiFi.setAutoReconnect(true); @@ -236,23 +214,31 @@ void setup() { }); WiFi.begin(WIFI_SSID, WIFI_PWD); - serial_interface.begin(TCP_PORT); -#elif defined(BLE_PIN_CODE) - serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin()); -#elif defined(SERIAL_RX) - companion_serial.setPins(SERIAL_RX, SERIAL_TX); - companion_serial.begin(115200); - serial_interface.begin(companion_serial); -#elif defined(ETHERNET_ENABLED) - serial_interface.begin(); -#else - serial_interface.begin(Serial); -#endif - the_mesh.startInterface(serial_interface); -#else - #error "need to define filesystem" + wifi_interface.begin(TCP_PORT); + interface_manager.addInterface(InterfaceType::WiFi, &wifi_interface); #endif +// add usb interface +#if defined(ENABLE_USB_INTERFACE) + usb_serial_interface.begin(Serial); + interface_manager.addInterface(InterfaceType::USB, &usb_serial_interface); +#endif + +// add ethernet interface +#if defined(ETHERNET_ENABLED) + ethernet_interface.begin(); + interface_manager.addInterface(InterfaceType::Ethernet, ðernet_interface); +#endif + +// add hardware serial interface +#if defined(SERIAL_RX) + companion_serial.setPins(SERIAL_RX, SERIAL_TX); + companion_serial.begin(115200); + hardware_serial_interface.begin(companion_serial); + interface_manager.addInterface(InterfaceType::HardwareSerial, &hardware_serial_interface); +#endif + + the_mesh.startInterface(interface_manager); sensors.begin(); #if ENV_INCLUDE_GPS == 1 @@ -268,6 +254,7 @@ void setup() { void loop() { the_mesh.loop(); + interface_manager.loop(); sensors.loop(); #ifdef DISPLAY_CLASS ui_task.loop(); @@ -277,10 +264,6 @@ void loop() { external_watchdog.loop(); #endif -#ifdef ETHERNET_ENABLED - serial_interface.loop(); -#endif - if (!the_mesh.hasPendingWork()) { #if defined(NRF52_PLATFORM) board.sleep(0); // nrf ignores seconds param, sleeps whenever possible diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 50dc91d2..051f3b31 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -289,7 +289,7 @@ public: } else if (_page == HomePage::BLUETOOTH) { display.setColor(UIColor::corp_blue); display.drawXbm((display.width() - 32) / 2, 18, - _task->isSerialEnabled() ? bluetooth_on : bluetooth_off, + _task->isBluetoothEnabled() ? bluetooth_on : bluetooth_off, 32, 32); display.setColor(UIColor::secondary_txt); display.setTextSize(1); @@ -449,10 +449,10 @@ public: return true; } if (c == KEY_ENTER && _page == HomePage::BLUETOOTH) { - if (_task->isSerialEnabled()) { // toggle Bluetooth on/off - _task->disableSerial(); + if (_task->isBluetoothEnabled()) { // toggle Bluetooth on/off + _task->disableBluetooth(); } else { - _task->enableSerial(); + _task->enableBluetooth(); } return true; } diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index a77ad6e7..52d3ffa1 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -4,7 +4,7 @@ #include #include #include -#include +#include #include #include @@ -65,7 +65,7 @@ class UITask : public AbstractUITask { public: - UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) { + UITask(mesh::MainBoard* board, MultiSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) { next_batt_chck = _next_refresh = 0; ui_started_at = 0; curr = NULL; diff --git a/examples/companion_radio/ui-orig/UITask.h b/examples/companion_radio/ui-orig/UITask.h index 60cd0d04..961c07a0 100644 --- a/examples/companion_radio/ui-orig/UITask.h +++ b/examples/companion_radio/ui-orig/UITask.h @@ -54,7 +54,7 @@ class UITask : public AbstractUITask { public: - UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) { + UITask(mesh::MainBoard* board, MultiSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) { _next_refresh = 0; ui_started_at = 0; } diff --git a/examples/companion_radio/ui-tiny/UITask.h b/examples/companion_radio/ui-tiny/UITask.h index 344e48b9..dc689478 100644 --- a/examples/companion_radio/ui-tiny/UITask.h +++ b/examples/companion_radio/ui-tiny/UITask.h @@ -4,7 +4,7 @@ #include #include #include -#include +#include #include #include @@ -71,7 +71,7 @@ class UITask : public AbstractUITask { public: - UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) { + UITask(mesh::MainBoard* board, MultiSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) { next_batt_chck = _next_refresh = 0; _cached_batt_mv = 0; ui_started_at = 0; diff --git a/src/helpers/BaseSerialInterface.h b/src/helpers/BaseSerialInterface.h index e9a3f2ab..23933fcb 100644 --- a/src/helpers/BaseSerialInterface.h +++ b/src/helpers/BaseSerialInterface.h @@ -14,6 +14,7 @@ public: virtual bool isEnabled() const = 0; virtual bool isConnected() const = 0; + virtual void loop() {}; virtual bool isWriteBusy() const = 0; virtual size_t writeFrame(const uint8_t src[], size_t len) = 0; diff --git a/src/helpers/MultiSerialInterface.h b/src/helpers/MultiSerialInterface.h new file mode 100644 index 00000000..f7742b24 --- /dev/null +++ b/src/helpers/MultiSerialInterface.h @@ -0,0 +1,200 @@ +#pragma once + +#include "BaseSerialInterface.h" + +#ifndef MAX_INTERFACES + // ble, usb, wifi, ethernet + #define MAX_INTERFACES 4 +#endif + +enum class InterfaceType : uint8_t { + NONE, + Bluetooth, + USB, + WiFi, + Ethernet, + HardwareSerial +}; + +class MultiSerialInterface : public BaseSerialInterface { +private: + + struct RegisteredInterface { + InterfaceType type = InterfaceType::NONE; + BaseSerialInterface* instance = nullptr; + }; + + bool _enabled = false; + RegisteredInterface _interfaces[MAX_INTERFACES] = {}; + +public: + bool addInterface(InterfaceType type, BaseSerialInterface* iface) { + // make sure an interface was provided + if(iface == nullptr){ + return false; + } + + // put it in the first free slot + for(int i = 0; i < MAX_INTERFACES; i++){ + if(_interfaces[i].instance == nullptr){ + _interfaces[i].instance = iface; + _interfaces[i].type = type; + return true; + } + } + + // no free slots available + return false; + } + + bool removeInterface(BaseSerialInterface* iface) { + // make sure an interface was provided + if(iface == nullptr){ + return false; + } + + // find and remove interface + for(int i = 0; i < MAX_INTERFACES; i++){ + if(_interfaces[i].instance == iface){ + _interfaces[i] = {}; + return true; + } + } + + // interface not found + return false; + } + + void enableBluetooth() { + for(auto iface : _interfaces){ + if(iface.instance && iface.type == InterfaceType::Bluetooth){ + iface.instance->enable(); + } + } + } + + void disableBluetooth() { + for(auto iface : _interfaces){ + if(iface.instance && iface.type == InterfaceType::Bluetooth){ + iface.instance->disable(); + } + } + } + + bool isBluetoothEnabled() { + for(auto iface : _interfaces){ + if(iface.instance && iface.type == InterfaceType::Bluetooth){ + return iface.instance->isEnabled(); + } + } + return false; + } + + // enable all interfaces + void enable() override { + _enabled = true; + for(auto iface : _interfaces){ + if(iface.instance){ + iface.instance->enable(); + } + } + } + + // disable all interfaces + void disable() override { + _enabled = false; + for(auto iface : _interfaces){ + if(iface.instance){ + iface.instance->disable(); + } + } + } + + bool isEnabled() const override { + return _enabled; + } + + bool isConnected() const override { + // not connected when disabled + if(!_enabled){ + return false; + } + + // check if any interface is connected + for(auto iface : _interfaces){ + if(iface.instance && iface.instance->isConnected()) { + return true; + } + } + + // nothing connected + return false; + } + + // loop all interfaces + void loop() override { + for(auto iface : _interfaces){ + if(iface.instance){ + iface.instance->loop(); + } + } + } + + bool isWriteBusy() const override { + // not busy when disabled + if(!_enabled){ + return false; + } + + // check if any interface is busy + for(auto iface : _interfaces){ + if(iface.instance && iface.instance->isEnabled() && iface.instance->isWriteBusy()){ + return true; + } + } + + // nothing busy + return false; + } + + size_t writeFrame(const uint8_t src[], size_t len) override { + // don't write when disabled or nothing provided + if(!_enabled || len == 0){ + return 0; + } + + // write frame to all enabled interfaces + bool allSuccessful = true; + for(auto iface : _interfaces){ + if(iface.instance && iface.instance->isEnabled()){ + if(iface.instance->writeFrame(src, len) != len){ + allSuccessful = false; + } + } + } + + // report success if all writes completed successfully + return allSuccessful ? len : 0; + } + + size_t checkRecvFrame(uint8_t dest[]) override { + // don't read when disabled + if(!_enabled){ + return 0; + } + + // try to read a frame from any enabled interface + for(auto iface : _interfaces){ + if(iface.instance && iface.instance->isEnabled()){ + size_t frameSize = iface.instance->checkRecvFrame(dest); + if(frameSize > 0){ + return frameSize; + } + } + } + + // no frame received + return 0; + } + +}; diff --git a/src/helpers/ethernet/SerialEthernetInterface.cpp b/src/helpers/ethernet/SerialEthernetInterface.cpp new file mode 100644 index 00000000..22bcabc1 --- /dev/null +++ b/src/helpers/ethernet/SerialEthernetInterface.cpp @@ -0,0 +1,140 @@ +#include "SerialEthernetInterface.h" + +#define RECV_STATE_IDLE 0 +#define RECV_STATE_HDR_FOUND 1 +#define RECV_STATE_LEN1_FOUND 2 +#define RECV_STATE_LEN2_FOUND 3 + +bool SerialEthernetInterface::begin() { + return true; +} + +void SerialEthernetInterface::enable() { + if (_isEnabled) return; + _isEnabled = true; + clearBuffers(); +} + +void SerialEthernetInterface::disable() { + _isEnabled = false; +} + +size_t SerialEthernetInterface::writeFrame(const uint8_t src[], size_t len) { + if (len > MAX_FRAME_SIZE) { + ETHERNET_DEBUG_PRINTLN("writeFrame(), frame too big, len=%d\n", len); + return 0; + } + + if (isConnected() && len > 0) { + if (send_queue_len >= FRAME_QUEUE_SIZE) { + ETHERNET_DEBUG_PRINTLN("writeFrame(), send_queue is full!"); + return 0; + } + + send_queue[send_queue_len].len = len; // add to send queue + memcpy(send_queue[send_queue_len].buf, src, len); + send_queue_len++; + + return len; + } + return 0; +} + +bool SerialEthernetInterface::isWriteBusy() const { + return false; +} + +void SerialEthernetInterface::onClientConnected() { + _state = RECV_STATE_IDLE; + _frame_len = 0; + _rx_len = 0; +} + +size_t SerialEthernetInterface::checkRecvFrame(uint8_t dest[]) { + + if (isConnected()) { + if (send_queue_len > 0) { // first, check send queue + + _last_write = millis(); + int len = send_queue[0].len; + +#if ETHERNET_RAW_LINE + ETHERNET_DEBUG_PRINTLN("TX line len=%d", len); + client.write(send_queue[0].buf, len); + client.write("\r\n", 2); +#else + uint8_t pkt[3+len]; // use same header as serial interface so client can delimit frames + pkt[0] = '>'; + pkt[1] = (len & 0xFF); // LSB + pkt[2] = (len >> 8); // MSB + memcpy(&pkt[3], send_queue[0].buf, send_queue[0].len); + ETHERNET_DEBUG_PRINTLN("Sending frame len=%d", len); + #if ETHERNET_DEBUG_LOGGING && ARDUINO + ETHERNET_DEBUG_PRINTLN("TX frame len=%d", len); + #endif + write(pkt, 3 + len); +#endif + send_queue_len--; + for (int i = 0; i < send_queue_len; i++) { // delete top item from queue + send_queue[i] = send_queue[i + 1]; + } + } else { + while (available()) { + int c = read(); + if (c < 0) break; + +#if ETHERNET_RAW_LINE + if (c == '\r' || c == '\n') { + if (_rx_len == 0) { + continue; + } + uint16_t out_len = _rx_len; + if (out_len > MAX_FRAME_SIZE) out_len = MAX_FRAME_SIZE; + memcpy(dest, _rx_buf, out_len); + _rx_len = 0; + return out_len; + } + if (_rx_len < MAX_FRAME_SIZE) { + _rx_buf[_rx_len] = (uint8_t)c; + _rx_len++; + } +#else + switch (_state) { + case RECV_STATE_IDLE: + if (c == '<') { + _state = RECV_STATE_HDR_FOUND; + } + break; + case RECV_STATE_HDR_FOUND: + _frame_len = (uint8_t)c; + _state = RECV_STATE_LEN1_FOUND; + break; + case RECV_STATE_LEN1_FOUND: + _frame_len |= ((uint16_t)c) << 8; + _rx_len = 0; + _state = _frame_len > 0 ? RECV_STATE_LEN2_FOUND : RECV_STATE_IDLE; + break; + default: + if (_rx_len < MAX_FRAME_SIZE) { + _rx_buf[_rx_len] = (uint8_t)c; + } + _rx_len++; + if (_rx_len >= _frame_len) { + if (_frame_len > MAX_FRAME_SIZE) { + _frame_len = MAX_FRAME_SIZE; + } + #if ETHERNET_DEBUG_LOGGING && ARDUINO + ETHERNET_DEBUG_PRINTLN("RX frame len=%d", _frame_len); + #endif + memcpy(dest, _rx_buf, _frame_len); + _state = RECV_STATE_IDLE; + return _frame_len; + } + } +#endif + } + } + } + + return 0; +} diff --git a/src/helpers/ethernet/SerialEthernetInterface.h b/src/helpers/ethernet/SerialEthernetInterface.h new file mode 100644 index 00000000..46789f71 --- /dev/null +++ b/src/helpers/ethernet/SerialEthernetInterface.h @@ -0,0 +1,75 @@ +#pragma once + +#include "../BaseSerialInterface.h" + +#ifndef ETHERNET_TCP_PORT + #define ETHERNET_TCP_PORT 5000 +#endif +// define ETHERNET_RAW_LINE=1 to use raw line-based CLI instead of framed packets + +class SerialEthernetInterface : public BaseSerialInterface { + bool _isEnabled; + unsigned long _last_write; + uint8_t _state; + uint16_t _frame_len; + uint16_t _rx_len; + uint8_t _rx_buf[MAX_FRAME_SIZE]; + + struct Frame { + uint8_t len; + uint8_t buf[MAX_FRAME_SIZE]; + }; + + #define FRAME_QUEUE_SIZE 4 + int send_queue_len; + Frame send_queue[FRAME_QUEUE_SIZE]; + + void clearBuffers() { + send_queue_len = 0; + _state = 0; + _frame_len = 0; + _rx_len = 0; + } + + protected: + + public: + SerialEthernetInterface() { + _isEnabled = false; + _last_write = 0; + send_queue_len = 0; + _state = 0; + _frame_len = 0; + _rx_len = 0; + } + bool begin(); + + void onClientConnected(); + + // BaseSerialInterface methods + void enable() override; + void disable() override; + bool isEnabled() const override { return _isEnabled; } + + bool isConnected() const override; + bool isWriteBusy() const override; + + size_t writeFrame(const uint8_t src[], size_t len) override; + size_t checkRecvFrame(uint8_t dest[]) override; + + virtual int available() = 0; + virtual int read() = 0; + virtual size_t write(const uint8_t *buf, size_t size) = 0; +}; + + +#if ETHERNET_DEBUG_LOGGING && ARDUINO + #include + #define ETHERNET_DEBUG_PRINT(F, ...) Serial.printf("ETH: " F, ##__VA_ARGS__) + #define ETHERNET_DEBUG_PRINTLN(F, ...) Serial.printf("ETH: " F "\n", ##__VA_ARGS__) + #define ETHERNET_DEBUG_PRINT_IP(name, ip) Serial.printf("ETH: " name ": %u.%u.%u.%u" "\n", ip[0], ip[1], ip[2], ip[3]) +#else + #define ETHERNET_DEBUG_PRINT(...) {} + #define ETHERNET_DEBUG_PRINTLN(...) {} + #define ETHERNET_DEBUG_PRINT_IP(...) {} +#endif diff --git a/src/helpers/ethernet/ch390/CH390EthernetInterface.cpp b/src/helpers/ethernet/ch390/CH390EthernetInterface.cpp index f7ee5503..7f696245 100644 --- a/src/helpers/ethernet/ch390/CH390EthernetInterface.cpp +++ b/src/helpers/ethernet/ch390/CH390EthernetInterface.cpp @@ -1,12 +1,33 @@ #include "CH390EthernetInterface.h" -#define RECV_STATE_IDLE 0 -#define RECV_STATE_HDR_FOUND 1 -#define RECV_STATE_LEN1_FOUND 2 -#define RECV_STATE_LEN2_FOUND 3 +void onWiFiEvent(WiFiEvent_t event) { + switch(event){ + case ARDUINO_EVENT_ETH_START: + ETHERNET_DEBUG_PRINTLN("Ethernet Started"); + break; + case ARDUINO_EVENT_ETH_CONNECTED: + ETHERNET_DEBUG_PRINTLN("Ethernet Connected"); + break; + case ARDUINO_EVENT_ETH_DISCONNECTED: + ETHERNET_DEBUG_PRINTLN("Ethernet Disconnected"); + break; + case ARDUINO_EVENT_ETH_GOT_IP: + ETHERNET_DEBUG_PRINTLN("Ethernet Got IP"); + ETHERNET_DEBUG_PRINT_IP("IP Address", CH390.localIP()); + ETHERNET_DEBUG_PRINT_IP("Subnet Mask", CH390.subnetMask()); + ETHERNET_DEBUG_PRINT_IP("Gateway", CH390.gatewayIP()); + ETHERNET_DEBUG_PRINT_IP("DNS", CH390.dnsIP()); + ETHERNET_DEBUG_PRINTLN("MAC Address: %s", CH390.macAddress().c_str()); + break; + default: + break; + } +} bool CH390EthernetInterface::begin() { - ETHERNET_DEBUG_PRINTLN("Ethernet initializing"); + + // listen to ethernet events + WiFi.onEvent(onWiFiEvent); // Init CH390 ch390_config_t config = CH390_DEFAULT_CONFIG(); @@ -29,179 +50,45 @@ bool CH390EthernetInterface::begin() { #endif // Start Server - server.begin(); + server.begin(ETHERNET_TCP_PORT); ETHERNET_DEBUG_PRINTLN("listening on TCP port: %d", ETHERNET_TCP_PORT); return true; } -void CH390EthernetInterface::enable() { - if (_isEnabled) return; - _isEnabled = true; - clearBuffers(); +int CH390EthernetInterface::available() { + return client.available(); } -void CH390EthernetInterface::disable() { - _isEnabled = false; +int CH390EthernetInterface::read() { + return client.read(); } -size_t CH390EthernetInterface::writeFrame(const uint8_t src[], size_t len) { - if (len > MAX_FRAME_SIZE) { - ETHERNET_DEBUG_PRINTLN("writeFrame(), frame too big, len=%d\n", len); - return 0; - } - - if (deviceConnected && len > 0) { - if (send_queue_len >= FRAME_QUEUE_SIZE) { - ETHERNET_DEBUG_PRINTLN("writeFrame(), send_queue is full!"); - return 0; - } - - send_queue[send_queue_len].len = len; // add to send queue - memcpy(send_queue[send_queue_len].buf, src, len); - send_queue_len++; - - return len; - } - return 0; -} - -bool CH390EthernetInterface::isWriteBusy() const { - return false; -} - -size_t CH390EthernetInterface::checkRecvFrame(uint8_t dest[]) { - if (server.hasClient()) { - auto newClient = server.available(); - if (newClient) { - IPAddress new_ip = newClient.remoteIP(); - uint16_t new_port = newClient.remotePort(); - ETHERNET_DEBUG_PRINTLN( - "New client accepted %u.%u.%u.%u:%u", - new_ip[0], new_ip[1], new_ip[2], new_ip[3], new_port); - - deviceConnected = false; - if (client) { - ETHERNET_DEBUG_PRINTLN("Closing previous client"); - client.stop(); - } - _state = RECV_STATE_IDLE; - _frame_len = 0; - _rx_len = 0; - client = newClient; - ETHERNET_DEBUG_PRINTLN("Switched to new client"); - } - } - - if (client.connected()) { - if (!deviceConnected) { - ETHERNET_DEBUG_PRINTLN( - "Got connection %u.%u.%u.%u:%u", - client.remoteIP()[0], - client.remoteIP()[1], - client.remoteIP()[2], - client.remoteIP()[3], - client.remotePort()); - deviceConnected = true; - } - } else { - if (deviceConnected) { - deviceConnected = false; - ETHERNET_DEBUG_PRINTLN("Disconnected"); - } - } - - if (deviceConnected) { - if (send_queue_len > 0) { // first, check send queue - - _last_write = millis(); - int len = send_queue[0].len; - -#if ETHERNET_RAW_LINE - ETHERNET_DEBUG_PRINTLN("TX line len=%d", len); - client.write(send_queue[0].buf, len); - client.write("\r\n", 2); -#else - uint8_t pkt[3+len]; // use same header as serial interface so client can delimit frames - pkt[0] = '>'; - pkt[1] = (len & 0xFF); // LSB - pkt[2] = (len >> 8); // MSB - memcpy(&pkt[3], send_queue[0].buf, send_queue[0].len); - ETHERNET_DEBUG_PRINTLN("Sending frame len=%d", len); - #if ETHERNET_DEBUG_LOGGING && ARDUINO - ETHERNET_DEBUG_PRINTLN("TX frame len=%d", len); - #endif - client.write(pkt, 3 + len); -#endif - send_queue_len--; - for (int i = 0; i < send_queue_len; i++) { // delete top item from queue - send_queue[i] = send_queue[i + 1]; - } - } else { - while (client.available()) { - int c = client.read(); - if (c < 0) break; - -#if ETHERNET_RAW_LINE - if (c == '\r' || c == '\n') { - if (_rx_len == 0) { - continue; - } - uint16_t out_len = _rx_len; - if (out_len > MAX_FRAME_SIZE) out_len = MAX_FRAME_SIZE; - memcpy(dest, _rx_buf, out_len); - _rx_len = 0; - return out_len; - } - if (_rx_len < MAX_FRAME_SIZE) { - _rx_buf[_rx_len] = (uint8_t)c; - _rx_len++; - } -#else - switch (_state) { - case RECV_STATE_IDLE: - if (c == '<') { - _state = RECV_STATE_HDR_FOUND; - } - break; - case RECV_STATE_HDR_FOUND: - _frame_len = (uint8_t)c; - _state = RECV_STATE_LEN1_FOUND; - break; - case RECV_STATE_LEN1_FOUND: - _frame_len |= ((uint16_t)c) << 8; - _rx_len = 0; - _state = _frame_len > 0 ? RECV_STATE_LEN2_FOUND : RECV_STATE_IDLE; - break; - default: - if (_rx_len < MAX_FRAME_SIZE) { - _rx_buf[_rx_len] = (uint8_t)c; - } - _rx_len++; - if (_rx_len >= _frame_len) { - if (_frame_len > MAX_FRAME_SIZE) { - _frame_len = MAX_FRAME_SIZE; - } - #if ETHERNET_DEBUG_LOGGING && ARDUINO - ETHERNET_DEBUG_PRINTLN("RX frame len=%d", _frame_len); - #endif - memcpy(dest, _rx_buf, _frame_len); - _state = RECV_STATE_IDLE; - return _frame_len; - } - } -#endif - } - } - } - - return 0; +size_t CH390EthernetInterface::write(const uint8_t *buf, size_t size) { + return client.write(buf, size); } bool CH390EthernetInterface::isConnected() const { - return deviceConnected; + return _isConnected; } void CH390EthernetInterface::loop() { + if (server.hasClient()) { + auto newClient = server.available(); + if (newClient) { + IPAddress remoteIp = newClient.remoteIP(); + uint16_t remotePort = newClient.remotePort(); + ETHERNET_DEBUG_PRINTLN("New client accepted %u.%u.%u.%u:%u", remoteIp[0], remoteIp[1], remoteIp[2], remoteIp[3], remotePort); + if (client) { + ETHERNET_DEBUG_PRINTLN("Closing previous client"); + client.stop(); + } + client = newClient; + onClientConnected(); + } + } + + _isConnected = client.connected(); + } diff --git a/src/helpers/ethernet/ch390/CH390EthernetInterface.h b/src/helpers/ethernet/ch390/CH390EthernetInterface.h index d638319b..09c3fc23 100644 --- a/src/helpers/ethernet/ch390/CH390EthernetInterface.h +++ b/src/helpers/ethernet/ch390/CH390EthernetInterface.h @@ -1,80 +1,30 @@ #pragma once -#include "../../BaseSerialInterface.h" +#include "../SerialEthernetInterface.h" #include #include #include #include #include -#ifndef ETHERNET_TCP_PORT - #define ETHERNET_TCP_PORT 5000 -#endif -// define ETHERNET_RAW_LINE=1 to use raw line-based CLI instead of framed packets - -class CH390EthernetInterface : public BaseSerialInterface { - bool deviceConnected; - bool _isEnabled; - unsigned long _last_write; - uint8_t _state; - uint16_t _frame_len; - uint16_t _rx_len; - uint8_t _rx_buf[MAX_FRAME_SIZE]; - +class CH390EthernetInterface : public SerialEthernetInterface { + + bool _isConnected; WiFiServer server; WiFiClient client; - struct Frame { - uint8_t len; - uint8_t buf[MAX_FRAME_SIZE]; - }; - - #define FRAME_QUEUE_SIZE 4 - int send_queue_len; - Frame send_queue[FRAME_QUEUE_SIZE]; - - void clearBuffers() { - send_queue_len = 0; - _state = 0; - _frame_len = 0; - _rx_len = 0; - } - - protected: - public: - CH390EthernetInterface() : server(ETHERNET_TCP_PORT) { - deviceConnected = false; - _isEnabled = false; - _last_write = 0; - send_queue_len = 0; - _state = 0; - _frame_len = 0; - _rx_len = 0; + CH390EthernetInterface(){ + _isConnected = false; } + bool begin(); - void loop(); + void loop() override; // BaseSerialInterface methods - void enable() override; - void disable() override; - bool isEnabled() const override { return _isEnabled; } - bool isConnected() const override; - bool isWriteBusy() const override; - size_t writeFrame(const uint8_t src[], size_t len) override; - size_t checkRecvFrame(uint8_t dest[]) override; + int available() override; + int read() override; + size_t write(const uint8_t *buf, size_t size) override; }; - - -#if ETHERNET_DEBUG_LOGGING && ARDUINO - #include - #define ETHERNET_DEBUG_PRINT(F, ...) Serial.printf("ETH: " F, ##__VA_ARGS__) - #define ETHERNET_DEBUG_PRINTLN(F, ...) Serial.printf("ETH: " F "\n", ##__VA_ARGS__) - #define ETHERNET_DEBUG_PRINT_IP(name, ip) Serial.printf(name ": %u.%u.%u.%u" "\n", ip[0], ip[1], ip[2], ip[3]) -#else - #define ETHERNET_DEBUG_PRINT(...) {} - #define ETHERNET_DEBUG_PRINTLN(...) {} - #define ETHERNET_DEBUG_PRINT_IP(...) {} -#endif diff --git a/variants/thinknode_m7/platformio.ini b/variants/thinknode_m7/platformio.ini index 43ff61c3..2ff6fc0a 100644 --- a/variants/thinknode_m7/platformio.ini +++ b/variants/thinknode_m7/platformio.ini @@ -1,6 +1,8 @@ [ThinkNode_M7] extends = esp32_base board = thinknode_m7 +board_upload.flash_size = 8MB +board_build.partitions = default_8MB.csv build_flags = ${esp32_base.build_flags} -I src/helpers/esp32 -I variants/thinknode_m7 @@ -46,6 +48,7 @@ build_flags = -D ETH_INT_PIN=45 -D ETHERNET_DEBUG_LOGGING=1 build_src_filter = + + + lib_deps = https://github.com/liamcottle/ESP32-CH390.git#47b401f1de546118b03c18b5689dedec45871f2d @@ -88,6 +91,7 @@ lib_deps = extends = ThinkNode_M7 build_flags = ${ThinkNode_M7.build_flags} + ${ThinkNode_M7_ethernet.build_flags} -I examples/companion_radio/ui-orig -D DISPLAY_CLASS=NullDisplayDriver -D MAX_CONTACTS=350 @@ -97,6 +101,7 @@ build_flags = ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 build_src_filter = ${ThinkNode_M7.build_src_filter} + ${ThinkNode_M7_ethernet.build_src_filter} + + + @@ -104,6 +109,7 @@ build_src_filter = ${ThinkNode_M7.build_src_filter} +<../examples/companion_radio/ui-orig/*.cpp> lib_deps = ${ThinkNode_M7.lib_deps} + ${ThinkNode_M7_ethernet.lib_deps} densaugeo/base64 @ ~1.4.0 [env:ThinkNode_M7_companion_radio_usb] From 203fd4e407ef17f22518734bc35ffacbab43f64d Mon Sep 17 00:00:00 2001 From: liamcottle Date: Tue, 28 Jul 2026 03:05:49 +1200 Subject: [PATCH 58/70] refactor rak13800 to new ethernet interface class --- examples/companion_radio/main.cpp | 12 +- src/helpers/ethernet/EthernetInterface.h | 2 + .../RAK13800/RAK13800EthernetInterface.cpp | 109 ++++++++++++++++++ .../RAK13800/RAK13800EthernetInterface.h | 27 +++++ variants/rak4631/platformio.ini | 5 +- 5 files changed, 144 insertions(+), 11 deletions(-) create mode 100644 src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.cpp create mode 100644 src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.h diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 7c67d90d..89f0e6cb 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -53,16 +53,8 @@ MultiSerialInterface interface_manager; // include ethernet interface #if defined(ETHERNET_ENABLED) - // todo refactor rak/nrf52 SerialEthernetInterface to new EthernetInterface - #if defined(NRF52_PLATFORM) - // include nrf52 ethernet interface - #include - SerialEthernetInterface ethernet_interface; - #else - // include ethernet interface - #include - ETHERNET_CLASS ethernet_interface; - #endif + #include + ETHERNET_CLASS ethernet_interface; #endif // include hardware serial interface diff --git a/src/helpers/ethernet/EthernetInterface.h b/src/helpers/ethernet/EthernetInterface.h index 4d889df4..4e1f9176 100644 --- a/src/helpers/ethernet/EthernetInterface.h +++ b/src/helpers/ethernet/EthernetInterface.h @@ -3,6 +3,8 @@ #if defined(ETHERNET_ENABLED) #if defined(ETHERNET_USE_CH390) #include "helpers/ethernet/ch390/CH390EthernetInterface.h" + #elif defined(ETHERNET_USE_RAK13800) + #include "helpers/ethernet/RAK13800/RAK13800EthernetInterface.h" #else #error "ETHERNET_ENABLED is defined, but no specific driver flag (e.g. ETHERNET_USE_CH390) was provided!" #endif diff --git a/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.cpp b/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.cpp new file mode 100644 index 00000000..027581e3 --- /dev/null +++ b/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.cpp @@ -0,0 +1,109 @@ +#include "RAK13800EthernetInterface.h" +#include "../../nrf52/EthernetMac.h" +#include +#include + +#define PIN_SPI1_MISO (29) // (0 + 29) +#define PIN_SPI1_MOSI (30) // (0 + 30) +#define PIN_SPI1_SCK (3) // (0 + 3) + +SPIClass ETHERNET_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI); + +#define PIN_ETHERNET_POWER_EN WB_IO2 // output, high to enable +#define PIN_ETHERNET_RESET 21 +#define PIN_ETHERNET_SS 26 + +bool RAK13800EthernetInterface::begin() { + + // WB_IO2 (power enable) is already driven HIGH by early constructor + // in RAK4631Board.cpp to support POE boot. + // Skip hardware reset — the W5100S comes out of power-on reset cleanly, + // and toggling reset kills the PHY link which breaks POE power. +#ifdef PIN_ETHERNET_RESET + pinMode(PIN_ETHERNET_RESET, OUTPUT); + digitalWrite(PIN_ETHERNET_RESET, HIGH); +#endif + + // generate mac address + uint8_t mac[6]; + generateEthernetMac(mac); + ETHERNET_DEBUG_PRINTLN( + "Ethernet MAC: %02X:%02X:%02X:%02X:%02X:%02X", + mac[0], + mac[1], + mac[2], + mac[3], + mac[4], + mac[5]); + ETHERNET_SPI_PORT.begin(); + Ethernet.init(ETHERNET_SPI_PORT, PIN_ETHERNET_SS); + + // Use static IP if build flags are defined, otherwise DHCP + #if defined(ETHERNET_STATIC_IP) && defined(ETHERNET_STATIC_GATEWAY) && defined(ETHERNET_STATIC_SUBNET) && defined(ETHERNET_STATIC_DNS) + IPAddress ip(ETHERNET_STATIC_IP); + IPAddress gateway(ETHERNET_STATIC_GATEWAY); + IPAddress subnet(ETHERNET_STATIC_SUBNET); + IPAddress dns(ETHERNET_STATIC_DNS); + Ethernet.begin(mac, ip, dns, gateway, subnet); + #else + if (Ethernet.begin(mac) == 0) { + ETHERNET_DEBUG_PRINTLN("Failed to initialize RAK13800 hardware."); + if (Ethernet.hardwareStatus() == EthernetNoHardware) { + ETHERNET_DEBUG_PRINTLN("Ethernet hardware not found."); + } else if (Ethernet.linkStatus() == LinkOFF) { + ETHERNET_DEBUG_PRINTLN("Ethernet cable not connected."); + } else { + ETHERNET_DEBUG_PRINTLN("DHCP failed for unknown reason."); + } + return false; + } + #endif + + ETHERNET_DEBUG_PRINTLN("Ethernet begin complete"); + ETHERNET_DEBUG_PRINT_IP("IP Address", Ethernet.localIP()); + ETHERNET_DEBUG_PRINT_IP("Subnet Mask", Ethernet.subnetMask()); + ETHERNET_DEBUG_PRINT_IP("Gateway", Ethernet.gatewayIP()); + ETHERNET_DEBUG_PRINT_IP("DNS", Ethernet.dnsServerIP()); + + server.begin(); + ETHERNET_DEBUG_PRINTLN("listening on TCP port: %d", ETHERNET_TCP_PORT); + + return true; +} + +int RAK13800EthernetInterface::available() { + return client.available(); +} + +int RAK13800EthernetInterface::read() { + return client.read(); +} + +size_t RAK13800EthernetInterface::write(const uint8_t *buf, size_t size) { + return client.write(buf, size); +} + +bool RAK13800EthernetInterface::isConnected() const { + return _isConnected; +} + +void RAK13800EthernetInterface::loop() { + + Ethernet.maintain(); + + auto newClient = server.accept(); + if (newClient) { + IPAddress remoteIp = newClient.remoteIP(); + uint16_t remotePort = newClient.remotePort(); + ETHERNET_DEBUG_PRINTLN("New client accepted %u.%u.%u.%u:%u", remoteIp[0], remoteIp[1], remoteIp[2], remoteIp[3], remotePort); + if (client) { + ETHERNET_DEBUG_PRINTLN("Closing previous client"); + client.stop(); + } + client = newClient; + onClientConnected(); + } + + _isConnected = client.connected(); + +} diff --git a/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.h b/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.h new file mode 100644 index 00000000..34bb7587 --- /dev/null +++ b/src/helpers/ethernet/RAK13800/RAK13800EthernetInterface.h @@ -0,0 +1,27 @@ +#pragma once + +#include "../SerialEthernetInterface.h" +#include +#include + +class RAK13800EthernetInterface : public SerialEthernetInterface { + + bool _isConnected; + EthernetServer server; + EthernetClient client; + + public: + RAK13800EthernetInterface() : server(EthernetServer(ETHERNET_TCP_PORT)) { + _isConnected = false; + } + + bool begin(); + void loop() override; + + // BaseSerialInterface methods + bool isConnected() const override; + + int available() override; + int read() override; + size_t write(const uint8_t *buf, size_t size) override; +}; diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 31b507b4..65c7ce54 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -190,13 +190,16 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D ETHERNET_ENABLED=1 + -D ETHERNET_USE_RAK13800 + -D ETHERNET_CLASS=RAK13800EthernetInterface ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 ; -D ETHERNET_DEBUG_LOGGING=1 build_src_filter = ${rak4631.build_src_filter} +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> - + + + + + lib_deps = ${rak4631.lib_deps} densaugeo/base64 @ ~1.4.0 From 834ad722a4f8a685e09e43e349a15a5cfe9caf17 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Tue, 28 Jul 2026 03:16:59 +1200 Subject: [PATCH 59/70] remove old rak ethernet interface --- src/helpers/nrf52/SerialEthernetInterface.cpp | 268 ------------------ src/helpers/nrf52/SerialEthernetInterface.h | 82 ------ 2 files changed, 350 deletions(-) delete mode 100644 src/helpers/nrf52/SerialEthernetInterface.cpp delete mode 100644 src/helpers/nrf52/SerialEthernetInterface.h diff --git a/src/helpers/nrf52/SerialEthernetInterface.cpp b/src/helpers/nrf52/SerialEthernetInterface.cpp deleted file mode 100644 index 36a998a4..00000000 --- a/src/helpers/nrf52/SerialEthernetInterface.cpp +++ /dev/null @@ -1,268 +0,0 @@ -#ifdef ETHERNET_ENABLED - -#include "SerialEthernetInterface.h" -#include "EthernetMac.h" -#include -#include - -#define PIN_SPI1_MISO (29) // (0 + 29) -#define PIN_SPI1_MOSI (30) // (0 + 30) -#define PIN_SPI1_SCK (3) // (0 + 3) - -SPIClass ETHERNET_SPI_PORT(NRF_SPIM1, PIN_SPI1_MISO, PIN_SPI1_SCK, PIN_SPI1_MOSI); - -#define PIN_ETHERNET_POWER_EN WB_IO2 // output, high to enable -#define PIN_ETHERNET_RESET 21 -#define PIN_ETHERNET_SS 26 - -#define RECV_STATE_IDLE 0 -#define RECV_STATE_HDR_FOUND 1 -#define RECV_STATE_LEN1_FOUND 2 -#define RECV_STATE_LEN2_FOUND 3 - -bool SerialEthernetInterface::begin() { - - ETHERNET_DEBUG_PRINTLN("Ethernet initializing"); - - // WB_IO2 (power enable) is already driven HIGH by early constructor - // in RAK4631Board.cpp to support POE boot. - // Skip hardware reset — the W5100S comes out of power-on reset cleanly, - // and toggling reset kills the PHY link which breaks POE power. -#ifdef PIN_ETHERNET_RESET - pinMode(PIN_ETHERNET_RESET, OUTPUT); - digitalWrite(PIN_ETHERNET_RESET, HIGH); -#endif - - uint8_t mac[6]; - generateEthernetMac(mac); - ETHERNET_DEBUG_PRINTLN( - "Ethernet MAC: %02X:%02X:%02X:%02X:%02X:%02X", - mac[0], - mac[1], - mac[2], - mac[3], - mac[4], - mac[5]); - ETHERNET_DEBUG_PRINTLN("Init"); - ETHERNET_SPI_PORT.begin(); - Ethernet.init(ETHERNET_SPI_PORT, PIN_ETHERNET_SS); - - // Use static IP if build flags are defined, otherwise DHCP - #if defined(ETHERNET_STATIC_IP) && defined(ETHERNET_STATIC_GATEWAY) && defined(ETHERNET_STATIC_SUBNET) && defined(ETHERNET_STATIC_DNS) - IPAddress ip(ETHERNET_STATIC_IP); - IPAddress gateway(ETHERNET_STATIC_GATEWAY); - IPAddress subnet(ETHERNET_STATIC_SUBNET); - IPAddress dns(ETHERNET_STATIC_DNS); - Ethernet.begin(mac, ip, dns, gateway, subnet); - #else - ETHERNET_DEBUG_PRINTLN("Begin"); - if (Ethernet.begin(mac) == 0) { - ETHERNET_DEBUG_PRINTLN("Begin failed."); - - // DHCP failed -- let's figure out why - if (Ethernet.hardwareStatus() == EthernetNoHardware) // Check for Ethernet hardware present. - { - ETHERNET_DEBUG_PRINTLN("Ethernet hardware not found."); - return false; - } - if (Ethernet.linkStatus() == LinkOFF) // No physical connection - { - ETHERNET_DEBUG_PRINTLN("Ethernet cable not connected."); - return false; - } - ETHERNET_DEBUG_PRINTLN("Ethernet: DHCP failed for unknown reason."); - return false; - } - #endif - ETHERNET_DEBUG_PRINTLN("Ethernet begin complete"); - ETHERNET_DEBUG_PRINT_IP("IP", Ethernet.localIP()); - ETHERNET_DEBUG_PRINT_IP("Subnet", Ethernet.subnetMask()); - ETHERNET_DEBUG_PRINT_IP("Gateway", Ethernet.gatewayIP()); - - server.begin(); // start listening for clients - ETHERNET_DEBUG_PRINTLN("Ethernet: listening on TCP port: %d", ETHERNET_TCP_PORT); - - return true; -} - -void SerialEthernetInterface::enable() { - if (_isEnabled) return; - - _isEnabled = true; - clearBuffers(); -} - -void SerialEthernetInterface::disable() { - _isEnabled = false; -} - -size_t SerialEthernetInterface::writeFrame(const uint8_t src[], size_t len) { - if (len > MAX_FRAME_SIZE) { - ETHERNET_DEBUG_PRINTLN("writeFrame(), frame too big, len=%d\n", len); - return 0; - } - - if (deviceConnected && len > 0) { - if (send_queue_len >= FRAME_QUEUE_SIZE) { - ETHERNET_DEBUG_PRINTLN("writeFrame(), send_queue is full!"); - return 0; - } - - send_queue[send_queue_len].len = len; // add to send queue - memcpy(send_queue[send_queue_len].buf, src, len); - send_queue_len++; - - return len; - } - return 0; -} - -bool SerialEthernetInterface::isWriteBusy() const { - return false; -} - -size_t SerialEthernetInterface::checkRecvFrame(uint8_t dest[]) { - // Use accept() (not available()) so we only see newly-accepted sockets. - // available() also returns existing connected sockets that have data, - // which would cause us to treat each inbound packet as a "new client" - // and stop() the underlying socket — disconnecting the companion. - auto newClient = server.accept(); - if (newClient) { - IPAddress new_ip = newClient.remoteIP(); - uint16_t new_port = newClient.remotePort(); - ETHERNET_DEBUG_PRINTLN( - "New client accepted %u.%u.%u.%u:%u", - new_ip[0], - new_ip[1], - new_ip[2], - new_ip[3], - new_port); - - deviceConnected = false; - if (client) { - ETHERNET_DEBUG_PRINTLN("Closing previous client"); - client.stop(); - } - _state = RECV_STATE_IDLE; - _frame_len = 0; - _rx_len = 0; - client = newClient; - ETHERNET_DEBUG_PRINTLN("Switched to new client"); - } - - if (client.connected()) { - if (!deviceConnected) { - ETHERNET_DEBUG_PRINTLN( - "Got connection %u.%u.%u.%u:%u", - client.remoteIP()[0], - client.remoteIP()[1], - client.remoteIP()[2], - client.remoteIP()[3], - client.remotePort()); - deviceConnected = true; - } - } else { - if (deviceConnected) { - deviceConnected = false; - ETHERNET_DEBUG_PRINTLN("Disconnected"); - } - } - - if (deviceConnected) { - if (send_queue_len > 0) { // first, check send queue - - _last_write = millis(); - int len = send_queue[0].len; - -#if ETHERNET_RAW_LINE - ETHERNET_DEBUG_PRINTLN("TX line len=%d", len); - client.write(send_queue[0].buf, len); - client.write("\r\n", 2); -#else - uint8_t pkt[3+len]; // use same header as serial interface so client can delimit frames - pkt[0] = '>'; - pkt[1] = (len & 0xFF); // LSB - pkt[2] = (len >> 8); // MSB - memcpy(&pkt[3], send_queue[0].buf, send_queue[0].len); - ETHERNET_DEBUG_PRINTLN("Sending frame len=%d", len); - #if ETHERNET_DEBUG_LOGGING && ARDUINO - ETHERNET_DEBUG_PRINTLN("TX frame len=%d", len); - #endif - client.write(pkt, 3 + len); -#endif - send_queue_len--; - for (int i = 0; i < send_queue_len; i++) { // delete top item from queue - send_queue[i] = send_queue[i + 1]; - } - } else { - while (client.available()) { - int c = client.read(); - if (c < 0) break; - -#if ETHERNET_RAW_LINE - if (c == '\r' || c == '\n') { - if (_rx_len == 0) { - continue; - } - uint16_t out_len = _rx_len; - if (out_len > MAX_FRAME_SIZE) { - out_len = MAX_FRAME_SIZE; - } - memcpy(dest, _rx_buf, out_len); - _rx_len = 0; - return out_len; - } - if (_rx_len < MAX_FRAME_SIZE) { - _rx_buf[_rx_len] = (uint8_t)c; - _rx_len++; - } -#else - switch (_state) { - case RECV_STATE_IDLE: - if (c == '<') { - _state = RECV_STATE_HDR_FOUND; - } - break; - case RECV_STATE_HDR_FOUND: - _frame_len = (uint8_t)c; - _state = RECV_STATE_LEN1_FOUND; - break; - case RECV_STATE_LEN1_FOUND: - _frame_len |= ((uint16_t)c) << 8; - _rx_len = 0; - _state = _frame_len > 0 ? RECV_STATE_LEN2_FOUND : RECV_STATE_IDLE; - break; - default: - if (_rx_len < MAX_FRAME_SIZE) { - _rx_buf[_rx_len] = (uint8_t)c; - } - _rx_len++; - if (_rx_len >= _frame_len) { - if (_frame_len > MAX_FRAME_SIZE) { - _frame_len = MAX_FRAME_SIZE; - } - #if ETHERNET_DEBUG_LOGGING && ARDUINO - ETHERNET_DEBUG_PRINTLN("RX frame len=%d", _frame_len); - #endif - memcpy(dest, _rx_buf, _frame_len); - _state = RECV_STATE_IDLE; - return _frame_len; - } - } -#endif - } - } - } - - return 0; -} - -bool SerialEthernetInterface::isConnected() const { - return deviceConnected; -} - -void SerialEthernetInterface::loop() { - Ethernet.maintain(); -} - -#endif // ETHERNET_ENABLED diff --git a/src/helpers/nrf52/SerialEthernetInterface.h b/src/helpers/nrf52/SerialEthernetInterface.h deleted file mode 100644 index b8b4e94b..00000000 --- a/src/helpers/nrf52/SerialEthernetInterface.h +++ /dev/null @@ -1,82 +0,0 @@ -#pragma once - -#ifdef ETHERNET_ENABLED - -#include "helpers/BaseSerialInterface.h" -#include -#include - -#ifndef ETHERNET_TCP_PORT - #define ETHERNET_TCP_PORT 5000 -#endif -// define ETHERNET_RAW_LINE=1 to use raw line-based CLI instead of framed packets - -class SerialEthernetInterface : public BaseSerialInterface { - bool deviceConnected; - bool _isEnabled; - unsigned long _last_write; - uint8_t _state; - uint16_t _frame_len; - uint16_t _rx_len; - uint8_t _rx_buf[MAX_FRAME_SIZE]; - - EthernetServer server; - EthernetClient client; - - struct Frame { - uint8_t len; - uint8_t buf[MAX_FRAME_SIZE]; - }; - - #define FRAME_QUEUE_SIZE 4 - int send_queue_len; - Frame send_queue[FRAME_QUEUE_SIZE]; - - void clearBuffers() { - send_queue_len = 0; - _state = 0; - _frame_len = 0; - _rx_len = 0; - } - - protected: - - public: - SerialEthernetInterface() : server(EthernetServer(ETHERNET_TCP_PORT)) { - deviceConnected = false; - _isEnabled = false; - _last_write = 0; - send_queue_len = 0; - _state = 0; - _frame_len = 0; - _rx_len = 0; - } - bool begin(); - - // BaseSerialInterface methods - void enable() override; - void disable() override; - bool isEnabled() const override { return _isEnabled; } - - bool isConnected() const override; - bool isWriteBusy() const override; - - size_t writeFrame(const uint8_t src[], size_t len) override; - size_t checkRecvFrame(uint8_t dest[]) override; - - void loop(); -}; - - -#if ETHERNET_DEBUG_LOGGING && ARDUINO - #include - #define ETHERNET_DEBUG_PRINT(F, ...) Serial.printf("ETH: " F, ##__VA_ARGS__) - #define ETHERNET_DEBUG_PRINTLN(F, ...) Serial.printf("ETH: " F "\n", ##__VA_ARGS__) - #define ETHERNET_DEBUG_PRINT_IP(name, ip) Serial.printf(name ": %u.%u.%u.%u" "\n", ip[0], ip[1], ip[2], ip[3]) -#else - #define ETHERNET_DEBUG_PRINT(...) {} - #define ETHERNET_DEBUG_PRINTLN(...) {} - #define ETHERNET_DEBUG_PRINT_IP(...) {} -#endif - -#endif // ETHERNET_ENABLED From e7f2c71701f5e5f42507440d05f8622dc2a9430d Mon Sep 17 00:00:00 2001 From: liamcottle Date: Tue, 28 Jul 2026 03:45:02 +1200 Subject: [PATCH 60/70] usb companion firmwares now require build flag to include usb interface --- variants/ebyte_eora_s3/platformio.ini | 1 + variants/gat562_30s_mesh_kit/platformio.ini | 1 + variants/gat562_mesh_tracker_pro/platformio.ini | 1 + variants/heltec_ct62/platformio.ini | 1 + variants/heltec_e213/platformio.ini | 1 + variants/heltec_mesh_solar/platformio.ini | 1 + variants/heltec_rc32/platformio.ini | 2 ++ variants/heltec_t096/platformio.ini | 1 + variants/heltec_t1/platformio.ini | 1 + variants/heltec_t114/platformio.ini | 2 ++ variants/heltec_t190/platformio.ini | 1 + variants/heltec_tower_v2/platformio.ini | 1 + variants/heltec_tracker/platformio.ini | 1 + variants/heltec_tracker_v2/platformio.ini | 1 + variants/heltec_v2/platformio.ini | 1 + variants/heltec_v3/platformio.ini | 2 ++ variants/heltec_v4/platformio.ini | 2 ++ variants/heltec_v4_r8/platformio.ini | 2 ++ variants/heltec_wireless_paper/platformio.ini | 1 + variants/ikoka_handheld_nrf/platformio.ini | 2 ++ variants/ikoka_nano_nrf/platformio.ini | 1 + variants/ikoka_stick_nrf/platformio.ini | 1 + variants/keepteen_lt1/platformio.ini | 1 + variants/lilygo_t3s3/platformio.ini | 1 + variants/lilygo_t3s3_sx1276/platformio.ini | 1 + variants/lilygo_tbeam_1w/platformio.ini | 1 + variants/lilygo_tdeck/platformio.ini | 1 + variants/lilygo_techo/platformio.ini | 1 + variants/lilygo_techo_card/platformio.ini | 1 + variants/lilygo_techo_lite/platformio.ini | 1 + variants/lilygo_teth_elite/platformio.ini | 1 + variants/lilygo_tlora_v2_1/platformio.ini | 1 + variants/m5stack_unit_c6l/platformio.ini | 1 + variants/mesh_pocket/platformio.ini | 1 + variants/meshadventurer/platformio.ini | 2 ++ variants/meshtiny/platformio.ini | 1 + variants/minewsemi_me25ls01/platformio.ini | 1 + variants/muziworks_r1_neo/platformio.ini | 1 + variants/nano_g2_ultra/platformio.ini | 1 + variants/nibble_screen_connect/platformio.ini | 1 + variants/nibble_zero_connect/platformio.ini | 1 + variants/promicro/platformio.ini | 1 + variants/rak11310/platformio.ini | 1 + variants/rak3112/platformio.ini | 1 + variants/rak3401/platformio.ini | 1 + variants/rak3x72/platformio.ini | 1 + variants/rak4631/platformio.ini | 1 + variants/rak_wismesh_tag/platformio.ini | 1 + variants/rpi_picow/platformio.ini | 1 + variants/sensecap_solar/platformio.ini | 1 + variants/station_g2/platformio.ini | 1 + variants/station_g3_esp32/platformio.ini | 1 + variants/t1000-e/platformio.ini | 1 + variants/thinknode_m1/platformio.ini | 1 + variants/thinknode_m2/platformio.ini | 1 + variants/thinknode_m3/platformio.ini | 1 + variants/thinknode_m5/platformio.ini | 1 + variants/thinknode_m6/platformio.ini | 1 + variants/thinknode_m7/platformio.ini | 1 + variants/thinknode_m9/platformio.ini | 1 + variants/tiny_relay/platformio.ini | 1 + variants/waveshare_rp2040_lora/platformio.ini | 1 + variants/wio-e5-dev/platformio.ini | 1 + variants/wio-e5-mini/platformio.ini | 1 + variants/wio-tracker-l1/platformio.ini | 1 + variants/xiao_c3/platformio.ini | 1 + variants/xiao_nrf52/platformio.ini | 1 + variants/xiao_rp2040/platformio.ini | 1 + variants/xiao_s3/platformio.ini | 1 + variants/xiao_s3_wio/platformio.ini | 1 + 70 files changed, 77 insertions(+) diff --git a/variants/ebyte_eora_s3/platformio.ini b/variants/ebyte_eora_s3/platformio.ini index 15fe761b..1ab3d3fb 100644 --- a/variants/ebyte_eora_s3/platformio.ini +++ b/variants/ebyte_eora_s3/platformio.ini @@ -102,6 +102,7 @@ build_flags = -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${Ebyte_EoRa-S3.build_src_filter} diff --git a/variants/gat562_30s_mesh_kit/platformio.ini b/variants/gat562_30s_mesh_kit/platformio.ini index 2baac256..89276a49 100644 --- a/variants/gat562_30s_mesh_kit/platformio.ini +++ b/variants/gat562_30s_mesh_kit/platformio.ini @@ -75,6 +75,7 @@ build_flags = -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${GAT562_30S_Mesh_Kit.build_src_filter} diff --git a/variants/gat562_mesh_tracker_pro/platformio.ini b/variants/gat562_mesh_tracker_pro/platformio.ini index af153b8f..142cfe4b 100644 --- a/variants/gat562_mesh_tracker_pro/platformio.ini +++ b/variants/gat562_mesh_tracker_pro/platformio.ini @@ -71,6 +71,7 @@ build_flags = -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${GAT562_Mesh_Tracker_Pro.build_src_filter} diff --git a/variants/heltec_ct62/platformio.ini b/variants/heltec_ct62/platformio.ini index 0179d965..401a30d2 100644 --- a/variants/heltec_ct62/platformio.ini +++ b/variants/heltec_ct62/platformio.ini @@ -101,6 +101,7 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_USB_INTERFACE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_ct62.build_src_filter} diff --git a/variants/heltec_e213/platformio.ini b/variants/heltec_e213/platformio.ini index 123edd91..02193819 100644 --- a/variants/heltec_e213/platformio.ini +++ b/variants/heltec_e213/platformio.ini @@ -73,6 +73,7 @@ build_flags = -D DISPLAY_CLASS=E213Display -D AUTO_OFF_MILLIS=0 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_USB_INTERFACE build_src_filter = ${Heltec_E213_base.build_src_filter} + + diff --git a/variants/heltec_mesh_solar/platformio.ini b/variants/heltec_mesh_solar/platformio.ini index fb5cd515..17f8ba21 100644 --- a/variants/heltec_mesh_solar/platformio.ini +++ b/variants/heltec_mesh_solar/platformio.ini @@ -87,6 +87,7 @@ build_flags = ${Heltec_mesh_solar.build_flags} -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; -D BLE_PIN_CODE=123456 ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 diff --git a/variants/heltec_rc32/platformio.ini b/variants/heltec_rc32/platformio.ini index 6361f61d..354004f0 100644 --- a/variants/heltec_rc32/platformio.ini +++ b/variants/heltec_rc32/platformio.ini @@ -131,6 +131,7 @@ build_flags = -D DISPLAY_CLASS=NullDisplayDriver -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE build_src_filter = ${Heltec_RC32.build_src_filter} + + @@ -275,6 +276,7 @@ build_flags = -D UI_HAS_ROTARY_INPUT -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE build_src_filter = ${Heltec_RC32_with_display.build_src_filter} + + diff --git a/variants/heltec_t096/platformio.ini b/variants/heltec_t096/platformio.ini index 0a062f00..02e88328 100644 --- a/variants/heltec_t096/platformio.ini +++ b/variants/heltec_t096/platformio.ini @@ -152,6 +152,7 @@ build_flags = -I examples/companion_radio/ui-new -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; -D BLE_PIN_CODE=123456 ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 diff --git a/variants/heltec_t1/platformio.ini b/variants/heltec_t1/platformio.ini index eaa8af37..28ed67ed 100644 --- a/variants/heltec_t1/platformio.ini +++ b/variants/heltec_t1/platformio.ini @@ -90,6 +90,7 @@ build_flags = -I examples/companion_radio/ui-new -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_t1.build_src_filter} diff --git a/variants/heltec_t114/platformio.ini b/variants/heltec_t114/platformio.ini index 48dfe078..48fb4b0f 100644 --- a/variants/heltec_t114/platformio.ini +++ b/variants/heltec_t114/platformio.ini @@ -127,6 +127,7 @@ build_flags = -D DISPLAY_CLASS=NullDisplayDriver -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE build_src_filter = ${Heltec_t114.build_src_filter} + + @@ -238,6 +239,7 @@ build_flags = -I examples/companion_radio/ui-new -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; -D BLE_PIN_CODE=123456 ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 diff --git a/variants/heltec_t190/platformio.ini b/variants/heltec_t190/platformio.ini index 411fee85..28355393 100644 --- a/variants/heltec_t190/platformio.ini +++ b/variants/heltec_t190/platformio.ini @@ -74,6 +74,7 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_USB_INTERFACE build_src_filter = ${Heltec_T190_base.build_src_filter} + +<../examples/companion_radio/*.cpp> diff --git a/variants/heltec_tower_v2/platformio.ini b/variants/heltec_tower_v2/platformio.ini index 9c703ae1..207e0a50 100644 --- a/variants/heltec_tower_v2/platformio.ini +++ b/variants/heltec_tower_v2/platformio.ini @@ -87,6 +87,7 @@ build_flags = -D DISPLAY_CLASS=NullDisplayDriver -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; -D BLE_PIN_CODE=123456 ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 diff --git a/variants/heltec_tracker/platformio.ini b/variants/heltec_tracker/platformio.ini index 07d2e987..75d1cadf 100644 --- a/variants/heltec_tracker/platformio.ini +++ b/variants/heltec_tracker/platformio.ini @@ -59,6 +59,7 @@ build_flags = -D DISPLAY_CLASS=ST7735Display -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; -D BLE_PIN_CODE=123456 ; HWT will use display for pin ; -D OFFLINE_QUEUE_SIZE=256 ; -D BLE_DEBUG_LOGGING=1 diff --git a/variants/heltec_tracker_v2/platformio.ini b/variants/heltec_tracker_v2/platformio.ini index f1fe9dd0..d040b72f 100644 --- a/variants/heltec_tracker_v2/platformio.ini +++ b/variants/heltec_tracker_v2/platformio.ini @@ -140,6 +140,7 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=ST7735Display + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${Heltec_tracker_v2.build_src_filter} diff --git a/variants/heltec_v2/platformio.ini b/variants/heltec_v2/platformio.ini index ba4f8694..78561a14 100644 --- a/variants/heltec_v2/platformio.ini +++ b/variants/heltec_v2/platformio.ini @@ -137,6 +137,7 @@ build_flags = -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=160 -D MAX_GROUP_CHANNELS=8 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v2.build_src_filter} diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini index a70a93a5..65e636eb 100644 --- a/variants/heltec_v3/platformio.ini +++ b/variants/heltec_v3/platformio.ini @@ -144,6 +144,7 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=SSD1306Display + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v3.build_src_filter} @@ -326,6 +327,7 @@ build_flags = ${Heltec_lora32_v3.build_flags} -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v3.build_src_filter} diff --git a/variants/heltec_v4/platformio.ini b/variants/heltec_v4/platformio.ini index cb76beb3..d718c006 100644 --- a/variants/heltec_v4/platformio.ini +++ b/variants/heltec_v4/platformio.ini @@ -187,6 +187,7 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=SSD1306Display + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${heltec_v4_oled.build_src_filter} @@ -351,6 +352,7 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=ST7789LCDDisplay + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${heltec_v4_tft.build_src_filter} diff --git a/variants/heltec_v4_r8/platformio.ini b/variants/heltec_v4_r8/platformio.ini index 4057d6f1..f523ebf9 100644 --- a/variants/heltec_v4_r8/platformio.ini +++ b/variants/heltec_v4_r8/platformio.ini @@ -138,6 +138,7 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=SSD1306Display + -D ENABLE_USB_INTERFACE build_src_filter = ${heltec_v4_r8_oled.build_src_filter} + + @@ -262,6 +263,7 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=ST7789LCDDisplay + -D ENABLE_USB_INTERFACE build_src_filter = ${heltec_v4_r8_tft.build_src_filter} + + diff --git a/variants/heltec_wireless_paper/platformio.ini b/variants/heltec_wireless_paper/platformio.ini index 48723d16..55ed62b3 100644 --- a/variants/heltec_wireless_paper/platformio.ini +++ b/variants/heltec_wireless_paper/platformio.ini @@ -73,6 +73,7 @@ build_flags = -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=E213Display -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_USB_INTERFACE build_src_filter = ${Heltec_Wireless_Paper_base.build_src_filter} + + diff --git a/variants/ikoka_handheld_nrf/platformio.ini b/variants/ikoka_handheld_nrf/platformio.ini index 51b602e4..1c6f17c2 100644 --- a/variants/ikoka_handheld_nrf/platformio.ini +++ b/variants/ikoka_handheld_nrf/platformio.ini @@ -74,6 +74,7 @@ extends = ikoka_handheld_nrf board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld build_flags = ${ikoka_handheld_nrf_ssd1306_companion.build_flags} -D LORA_TX_POWER=20 + -D ENABLE_USB_INTERFACE build_src_filter = ${ikoka_handheld_nrf_ssd1306_companion.build_src_filter} [env:ikoka_handheld_nrf_e22_30dbm_096_rotated_companion_radio_usb] @@ -82,6 +83,7 @@ board_build.ldscript = boards/nrf52840_s140_v7_extrafs.ld build_flags = ${ikoka_handheld_nrf_ssd1306_companion.build_flags} -D LORA_TX_POWER=20 -D DISPLAY_ROTATION=2 + -D ENABLE_USB_INTERFACE build_src_filter = ${ikoka_handheld_nrf_ssd1306_companion.build_src_filter} [env:ikoka_handheld_nrf_e22_30dbm_repeater] diff --git a/variants/ikoka_nano_nrf/platformio.ini b/variants/ikoka_nano_nrf/platformio.ini index 7880ea4a..87c6240e 100644 --- a/variants/ikoka_nano_nrf/platformio.ini +++ b/variants/ikoka_nano_nrf/platformio.ini @@ -105,6 +105,7 @@ build_flags = -D MAX_GROUP_CHANNELS=40 -I examples/companion_radio/ui-new -D QSPIFLASH=1 + -D ENABLE_USB_INTERFACE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${ikoka_nano_nrf.build_src_filter} diff --git a/variants/ikoka_stick_nrf/platformio.ini b/variants/ikoka_stick_nrf/platformio.ini index 06e39e84..c8184fc4 100644 --- a/variants/ikoka_stick_nrf/platformio.ini +++ b/variants/ikoka_stick_nrf/platformio.ini @@ -111,6 +111,7 @@ build_flags = -D MAX_GROUP_CHANNELS=40 -I examples/companion_radio/ui-new -D QSPIFLASH=1 + -D ENABLE_USB_INTERFACE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${ikoka_stick_nrf.build_src_filter} diff --git a/variants/keepteen_lt1/platformio.ini b/variants/keepteen_lt1/platformio.ini index 27cf809e..11dc214b 100644 --- a/variants/keepteen_lt1/platformio.ini +++ b/variants/keepteen_lt1/platformio.ini @@ -66,6 +66,7 @@ build_flags = ${KeepteenLT1.build_flags} -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=SSD1306Display + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${KeepteenLT1.build_src_filter} diff --git a/variants/lilygo_t3s3/platformio.ini b/variants/lilygo_t3s3/platformio.ini index 54990117..966235c6 100644 --- a/variants/lilygo_t3s3/platformio.ini +++ b/variants/lilygo_t3s3/platformio.ini @@ -140,6 +140,7 @@ build_flags = -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${LilyGo_T3S3_sx1262.build_src_filter} diff --git a/variants/lilygo_t3s3_sx1276/platformio.ini b/variants/lilygo_t3s3_sx1276/platformio.ini index e579e91c..c6497dcb 100644 --- a/variants/lilygo_t3s3_sx1276/platformio.ini +++ b/variants/lilygo_t3s3_sx1276/platformio.ini @@ -138,6 +138,7 @@ build_flags = -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE build_src_filter = ${LilyGo_T3S3_sx1276.build_src_filter} + + diff --git a/variants/lilygo_tbeam_1w/platformio.ini b/variants/lilygo_tbeam_1w/platformio.ini index c7a59552..0f604fac 100644 --- a/variants/lilygo_tbeam_1w/platformio.ini +++ b/variants/lilygo_tbeam_1w/platformio.ini @@ -114,6 +114,7 @@ build_flags = -D MAX_GROUP_CHANNELS=40 -D PERSISTANT_GPS=1 -D ENV_SKIP_GPS_DETECT=1 + -D ENABLE_USB_INTERFACE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${LilyGo_TBeam_1W.build_src_filter} diff --git a/variants/lilygo_tdeck/platformio.ini b/variants/lilygo_tdeck/platformio.ini index 745d8ff5..00571db9 100644 --- a/variants/lilygo_tdeck/platformio.ini +++ b/variants/lilygo_tdeck/platformio.ini @@ -71,6 +71,7 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_USB_INTERFACE build_src_filter = ${LilyGo_TDeck.build_src_filter} + + diff --git a/variants/lilygo_techo/platformio.ini b/variants/lilygo_techo/platformio.ini index 5df77f95..81da2e48 100644 --- a/variants/lilygo_techo/platformio.ini +++ b/variants/lilygo_techo/platformio.ini @@ -120,6 +120,7 @@ build_flags = -D UI_SENSORS_PAGE=1 -D AUTO_SHUTDOWN_MILLIVOLTS=3300 -D QSPIFLASH=1 + -D ENABLE_USB_INTERFACE build_src_filter = ${LilyGo_T-Echo.build_src_filter} +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> diff --git a/variants/lilygo_techo_card/platformio.ini b/variants/lilygo_techo_card/platformio.ini index c7bb7378..4ebf43da 100644 --- a/variants/lilygo_techo_card/platformio.ini +++ b/variants/lilygo_techo_card/platformio.ini @@ -110,6 +110,7 @@ build_flags = ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 -D AUTO_SHUTDOWN_MILLIVOLTS=3300 + -D ENABLE_USB_INTERFACE build_src_filter = ${LilyGo_T-Echo_Card.build_src_filter} +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-tiny/*.cpp> diff --git a/variants/lilygo_techo_lite/platformio.ini b/variants/lilygo_techo_lite/platformio.ini index a9b3d124..9ec73d59 100644 --- a/variants/lilygo_techo_lite/platformio.ini +++ b/variants/lilygo_techo_lite/platformio.ini @@ -174,6 +174,7 @@ build_flags = -D MAX_GROUP_CHANNELS=40 -D UI_RECENT_LIST_SIZE=9 -D AUTO_SHUTDOWN_MILLIVOLTS=3300 + -D ENABLE_USB_INTERFACE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${nrf52_base.build_src_filter} diff --git a/variants/lilygo_teth_elite/platformio.ini b/variants/lilygo_teth_elite/platformio.ini index 97728f8b..ee1b9879 100644 --- a/variants/lilygo_teth_elite/platformio.ini +++ b/variants/lilygo_teth_elite/platformio.ini @@ -72,6 +72,7 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_USB_INTERFACE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${LilyGo_TETH_Elite_sx1262.build_src_filter} diff --git a/variants/lilygo_tlora_v2_1/platformio.ini b/variants/lilygo_tlora_v2_1/platformio.ini index 36731668..1aea74d2 100644 --- a/variants/lilygo_tlora_v2_1/platformio.ini +++ b/variants/lilygo_tlora_v2_1/platformio.ini @@ -77,6 +77,7 @@ build_flags = -I examples/companion_radio/ui-new -D MAX_CONTACTS=160 -D MAX_GROUP_CHANNELS=8 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter} diff --git a/variants/m5stack_unit_c6l/platformio.ini b/variants/m5stack_unit_c6l/platformio.ini index 94083eb4..190add04 100644 --- a/variants/m5stack_unit_c6l/platformio.ini +++ b/variants/m5stack_unit_c6l/platformio.ini @@ -97,6 +97,7 @@ build_flags = ${M5Stack_Unit_C6L.build_flags} -D OFFLINE_QUEUE_SIZE=256 -D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 + -D ENABLE_USB_INTERFACE build_src_filter = ${M5Stack_Unit_C6L.build_src_filter} + - diff --git a/variants/mesh_pocket/platformio.ini b/variants/mesh_pocket/platformio.ini index 0d2a74ad..a6f823db 100644 --- a/variants/mesh_pocket/platformio.ini +++ b/variants/mesh_pocket/platformio.ini @@ -96,6 +96,7 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D AUTO_OFF_MILLIS=0 + -D ENABLE_USB_INTERFACE ; -D BLE_PIN_CODE=123456 ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 diff --git a/variants/meshadventurer/platformio.ini b/variants/meshadventurer/platformio.ini index f85be238..149d324f 100644 --- a/variants/meshadventurer/platformio.ini +++ b/variants/meshadventurer/platformio.ini @@ -186,6 +186,7 @@ build_flags = -D MAX_CONTACTS=160 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=128 + -D ENABLE_USB_INTERFACE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 lib_deps = @@ -266,6 +267,7 @@ build_flags = -D MAX_CONTACTS=160 -D OFFLINE_QUEUE_SIZE=128 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 lib_deps = diff --git a/variants/meshtiny/platformio.ini b/variants/meshtiny/platformio.ini index c5439c88..c0a3c5f9 100644 --- a/variants/meshtiny/platformio.ini +++ b/variants/meshtiny/platformio.ini @@ -36,6 +36,7 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_USB_INTERFACE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Meshtiny.build_src_filter} diff --git a/variants/minewsemi_me25ls01/platformio.ini b/variants/minewsemi_me25ls01/platformio.ini index ac81fa16..b1d3a161 100644 --- a/variants/minewsemi_me25ls01/platformio.ini +++ b/variants/minewsemi_me25ls01/platformio.ini @@ -144,6 +144,7 @@ build_flags = ${me25ls01.build_flags} -D RX_BOOSTED_GAIN=true -D RF_SWITCH_TABLE -D DISPLAY_CLASS=NullDisplayDriver + -D ENABLE_USB_INTERFACE build_src_filter = ${me25ls01.build_src_filter} + + diff --git a/variants/muziworks_r1_neo/platformio.ini b/variants/muziworks_r1_neo/platformio.ini index 3dbecf1e..cf02b9e2 100644 --- a/variants/muziworks_r1_neo/platformio.ini +++ b/variants/muziworks_r1_neo/platformio.ini @@ -66,6 +66,7 @@ build_flags = -I examples/companion_radio/ui-orig -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${R1Neo.build_src_filter} diff --git a/variants/nano_g2_ultra/platformio.ini b/variants/nano_g2_ultra/platformio.ini index 3cdc29ff..b817b3e9 100644 --- a/variants/nano_g2_ultra/platformio.ini +++ b/variants/nano_g2_ultra/platformio.ini @@ -99,6 +99,7 @@ build_flags = -D OFFLINE_QUEUE_SIZE=256 -D DISPLAY_CLASS=SH1106Display -D PIN_BUZZER=4 + -D ENABLE_USB_INTERFACE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Nano_G2_Ultra.build_src_filter} diff --git a/variants/nibble_screen_connect/platformio.ini b/variants/nibble_screen_connect/platformio.ini index 6a2f3dec..112181df 100644 --- a/variants/nibble_screen_connect/platformio.ini +++ b/variants/nibble_screen_connect/platformio.ini @@ -107,6 +107,7 @@ build_flags = -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=300 -D MAX_GROUP_CHANNELS=8 + -D ENABLE_USB_INTERFACE build_src_filter = ${nibble_screen_connect_base.build_src_filter} + + diff --git a/variants/nibble_zero_connect/platformio.ini b/variants/nibble_zero_connect/platformio.ini index 83495cd8..1161743e 100644 --- a/variants/nibble_zero_connect/platformio.ini +++ b/variants/nibble_zero_connect/platformio.ini @@ -104,6 +104,7 @@ build_flags = -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=300 -D MAX_GROUP_CHANNELS=8 + -D ENABLE_USB_INTERFACE build_src_filter = ${nibble_zero_connect_base.build_src_filter} + + diff --git a/variants/promicro/platformio.ini b/variants/promicro/platformio.ini index 5415e158..dea4ccfa 100644 --- a/variants/promicro/platformio.ini +++ b/variants/promicro/platformio.ini @@ -119,6 +119,7 @@ build_flags = ${Promicro.build_flags} -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=SSD1306Display + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${Promicro.build_src_filter} diff --git a/variants/rak11310/platformio.ini b/variants/rak11310/platformio.ini index ab820cf6..c8526317 100644 --- a/variants/rak11310/platformio.ini +++ b/variants/rak11310/platformio.ini @@ -85,6 +85,7 @@ extends = rak11310 build_flags = ${rak11310.build_flags} -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${rak11310.build_src_filter} diff --git a/variants/rak3112/platformio.ini b/variants/rak3112/platformio.ini index 9cd32c4b..8bd3c597 100644 --- a/variants/rak3112/platformio.ini +++ b/variants/rak3112/platformio.ini @@ -136,6 +136,7 @@ build_flags = -I examples/companion_radio/ui-orig -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${rak3112.build_src_filter} diff --git a/variants/rak3401/platformio.ini b/variants/rak3401/platformio.ini index 20a8a548..48e71922 100644 --- a/variants/rak3401/platformio.ini +++ b/variants/rak3401/platformio.ini @@ -67,6 +67,7 @@ build_flags = -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${rak3401.build_src_filter} diff --git a/variants/rak3x72/platformio.ini b/variants/rak3x72/platformio.ini index f9667860..c23019c8 100644 --- a/variants/rak3x72/platformio.ini +++ b/variants/rak3x72/platformio.ini @@ -37,6 +37,7 @@ build_flags = ${rak3x72.build_flags} ; -D FORMAT_FS=true -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 + -D ENABLE_USB_INTERFACE build_src_filter = ${rak3x72.build_src_filter} +<../examples/companion_radio/*.cpp> lib_deps = ${rak3x72.lib_deps} diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 65c7ce54..1a610956 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -165,6 +165,7 @@ build_flags = -D DISPLAY_CLASS=SSD1306Display -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${rak4631.build_src_filter} diff --git a/variants/rak_wismesh_tag/platformio.ini b/variants/rak_wismesh_tag/platformio.ini index e9cddb74..d6140187 100644 --- a/variants/rak_wismesh_tag/platformio.ini +++ b/variants/rak_wismesh_tag/platformio.ini @@ -71,6 +71,7 @@ build_flags = -I examples/companion_radio/ui-orig -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${rak_wismesh_tag.build_src_filter} diff --git a/variants/rpi_picow/platformio.ini b/variants/rpi_picow/platformio.ini index 66c7b9f8..0fe8c436 100644 --- a/variants/rpi_picow/platformio.ini +++ b/variants/rpi_picow/platformio.ini @@ -58,6 +58,7 @@ extends = rpi_picow build_flags = ${rpi_picow.build_flags} -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${rpi_picow.build_src_filter} diff --git a/variants/sensecap_solar/platformio.ini b/variants/sensecap_solar/platformio.ini index effef38c..6e0eadbc 100644 --- a/variants/sensecap_solar/platformio.ini +++ b/variants/sensecap_solar/platformio.ini @@ -92,6 +92,7 @@ build_flags = ${SenseCap_Solar.build_flags} -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${SenseCap_Solar.build_src_filter} diff --git a/variants/station_g2/platformio.ini b/variants/station_g2/platformio.ini index 508ffe4b..e463aee0 100644 --- a/variants/station_g2/platformio.ini +++ b/variants/station_g2/platformio.ini @@ -188,6 +188,7 @@ build_flags = -I examples/companion_radio/ui-new -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${Station_G2.build_src_filter} diff --git a/variants/station_g3_esp32/platformio.ini b/variants/station_g3_esp32/platformio.ini index b29388ef..96eca1fc 100644 --- a/variants/station_g3_esp32/platformio.ini +++ b/variants/station_g3_esp32/platformio.ini @@ -103,6 +103,7 @@ build_flags = -I examples/companion_radio/ui-new -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${Station_G3_ESP32.build_src_filter} diff --git a/variants/t1000-e/platformio.ini b/variants/t1000-e/platformio.ini index 6f32e54d..8456dc91 100644 --- a/variants/t1000-e/platformio.ini +++ b/variants/t1000-e/platformio.ini @@ -82,6 +82,7 @@ build_flags = ${t1000-e.build_flags} -D DISPLAY_CLASS=NullDisplayDriver -D PIN_BUZZER=25 -D PIN_BUZZER_EN=37 ; P1/5 - required for T1000-E + -D ENABLE_USB_INTERFACE build_src_filter = ${t1000-e.build_src_filter} + + diff --git a/variants/thinknode_m1/platformio.ini b/variants/thinknode_m1/platformio.ini index 617f9240..89c48403 100644 --- a/variants/thinknode_m1/platformio.ini +++ b/variants/thinknode_m1/platformio.ini @@ -117,6 +117,7 @@ build_flags = -D OFFLINE_QUEUE_SIZE=256 -D PIN_BUZZER=6 -D AUTO_SHUTDOWN_MILLIVOLTS=3300 + -D ENABLE_USB_INTERFACE build_src_filter = ${ThinkNode_M1.build_src_filter} + + diff --git a/variants/thinknode_m2/platformio.ini b/variants/thinknode_m2/platformio.ini index aae83324..79dac6ab 100644 --- a/variants/thinknode_m2/platformio.ini +++ b/variants/thinknode_m2/platformio.ini @@ -159,6 +159,7 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_USB_INTERFACE build_src_filter = ${ThinkNode_M2.build_src_filter} + + diff --git a/variants/thinknode_m3/platformio.ini b/variants/thinknode_m3/platformio.ini index f80a6161..74382b36 100644 --- a/variants/thinknode_m3/platformio.ini +++ b/variants/thinknode_m3/platformio.ini @@ -83,6 +83,7 @@ build_flags = ${ThinkNode_M3.build_flags} -D DISPLAY_CLASS=NullDisplayDriver -D PIN_BUZZER=23 -D PIN_BUZZER_EN=36 + -D ENABLE_USB_INTERFACE build_src_filter = ${ThinkNode_M3.build_src_filter} + + diff --git a/variants/thinknode_m5/platformio.ini b/variants/thinknode_m5/platformio.ini index 8572d1eb..eda21bf0 100644 --- a/variants/thinknode_m5/platformio.ini +++ b/variants/thinknode_m5/platformio.ini @@ -173,6 +173,7 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_USB_INTERFACE build_src_filter = ${ThinkNode_M5.build_src_filter} + + diff --git a/variants/thinknode_m6/platformio.ini b/variants/thinknode_m6/platformio.ini index 187a8819..3606d6a8 100644 --- a/variants/thinknode_m6/platformio.ini +++ b/variants/thinknode_m6/platformio.ini @@ -110,6 +110,7 @@ build_flags = -D QSPIFLASH=1 -D OFFLINE_QUEUE_SIZE=256 -D AUTO_SHUTDOWN_MILLIVOLTS=3300 + -D ENABLE_USB_INTERFACE build_src_filter = ${ThinkNode_M6.build_src_filter} + + diff --git a/variants/thinknode_m7/platformio.ini b/variants/thinknode_m7/platformio.ini index 2ff6fc0a..eb745b6d 100644 --- a/variants/thinknode_m7/platformio.ini +++ b/variants/thinknode_m7/platformio.ini @@ -120,6 +120,7 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_USB_INTERFACE build_src_filter = ${ThinkNode_M7.build_src_filter} + + diff --git a/variants/thinknode_m9/platformio.ini b/variants/thinknode_m9/platformio.ini index a7171726..09b1391d 100755 --- a/variants/thinknode_m9/platformio.ini +++ b/variants/thinknode_m9/platformio.ini @@ -120,6 +120,7 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_USB_INTERFACE build_src_filter = ${ThinkNode_M9.build_src_filter} + + diff --git a/variants/tiny_relay/platformio.ini b/variants/tiny_relay/platformio.ini index 82cb251f..13c2ec1c 100644 --- a/variants/tiny_relay/platformio.ini +++ b/variants/tiny_relay/platformio.ini @@ -44,6 +44,7 @@ build_flags = ${Tiny_Relay.build_flags} -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 -D MAX_LORA_TX_POWER=22 + -D ENABLE_USB_INTERFACE build_src_filter = ${Tiny_Relay.build_src_filter} +<../examples/companion_radio/*.cpp> lib_deps = ${Tiny_Relay.lib_deps} diff --git a/variants/waveshare_rp2040_lora/platformio.ini b/variants/waveshare_rp2040_lora/platformio.ini index 7dfe1401..db1ec603 100644 --- a/variants/waveshare_rp2040_lora/platformio.ini +++ b/variants/waveshare_rp2040_lora/platformio.ini @@ -84,6 +84,7 @@ extends = waveshare_rp2040_lora build_flags = ${waveshare_rp2040_lora.build_flags} -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${waveshare_rp2040_lora.build_src_filter} diff --git a/variants/wio-e5-dev/platformio.ini b/variants/wio-e5-dev/platformio.ini index 22bdc3c8..82b3781e 100644 --- a/variants/wio-e5-dev/platformio.ini +++ b/variants/wio-e5-dev/platformio.ini @@ -46,6 +46,7 @@ build_flags = ${lora_e5.build_flags} -D LORA_TX_POWER=22 -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 + -D ENABLE_USB_INTERFACE build_src_filter = ${lora_e5.build_src_filter} +<../examples/companion_radio/*.cpp> lib_deps = ${lora_e5.lib_deps} diff --git a/variants/wio-e5-mini/platformio.ini b/variants/wio-e5-mini/platformio.ini index 7cf26198..021c7db2 100644 --- a/variants/wio-e5-mini/platformio.ini +++ b/variants/wio-e5-mini/platformio.ini @@ -44,6 +44,7 @@ build_flags = ${lora_e5_mini.build_flags} -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 -D DISPLAY_CLASS=NullDisplayDriver + -D ENABLE_USB_INTERFACE build_src_filter = ${lora_e5_mini.build_src_filter} + +<../examples/companion_radio/*.cpp> diff --git a/variants/wio-tracker-l1/platformio.ini b/variants/wio-tracker-l1/platformio.ini index 7bb175bb..fc958ea2 100644 --- a/variants/wio-tracker-l1/platformio.ini +++ b/variants/wio-tracker-l1/platformio.ini @@ -68,6 +68,7 @@ build_flags = ${WioTrackerL1.build_flags} -D OFFLINE_QUEUE_SIZE=256 -D PIN_BUZZER=12 -D QSPIFLASH=1 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${WioTrackerL1.build_src_filter} diff --git a/variants/xiao_c3/platformio.ini b/variants/xiao_c3/platformio.ini index c0e8458d..587c5c27 100644 --- a/variants/xiao_c3/platformio.ini +++ b/variants/xiao_c3/platformio.ini @@ -100,6 +100,7 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_USB_INTERFACE ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 diff --git a/variants/xiao_nrf52/platformio.ini b/variants/xiao_nrf52/platformio.ini index a0854336..4551a233 100644 --- a/variants/xiao_nrf52/platformio.ini +++ b/variants/xiao_nrf52/platformio.ini @@ -74,6 +74,7 @@ build_flags = -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 -D QSPIFLASH=1 + -D ENABLE_USB_INTERFACE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Xiao_nrf52.build_src_filter} diff --git a/variants/xiao_rp2040/platformio.ini b/variants/xiao_rp2040/platformio.ini index ca00e38b..63c6798d 100644 --- a/variants/xiao_rp2040/platformio.ini +++ b/variants/xiao_rp2040/platformio.ini @@ -61,6 +61,7 @@ extends = Xiao_rp2040 build_flags = ${Xiao_rp2040.build_flags} -D MAX_CONTACTS=100 -D MAX_GROUP_CHANNELS=8 + -D ENABLE_USB_INTERFACE ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${Xiao_rp2040.build_src_filter} diff --git a/variants/xiao_s3/platformio.ini b/variants/xiao_s3/platformio.ini index 22464e7d..1632d112 100644 --- a/variants/xiao_s3/platformio.ini +++ b/variants/xiao_s3/platformio.ini @@ -123,6 +123,7 @@ build_flags = -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=SSD1306Display -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_USB_INTERFACE ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 diff --git a/variants/xiao_s3_wio/platformio.ini b/variants/xiao_s3_wio/platformio.ini index db8c5a94..293a13c1 100644 --- a/variants/xiao_s3_wio/platformio.ini +++ b/variants/xiao_s3_wio/platformio.ini @@ -136,6 +136,7 @@ build_flags = -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=SSD1306Display -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_USB_INTERFACE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Xiao_S3_WIO.build_src_filter} From dd778e2189cf4fe6e3ba4623aea7d2c239ff2363 Mon Sep 17 00:00:00 2001 From: liamcottle Date: Tue, 28 Jul 2026 03:50:08 +1200 Subject: [PATCH 61/70] fix bluetooth toggle on ui tiny --- examples/companion_radio/ui-tiny/UITask.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/companion_radio/ui-tiny/UITask.cpp b/examples/companion_radio/ui-tiny/UITask.cpp index 5b9ebfd3..b6bdbcf4 100644 --- a/examples/companion_radio/ui-tiny/UITask.cpp +++ b/examples/companion_radio/ui-tiny/UITask.cpp @@ -240,7 +240,7 @@ public: } else if (_page == HomePage::BLUETOOTH) { display.setColor(UIColor::corp_blue); display.drawXbm((display.width() - 32) / 2, 8, - _task->isSerialEnabled() ? bluetooth_on : bluetooth_off, + _task->isBluetoothEnabled() ? bluetooth_on : bluetooth_off, 32, 32); display.setTextSize(1); // display.drawTextCentered(display.width() / 2, 40 - 11, "toggle: " PRESS_LABEL); @@ -391,10 +391,10 @@ public: return true; } if (c == KEY_ENTER && _page == HomePage::BLUETOOTH) { - if (_task->isSerialEnabled()) { // toggle Bluetooth on/off - _task->disableSerial(); + if (_task->isBluetoothEnabled()) { // toggle Bluetooth on/off + _task->disableBluetooth(); } else { - _task->enableSerial(); + _task->enableBluetooth(); } return true; } @@ -677,7 +677,7 @@ void UITask::loop() { _cached_batt_mv, isBuzzerQuiet(), getGPSState(), - isSerialEnabled()); + isBluetoothEnabled()); bool status_dirty = _statusBar.needsRedraw(); bool content_dirty = (millis() >= _next_refresh && curr); From 4050b7d5261a8ae52278b949ea2a43a68a052cfb Mon Sep 17 00:00:00 2001 From: entr0p1 <1475255+entr0p1@users.noreply.github.com> Date: Tue, 28 Jul 2026 10:14:59 +1000 Subject: [PATCH 62/70] nRF52840 "start ota" Failure Recovery If Bluefruit.begin fails, BLE OTA mode won't actually start and the board might require a reboot to reattempt. Fixes: - Bluefruit.begin returns false in NRF52Board.startOTAUpdate if OTA mode fails to start. User is notified of the fault through existing error message in CommonCLI and can reattempt "start ota" command. --- src/helpers/NRF52Board.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index 23f7cafc..b6c8fec5 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -396,7 +396,8 @@ bool NRF52Board::startOTAUpdate(const char *id, char reply[]) { Bluefruit.configPrphBandwidth(BANDWIDTH_MAX); Bluefruit.configPrphConn(92, BLE_GAP_EVENT_LENGTH_MIN, 16, 16); - Bluefruit.begin(1, 0); + if (!Bluefruit.begin(1, 0)) return false; + // Set max power. Accepted values are: -40, -30, -20, -16, -12, -8, -4, 0, 4 Bluefruit.setTxPower(4); // Set the BLE device name From 3a9f19c14b2537e77f5297ff5e3ab5578d84c266 Mon Sep 17 00:00:00 2001 From: Che177 Date: Mon, 27 Jul 2026 19:32:47 -0700 Subject: [PATCH 63/70] room_server: add room.post system posts --- examples/simple_room_server/MyMesh.cpp | 42 +++++++++++++++++++++++--- examples/simple_room_server/MyMesh.h | 2 ++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 7bf941a9..e7cd2d75 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -39,18 +39,43 @@ struct ServerStats { }; void MyMesh::addPost(ClientInfo *client, const char *postData) { - // TODO: suggested postData format: /<descrption> - posts[next_post_idx].author = client->id; // add to cyclic queue - StrHelper::strncpy(posts[next_post_idx].text, postData, MAX_POST_TEXT_LEN); + storePost(client->id, postData); +} - posts[next_post_idx].post_timestamp = getRTCClock()->getCurrentTimeUnique(); +void MyMesh::addSystemPost(const char *postData) { + if (!postData || postData[0] == 0) return; + +#if defined(ENABLE_ROOM_POST_DEBUG) && ENABLE_ROOM_POST_DEBUG == 1 + Serial.print("room.post: addSystemPost: "); Serial.println(postData); +#endif + + storePost(self_id, postData); +} + +void MyMesh::storePost(const mesh::Identity &author, const char *postData) { + int idx = next_post_idx; + // TODO: suggested postData format: <title>/<descrption> + posts[idx].author = author; // add to cyclic queue + StrHelper::strncpy(posts[idx].text, postData, MAX_POST_TEXT_LEN); + + posts[idx].post_timestamp = getRTCClock()->getCurrentTimeUnique(); +#if defined(ENABLE_ROOM_POST_DEBUG) && ENABLE_ROOM_POST_DEBUG == 1 + Serial.printf("room.post: storePost idx=%d text=%s\n", idx, posts[idx].text); + Serial.printf("room.post: timestamp=%u\n", posts[idx].post_timestamp); +#endif next_post_idx = (next_post_idx + 1) % MAX_UNSYNCED_POSTS; next_push = futureMillis(PUSH_NOTIFY_DELAY_MILLIS); _num_posted++; // stats +#if defined(ENABLE_ROOM_POST_DEBUG) && ENABLE_ROOM_POST_DEBUG == 1 + Serial.printf("room.post: next_post_idx=%d num_posted=%d push scheduled\n", next_post_idx, _num_posted); +#endif } void MyMesh::pushPostToClient(ClientInfo *client, PostInfo &post) { +#if defined(ENABLE_ROOM_POST_DEBUG) && ENABLE_ROOM_POST_DEBUG == 1 + Serial.print("room.post: pushPostToClient text="); Serial.println(post.text); +#endif int len = 0; memcpy(&reply_data[len], &post.post_timestamp, 4); len += 4; // this is a PAST timestamp... but should be accepted by client @@ -948,6 +973,15 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply Serial.printf("\n"); } reply[0] = 0; + } else if (strncmp(command, "room.post", 9) == 0) { + char* msg = command + 9; + while (*msg == ' ') msg++; + if (*msg == 0) { + snprintf(reply, MAX_POST_TEXT_LEN, "ERR empty message"); + } else { + addSystemPost(msg); + snprintf(reply, MAX_POST_TEXT_LEN, "OK"); + } } else{ _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands } diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index caef6920..5f78bee8 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -119,6 +119,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { int matching_peer_indexes[MAX_CLIENTS]; void addPost(ClientInfo* client, const char* postData); + void storePost(const mesh::Identity& author, const char* postData); void pushPostToClient(ClientInfo* client, PostInfo& post); uint8_t getUnsyncedCount(ClientInfo* client); bool processAck(const uint8_t *data); @@ -176,6 +177,7 @@ public: MyMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, mesh::RTCClock& rtc, mesh::MeshTables& tables); void begin(FILESYSTEM* fs); + void addSystemPost(const char* postData); const char* getFirmwareVer() override { return FIRMWARE_VERSION; } const char* getBuildDate() override { return FIRMWARE_BUILD_DATE; } From 5a04e060a59a986758f7d0b4619280598b7dc6d2 Mon Sep 17 00:00:00 2001 From: Che177 <github@kitagor.com> Date: Tue, 28 Jul 2026 21:57:42 -0700 Subject: [PATCH 64/70] room_server: use standard mesh debug logging --- examples/simple_room_server/MyMesh.cpp | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index e7cd2d75..0aff39cc 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -45,9 +45,7 @@ void MyMesh::addPost(ClientInfo *client, const char *postData) { void MyMesh::addSystemPost(const char *postData) { if (!postData || postData[0] == 0) return; -#if defined(ENABLE_ROOM_POST_DEBUG) && ENABLE_ROOM_POST_DEBUG == 1 - Serial.print("room.post: addSystemPost: "); Serial.println(postData); -#endif + MESH_DEBUG_PRINTLN("room.post: addSystemPost: %s", postData); storePost(self_id, postData); } @@ -59,23 +57,17 @@ void MyMesh::storePost(const mesh::Identity &author, const char *postData) { StrHelper::strncpy(posts[idx].text, postData, MAX_POST_TEXT_LEN); posts[idx].post_timestamp = getRTCClock()->getCurrentTimeUnique(); -#if defined(ENABLE_ROOM_POST_DEBUG) && ENABLE_ROOM_POST_DEBUG == 1 - Serial.printf("room.post: storePost idx=%d text=%s\n", idx, posts[idx].text); - Serial.printf("room.post: timestamp=%u\n", posts[idx].post_timestamp); -#endif + MESH_DEBUG_PRINTLN("room.post: storePost idx=%d text=%s", idx, posts[idx].text); + MESH_DEBUG_PRINTLN("room.post: timestamp=%u", posts[idx].post_timestamp); next_post_idx = (next_post_idx + 1) % MAX_UNSYNCED_POSTS; next_push = futureMillis(PUSH_NOTIFY_DELAY_MILLIS); _num_posted++; // stats -#if defined(ENABLE_ROOM_POST_DEBUG) && ENABLE_ROOM_POST_DEBUG == 1 - Serial.printf("room.post: next_post_idx=%d num_posted=%d push scheduled\n", next_post_idx, _num_posted); -#endif + MESH_DEBUG_PRINTLN("room.post: next_post_idx=%d num_posted=%d push scheduled", next_post_idx, _num_posted); } void MyMesh::pushPostToClient(ClientInfo *client, PostInfo &post) { -#if defined(ENABLE_ROOM_POST_DEBUG) && ENABLE_ROOM_POST_DEBUG == 1 - Serial.print("room.post: pushPostToClient text="); Serial.println(post.text); -#endif + MESH_DEBUG_PRINTLN("room.post: pushPostToClient text=%s", post.text); int len = 0; memcpy(&reply_data[len], &post.post_timestamp, 4); len += 4; // this is a PAST timestamp... but should be accepted by client From d7f378b489e9e298effb993af0700a3a21f974ad Mon Sep 17 00:00:00 2001 From: entr0p1 <1475255+entr0p1@users.noreply.github.com> Date: Wed, 29 Jul 2026 18:27:15 +1000 Subject: [PATCH 65/70] Clean up debugs left on in platform.ini files --- variants/meshadventurer/platformio.ini | 8 ++++---- variants/minewsemi_me25ls01/platformio.ini | 16 ++++++++-------- variants/muziworks_r1_neo/platformio.ini | 2 +- variants/promicro/platformio.ini | 2 +- variants/rak4631/platformio.ini | 2 +- variants/station_g2/platformio.ini | 4 ++-- variants/station_g3_esp32/platformio.ini | 2 +- variants/thinknode_m2/platformio.ini | 2 +- variants/thinknode_m3/platformio.ini | 2 +- variants/thinknode_m5/platformio.ini | 2 +- variants/thinknode_m7/platformio.ini | 2 +- variants/xiao_nrf52/platformio.ini | 2 +- variants/xiao_rp2040/platformio.ini | 4 ++-- 13 files changed, 25 insertions(+), 25 deletions(-) diff --git a/variants/meshadventurer/platformio.ini b/variants/meshadventurer/platformio.ini index f85be238..5def659c 100644 --- a/variants/meshadventurer/platformio.ini +++ b/variants/meshadventurer/platformio.ini @@ -209,8 +209,8 @@ build_flags = -D BLE_PIN_CODE=123456 -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=128 - -D MESH_PACKET_LOGGING=1 - -D MESH_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 lib_deps = ${Meshadventurer.lib_deps} densaugeo/base64 @ ~1.4.0 @@ -289,8 +289,8 @@ build_flags = -D BLE_PIN_CODE=123456 -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=128 - -D MESH_PACKET_LOGGING=1 - -D MESH_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 lib_deps = ${Meshadventurer.lib_deps} densaugeo/base64 @ ~1.4.0 diff --git a/variants/minewsemi_me25ls01/platformio.ini b/variants/minewsemi_me25ls01/platformio.ini index ac81fa16..97c6c11e 100644 --- a/variants/minewsemi_me25ls01/platformio.ini +++ b/variants/minewsemi_me25ls01/platformio.ini @@ -46,8 +46,8 @@ build_flags = ${me25ls01.build_flags} -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 ; -D BLE_DEBUG_LOGGING=1 - -D MESH_PACKET_LOGGING=1 - -D MESH_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 -D OFFLINE_QUEUE_SIZE=256 -D RX_BOOSTED_GAIN=true -D RF_SWITCH_TABLE @@ -67,8 +67,8 @@ build_flags = ${me25ls01.build_flags} -D MAX_GROUP_CHANNELS=8 -D BLE_PIN_CODE=123456 ; -D BLE_DEBUG_LOGGING=1 - -D MESH_PACKET_LOGGING=1 - -D MESH_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 -D OFFLINE_QUEUE_SIZE=256 -D RX_BOOSTED_GAIN=true -D RF_SWITCH_TABLE @@ -112,8 +112,8 @@ build_flags = ${me25ls01.build_flags} -D MAX_GROUP_CHANNELS=8 -D BLE_PIN_CODE=123456 ; -D BLE_DEBUG_LOGGING=1 - -D MESH_PACKET_LOGGING=1 - -D MESH_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 -D OFFLINE_QUEUE_SIZE=256 -D RX_BOOSTED_GAIN=true -D RF_SWITCH_TABLE @@ -138,8 +138,8 @@ build_flags = ${me25ls01.build_flags} -D MAX_GROUP_CHANNELS=40 ;-D BLE_PIN_CODE=123456 ; -D BLE_DEBUG_LOGGING=1 - -D MESH_PACKET_LOGGING=1 - -D MESH_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 -D OFFLINE_QUEUE_SIZE=256 -D RX_BOOSTED_GAIN=true -D RF_SWITCH_TABLE diff --git a/variants/muziworks_r1_neo/platformio.ini b/variants/muziworks_r1_neo/platformio.ini index 3dbecf1e..13795879 100644 --- a/variants/muziworks_r1_neo/platformio.ini +++ b/variants/muziworks_r1_neo/platformio.ini @@ -127,7 +127,7 @@ build_flags = -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' ; -D MESH_PACKET_LOGGING=1 - -D MESH_DEBUG=1 +; -D MESH_DEBUG=1 build_src_filter = ${R1Neo.build_src_filter} +<../examples/simple_sensor> diff --git a/variants/promicro/platformio.ini b/variants/promicro/platformio.ini index 5415e158..01fd8932 100644 --- a/variants/promicro/platformio.ini +++ b/variants/promicro/platformio.ini @@ -143,7 +143,7 @@ build_flags = ${Promicro.build_flags} -D OFFLINE_QUEUE_SIZE=256 -D DISPLAY_CLASS=SSD1306Display ; -D MESH_PACKET_LOGGING=1 - -D MESH_DEBUG=1 +; -D MESH_DEBUG=1 build_src_filter = ${Promicro.build_src_filter} +<helpers/nrf52/SerialBLEInterface.cpp> +<helpers/ui/SSD1306Display.cpp> diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 31b507b4..ea900c6c 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -254,7 +254,7 @@ build_flags = -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' ; -D MESH_PACKET_LOGGING=1 - -D MESH_DEBUG=1 +; -D MESH_DEBUG=1 build_src_filter = ${rak4631.build_src_filter} +<helpers/ui/SSD1306Display.cpp> +<../examples/simple_sensor> diff --git a/variants/station_g2/platformio.ini b/variants/station_g2/platformio.ini index 508ffe4b..ffbf387a 100644 --- a/variants/station_g2/platformio.ini +++ b/variants/station_g2/platformio.ini @@ -110,7 +110,7 @@ build_flags = -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' -D MAX_NEIGHBOURS=50 - -D MESH_PACKET_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 -D SX126X_RX_BOOSTED_GAIN=1 ; https://wiki.uniteng.com/en/meshtastic/station-g2#impact-of-lora-node-dense-areashigh-noise-environments-on-rf-performance ; -D MESH_DEBUG=1 @@ -152,7 +152,7 @@ build_flags = -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' -D MAX_NEIGHBOURS=50 - -D MESH_PACKET_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 -D SX126X_RX_BOOSTED_GAIN=1 -D WITH_ESPNOW_BRIDGE=1 ; -D BRIDGE_DEBUG=1 diff --git a/variants/station_g3_esp32/platformio.ini b/variants/station_g3_esp32/platformio.ini index b29388ef..71bad18b 100644 --- a/variants/station_g3_esp32/platformio.ini +++ b/variants/station_g3_esp32/platformio.ini @@ -71,7 +71,7 @@ build_flags = -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' -D MAX_NEIGHBOURS=50 - -D MESH_PACKET_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Station_G3_ESP32.build_src_filter} +<../examples/simple_repeater> diff --git a/variants/thinknode_m2/platformio.ini b/variants/thinknode_m2/platformio.ini index aae83324..cc9f7688 100644 --- a/variants/thinknode_m2/platformio.ini +++ b/variants/thinknode_m2/platformio.ini @@ -30,7 +30,7 @@ build_flags = ${esp32_base.build_flags} -D WRAPPER_CLASS=CustomSX1262Wrapper -D LORA_TX_POWER=22 -D SX126X_RX_BOOSTED_GAIN=1 - -D MESH_DEBUG=1 +; -D MESH_DEBUG=1 build_src_filter = ${esp32_base.build_src_filter} +<helpers/ui/SH1106Display.cpp> +<helpers/ui/MomentaryButton.cpp> diff --git a/variants/thinknode_m3/platformio.ini b/variants/thinknode_m3/platformio.ini index f80a6161..39eaed84 100644 --- a/variants/thinknode_m3/platformio.ini +++ b/variants/thinknode_m3/platformio.ini @@ -26,7 +26,7 @@ build_flags = ${nrf52_base.build_flags} -D P_LORA_TX_LED=PIN_LED_BLUE -D LR11X0_DIO_AS_RF_SWITCH=true -D LR11X0_DIO3_TCXO_VOLTAGE=3.3 - -D MESH_DEBUG=1 +; -D MESH_DEBUG=1 -D ENV_INCLUDE_GPS=1 build_src_filter = ${nrf52_base.build_src_filter} +<helpers/*.cpp> diff --git a/variants/thinknode_m5/platformio.ini b/variants/thinknode_m5/platformio.ini index 8572d1eb..cdbcceef 100644 --- a/variants/thinknode_m5/platformio.ini +++ b/variants/thinknode_m5/platformio.ini @@ -37,7 +37,7 @@ build_flags = ${esp32_base.build_flags} -D WRAPPER_CLASS=CustomSX1262Wrapper -D LORA_TX_POWER=22 -D SX126X_RX_BOOSTED_GAIN=1 - -D MESH_DEBUG=1 +; -D MESH_DEBUG=1 -D ENV_INCLUDE_GPS=1 -D PERSISTANT_GPS=1 -D ENV_SKIP_GPS_DETECT=1 diff --git a/variants/thinknode_m7/platformio.ini b/variants/thinknode_m7/platformio.ini index acd98c8c..c4bed017 100644 --- a/variants/thinknode_m7/platformio.ini +++ b/variants/thinknode_m7/platformio.ini @@ -119,7 +119,7 @@ build_flags = -D WIFI_SSID='"myssid"' -D WIFI_PWD='"mypwd"' -D OFFLINE_QUEUE_SIZE=256 - -D MESH_PACKET_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 build_src_filter = ${ThinkNode_M7.build_src_filter} +<helpers/esp32/*.cpp> +<helpers/ui/MomentaryButton.cpp> diff --git a/variants/xiao_nrf52/platformio.ini b/variants/xiao_nrf52/platformio.ini index a0854336..56e27cbb 100644 --- a/variants/xiao_nrf52/platformio.ini +++ b/variants/xiao_nrf52/platformio.ini @@ -53,7 +53,7 @@ build_flags = -D OFFLINE_QUEUE_SIZE=256 ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 - -D MESH_DEBUG=1 +; -D MESH_DEBUG=1 -D QSPIFLASH=1 build_src_filter = ${Xiao_nrf52.build_src_filter} +<helpers/nrf52/SerialBLEInterface.cpp> diff --git a/variants/xiao_rp2040/platformio.ini b/variants/xiao_rp2040/platformio.ini index ca00e38b..62d36005 100644 --- a/variants/xiao_rp2040/platformio.ini +++ b/variants/xiao_rp2040/platformio.ini @@ -38,8 +38,8 @@ build_flags = ${Xiao_rp2040.build_flags} -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' -D MAX_NEIGHBOURS=50 - -D MESH_PACKET_LOGGING=1 - -D MESH_DEBUG=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 build_src_filter = ${Xiao_rp2040.build_src_filter} +<../examples/simple_repeater> From 20f55a4f42d11b2859734bab9db2224cc2e3a6fa Mon Sep 17 00:00:00 2001 From: taco <taco@sly.nu> Date: Wed, 29 Jul 2026 16:10:15 +1000 Subject: [PATCH 66/70] fix unnecessary IRQ clear --- src/helpers/radiolib/CustomSX1262.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/helpers/radiolib/CustomSX1262.h b/src/helpers/radiolib/CustomSX1262.h index 6a7c2144..b4ee6c97 100644 --- a/src/helpers/radiolib/CustomSX1262.h +++ b/src/helpers/radiolib/CustomSX1262.h @@ -117,6 +117,12 @@ class CustomSX1262 : public SX1262 { _headerSeen = false; return false; } + if (!header && _headerSeen) { + // something cleared the header flag, reset our state. + _activityAt = 0; _headerSeen = false; + return false; + } + if (header) { if (!_headerSeen) { _headerSeen = true; _activityAt = now; }; if (now - _activityAt > _maxPayloadMillis) { From a7426de0bf938c089531e5e2b2177d989fbc07e7 Mon Sep 17 00:00:00 2001 From: taco <taco@sly.nu> Date: Wed, 29 Jul 2026 23:18:15 +1000 Subject: [PATCH 67/70] LR1110: fix unnecessary IRQ clear --- src/helpers/radiolib/CustomLR1110.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/helpers/radiolib/CustomLR1110.h b/src/helpers/radiolib/CustomLR1110.h index 20aea9ff..c481cf5e 100644 --- a/src/helpers/radiolib/CustomLR1110.h +++ b/src/helpers/radiolib/CustomLR1110.h @@ -52,6 +52,11 @@ class CustomLR1110 : public LR1110 { _headerSeen = false; return false; } + if (!header && _headerSeen) { + // something cleared the header flag, reset our state. + _activityAt = 0; _headerSeen = false; + return false; + } if (header) { if (!_headerSeen) { _headerSeen = true; _activityAt = now; }; if (now - _activityAt > _maxPayloadMillis) { From a5c323cc1939dd3b2b8789c19f0c258ac2adb257 Mon Sep 17 00:00:00 2001 From: taco <taco@sly.nu> Date: Wed, 29 Jul 2026 23:19:40 +1000 Subject: [PATCH 68/70] STM32WLx: add IRQ timeout logic --- src/helpers/radiolib/CustomSTM32WLx.h | 58 +++++++++++++++++++- src/helpers/radiolib/CustomSTM32WLxWrapper.h | 3 + 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/helpers/radiolib/CustomSTM32WLx.h b/src/helpers/radiolib/CustomSTM32WLx.h index 01190d06..e312a5ee 100644 --- a/src/helpers/radiolib/CustomSTM32WLx.h +++ b/src/helpers/radiolib/CustomSTM32WLx.h @@ -3,6 +3,11 @@ #include <RadioLib.h> class CustomSTM32WLx : public STM32WLx { + uint32_t _preambleMillis = 66; + uint32_t _maxPayloadMillis = 3934; + uint32_t _activityAt = 0; + bool _headerSeen = false; + public: CustomSTM32WLx(STM32WLx_Module *mod) : STM32WLx(mod) { } @@ -12,8 +17,55 @@ class CustomSTM32WLx : public STM32WLx { } bool isReceiving() { - uint16_t irq = getIrqFlags(); - bool detected = (irq & RADIOLIB_SX126X_IRQ_HEADER_VALID) || (irq & RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED); - return detected; + uint32_t irq = getIrqFlags(); + bool preamble = irq & RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED; // bit 2 + bool header = irq & RADIOLIB_SX126X_IRQ_HEADER_VALID; // bit 4 + bool hdrErr = irq & RADIOLIB_SX126X_IRQ_HEADER_ERR; // bit 5 + uint32_t now = millis(); + if (hdrErr) { + clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED | RADIOLIB_SX126X_IRQ_HEADER_VALID | RADIOLIB_SX126X_IRQ_HEADER_ERR | RADIOLIB_SX126X_IRQ_SYNC_WORD_VALID); + _activityAt = 0; + _headerSeen = false; + return false; + } + if (!header && _headerSeen) { + // something cleared the header flag, reset our state. + _activityAt = 0; _headerSeen = false; + return false; + } + + if (header) { + if (!_headerSeen) { _headerSeen = true; _activityAt = now; }; + if (now - _activityAt > _maxPayloadMillis) { + MESH_DEBUG_PRINTLN("Clearing header IRQ after %ums", _maxPayloadMillis); + clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED | RADIOLIB_SX126X_IRQ_HEADER_VALID | RADIOLIB_SX126X_IRQ_HEADER_ERR | RADIOLIB_SX126X_IRQ_SYNC_WORD_VALID); + _activityAt = 0; _headerSeen = false; + return false; + } + return true; + } + if (preamble) { + if (_activityAt == 0) _activityAt = now; + if (now - _activityAt > _preambleMillis) { + clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED); + _activityAt = 0; + MESH_DEBUG_PRINTLN("Clearing preamble IRQ after %ums", _preambleMillis); + + return false; + } + return true; + } + _activityAt = 0; _headerSeen = false; + return false; } + + void setPreambleMillis(uint32_t preambleMillis) { + _preambleMillis = preambleMillis; + MESH_DEBUG_PRINTLN("Set _preambleMillis=%u", _preambleMillis); + } + void setMaxPayloadMillis(uint32_t payloadMillis) { + _maxPayloadMillis = payloadMillis; + MESH_DEBUG_PRINTLN("Set _maxPayloadMillis=%u", _maxPayloadMillis); + } + }; \ No newline at end of file diff --git a/src/helpers/radiolib/CustomSTM32WLxWrapper.h b/src/helpers/radiolib/CustomSTM32WLxWrapper.h index 97bf6820..a792a877 100644 --- a/src/helpers/radiolib/CustomSTM32WLxWrapper.h +++ b/src/helpers/radiolib/CustomSTM32WLxWrapper.h @@ -15,6 +15,9 @@ public: ((CustomSTM32WLx *)_radio)->setBandwidth(bw); ((CustomSTM32WLx *)_radio)->setCodingRate(cr); updatePreamble(sf); + PacketMillis pm = calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf)); + ((CustomSTM32WLx *)_radio)->setPreambleMillis(pm.preambleMillis); + ((CustomSTM32WLx *)_radio)->setMaxPayloadMillis(pm.payloadMillis); } bool isReceivingPacket() override { From a3732e1c65433a0e2bdbd825c58c908a36091699 Mon Sep 17 00:00:00 2001 From: taco <taco@sly.nu> Date: Wed, 29 Jul 2026 23:29:37 +1000 Subject: [PATCH 69/70] SX1268: add IRQ timeout logic --- src/helpers/radiolib/CustomSX1268.h | 58 ++++++++++++++++++++-- src/helpers/radiolib/CustomSX1268Wrapper.h | 3 ++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/helpers/radiolib/CustomSX1268.h b/src/helpers/radiolib/CustomSX1268.h index 1e71a3fa..f915332d 100644 --- a/src/helpers/radiolib/CustomSX1268.h +++ b/src/helpers/radiolib/CustomSX1268.h @@ -3,6 +3,11 @@ #include <RadioLib.h> class CustomSX1268 : public SX1268 { + uint32_t _preambleMillis = 66; + uint32_t _maxPayloadMillis = 3934; + uint32_t _activityAt = 0; + bool _headerSeen = false; + public: CustomSX1268(Module *mod) : SX1268(mod) { } @@ -81,10 +86,57 @@ class CustomSX1268 : public SX1268 { } bool isReceiving() { - uint16_t irq = getIrqFlags(); - bool detected = (irq & RADIOLIB_SX126X_IRQ_HEADER_VALID) || (irq & RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED); - return detected; + uint32_t irq = getIrqFlags(); + bool preamble = irq & RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED; // bit 2 + bool header = irq & RADIOLIB_SX126X_IRQ_HEADER_VALID; // bit 4 + bool hdrErr = irq & RADIOLIB_SX126X_IRQ_HEADER_ERR; // bit 5 + uint32_t now = millis(); + if (hdrErr) { + clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED | RADIOLIB_SX126X_IRQ_HEADER_VALID | RADIOLIB_SX126X_IRQ_HEADER_ERR | RADIOLIB_SX126X_IRQ_SYNC_WORD_VALID); + _activityAt = 0; + _headerSeen = false; + return false; + } + if (!header && _headerSeen) { + // something cleared the header flag, reset our state. + _activityAt = 0; _headerSeen = false; + return false; + } + + if (header) { + if (!_headerSeen) { _headerSeen = true; _activityAt = now; }; + if (now - _activityAt > _maxPayloadMillis) { + MESH_DEBUG_PRINTLN("Clearing header IRQ after %ums", _maxPayloadMillis); + clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED | RADIOLIB_SX126X_IRQ_HEADER_VALID | RADIOLIB_SX126X_IRQ_HEADER_ERR | RADIOLIB_SX126X_IRQ_SYNC_WORD_VALID); + _activityAt = 0; _headerSeen = false; + return false; + } + return true; + } + if (preamble) { + if (_activityAt == 0) _activityAt = now; + if (now - _activityAt > _preambleMillis) { + clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED); + _activityAt = 0; + MESH_DEBUG_PRINTLN("Clearing preamble IRQ after %ums", _preambleMillis); + + return false; + } + return true; + } + _activityAt = 0; _headerSeen = false; + return false; } + + void setPreambleMillis(uint32_t preambleMillis) { + _preambleMillis = preambleMillis; + MESH_DEBUG_PRINTLN("Set _preambleMillis=%u", _preambleMillis); + } + void setMaxPayloadMillis(uint32_t payloadMillis) { + _maxPayloadMillis = payloadMillis; + MESH_DEBUG_PRINTLN("Set _maxPayloadMillis=%u", _maxPayloadMillis); + } + bool getRxBoostedGainMode() { uint8_t rxGain = 0; diff --git a/src/helpers/radiolib/CustomSX1268Wrapper.h b/src/helpers/radiolib/CustomSX1268Wrapper.h index bce56b99..104ba08b 100644 --- a/src/helpers/radiolib/CustomSX1268Wrapper.h +++ b/src/helpers/radiolib/CustomSX1268Wrapper.h @@ -18,6 +18,9 @@ public: ((CustomSX1268 *)_radio)->setBandwidth(bw); ((CustomSX1268 *)_radio)->setCodingRate(cr); updatePreamble(sf); + PacketMillis pm = calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf)); + ((CustomSX1268 *)_radio)->setPreambleMillis(pm.preambleMillis); + ((CustomSX1268 *)_radio)->setMaxPayloadMillis(pm.payloadMillis); } bool isReceivingPacket() override { From 79eb7f5d8dd223bf19303099dfff3adbd54205b3 Mon Sep 17 00:00:00 2001 From: taco <taco@sly.nu> Date: Wed, 29 Jul 2026 23:33:24 +1000 Subject: [PATCH 70/70] LLCC68: add IRQ timeout logic --- src/helpers/radiolib/CustomLLCC68.h | 57 ++++++++++++++++++++-- src/helpers/radiolib/CustomLLCC68Wrapper.h | 4 ++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/helpers/radiolib/CustomLLCC68.h b/src/helpers/radiolib/CustomLLCC68.h index bde44b6c..1dcd916f 100644 --- a/src/helpers/radiolib/CustomLLCC68.h +++ b/src/helpers/radiolib/CustomLLCC68.h @@ -3,6 +3,11 @@ #include <RadioLib.h> class CustomLLCC68 : public LLCC68 { + uint32_t _preambleMillis = 66; + uint32_t _maxPayloadMillis = 3934; + uint32_t _activityAt = 0; + bool _headerSeen = false; + public: CustomLLCC68(Module *mod) : LLCC68(mod) { } @@ -81,9 +86,55 @@ class CustomLLCC68 : public LLCC68 { } bool isReceiving() { - uint16_t irq = getIrqFlags(); - bool detected = (irq & RADIOLIB_SX126X_IRQ_HEADER_VALID) || (irq & RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED); - return detected; + uint32_t irq = getIrqFlags(); + bool preamble = irq & RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED; // bit 2 + bool header = irq & RADIOLIB_SX126X_IRQ_HEADER_VALID; // bit 4 + bool hdrErr = irq & RADIOLIB_SX126X_IRQ_HEADER_ERR; // bit 5 + uint32_t now = millis(); + if (hdrErr) { + clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED | RADIOLIB_SX126X_IRQ_HEADER_VALID | RADIOLIB_SX126X_IRQ_HEADER_ERR | RADIOLIB_SX126X_IRQ_SYNC_WORD_VALID); + _activityAt = 0; + _headerSeen = false; + return false; + } + if (!header && _headerSeen) { + // something cleared the header flag, reset our state. + _activityAt = 0; _headerSeen = false; + return false; + } + + if (header) { + if (!_headerSeen) { _headerSeen = true; _activityAt = now; }; + if (now - _activityAt > _maxPayloadMillis) { + MESH_DEBUG_PRINTLN("Clearing header IRQ after %ums", _maxPayloadMillis); + clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED | RADIOLIB_SX126X_IRQ_HEADER_VALID | RADIOLIB_SX126X_IRQ_HEADER_ERR | RADIOLIB_SX126X_IRQ_SYNC_WORD_VALID); + _activityAt = 0; _headerSeen = false; + return false; + } + return true; + } + if (preamble) { + if (_activityAt == 0) _activityAt = now; + if (now - _activityAt > _preambleMillis) { + clearIrqFlags(RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED); + _activityAt = 0; + MESH_DEBUG_PRINTLN("Clearing preamble IRQ after %ums", _preambleMillis); + + return false; + } + return true; + } + _activityAt = 0; _headerSeen = false; + return false; + } + + void setPreambleMillis(uint32_t preambleMillis) { + _preambleMillis = preambleMillis; + MESH_DEBUG_PRINTLN("Set _preambleMillis=%u", _preambleMillis); + } + void setMaxPayloadMillis(uint32_t payloadMillis) { + _maxPayloadMillis = payloadMillis; + MESH_DEBUG_PRINTLN("Set _maxPayloadMillis=%u", _maxPayloadMillis); } bool getRxBoostedGainMode() { diff --git a/src/helpers/radiolib/CustomLLCC68Wrapper.h b/src/helpers/radiolib/CustomLLCC68Wrapper.h index 851fd644..ae0fe0a2 100644 --- a/src/helpers/radiolib/CustomLLCC68Wrapper.h +++ b/src/helpers/radiolib/CustomLLCC68Wrapper.h @@ -14,6 +14,10 @@ public: ((CustomLLCC68 *)_radio)->setBandwidth(bw); ((CustomLLCC68 *)_radio)->setCodingRate(cr); updatePreamble(sf); + PacketMillis pm = calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf)); + ((CustomLLCC68 *)_radio)->setPreambleMillis(pm.preambleMillis); + ((CustomLLCC68 *)_radio)->setMaxPayloadMillis(pm.payloadMillis); + } bool isReceivingPacket() override {