From 073c35c782447f3749bd624c2a6a94959d74b7bc Mon Sep 17 00:00:00 2001 From: Tom Fifield Date: Fri, 17 Oct 2025 09:01:51 +1100 Subject: [PATCH 01/14] Update exempt labels for stale bot workflow Adds triaged and backlog to the list of exempt labels. --- .github/workflows/stale_bot.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/stale_bot.yml b/.github/workflows/stale_bot.yml index a80619e90..441d53ee3 100644 --- a/.github/workflows/stale_bot.yml +++ b/.github/workflows/stale_bot.yml @@ -20,5 +20,5 @@ jobs: uses: actions/stale@v10.1.0 with: days-before-stale: 45 - exempt-issue-labels: pinned,3.0 - exempt-pr-labels: pinned,3.0 + exempt-issue-labels: pinned,3.0,triaged,backlog + exempt-pr-labels: pinned,3.0,triaged,backlog From acab814b6f3070f039f231804b304545e790c28c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 18 Oct 2025 09:14:34 +1100 Subject: [PATCH 02/14] Update meshtastic/web to v2.6.7 (#8381) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- bin/web.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/web.version b/bin/web.version index 952f449f1..ba5c9fca6 100644 --- a/bin/web.version +++ b/bin/web.version @@ -1 +1 @@ -2.6.6 \ No newline at end of file +2.6.7 \ No newline at end of file From d9905f3c316f37f9d95bb6b4cfb2579eb08becce Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 18 Oct 2025 19:17:34 +1100 Subject: [PATCH 03/14] Update DFRobot_RTU to v1.0.6 (#8387) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 376f6e5a8..7c63ad7ad 100644 --- a/platformio.ini +++ b/platformio.ini @@ -164,7 +164,7 @@ lib_deps = # renovate: datasource=custom.pio depName=QMC5883L Compass packageName=mprograms/library/QMC5883LCompass mprograms/QMC5883LCompass@1.2.3 # renovate: datasource=custom.pio depName=DFRobot_RTU packageName=dfrobot/library/DFRobot_RTU - dfrobot/DFRobot_RTU@1.0.3 + dfrobot/DFRobot_RTU@1.0.6 # renovate: datasource=git-refs depName=DFRobot_RainfallSensor packageName=https://github.com/DFRobot/DFRobot_RainfallSensor gitBranch=master https://github.com/DFRobot/DFRobot_RainfallSensor/archive/38fea5e02b40a5430be6dab39a99a6f6347d667e.zip # renovate: datasource=custom.pio depName=INA226 packageName=robtillaart/library/INA226 From 0bfc342b4832bcdef0463c7272733aa4f878763d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 18 Oct 2025 19:20:07 +1100 Subject: [PATCH 04/14] Update mcr.microsoft.com/devcontainers/cpp Docker tag to v2 (#8375) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .devcontainer/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 30af24bd2..a2c56fad9 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,7 +1,7 @@ # trunk-ignore-all(terrascan/AC_DOCKER_0002): Known terrascan issue # trunk-ignore-all(hadolint/DL3008): Do not pin apt package versions # trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions -FROM mcr.microsoft.com/devcontainers/cpp:1-debian-12 +FROM mcr.microsoft.com/devcontainers/cpp:2-debian-12 USER root From 4df79374b0e220918eaa9c35f4a004653125afd6 Mon Sep 17 00:00:00 2001 From: Tom Fifield Date: Sun, 19 Oct 2025 08:52:03 +1100 Subject: [PATCH 05/14] manual merge stale bot config (#8392) --- .github/workflows/stale_bot.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/stale_bot.yml b/.github/workflows/stale_bot.yml index 441d53ee3..11ba59386 100644 --- a/.github/workflows/stale_bot.yml +++ b/.github/workflows/stale_bot.yml @@ -20,5 +20,7 @@ jobs: uses: actions/stale@v10.1.0 with: days-before-stale: 45 + stale-issue-message: This issue has not had any comment or update in the last month. If it is still relevant, please post update comments. If no comments are made, this issue will be closed automagically in 7 days. + close-issue-message: This issue has not had any comment since the last notice. It has been closed automatically. If this is incorrect, or the issue becomes relevant again, please request that it is reopened. exempt-issue-labels: pinned,3.0,triaged,backlog exempt-pr-labels: pinned,3.0,triaged,backlog From e5d67310d6c6d76881c087b23c243ed6db70803b Mon Sep 17 00:00:00 2001 From: Tom Fifield Date: Sun, 19 Oct 2025 08:52:56 +1100 Subject: [PATCH 06/14] Master ---> Develop (#8391) * Update exempt labels for stale bot workflow Adds triaged and backlog to the list of exempt labels. * Update meshtastic/web to v2.6.7 (#8381) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update DFRobot_RTU to v1.0.6 (#8387) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update mcr.microsoft.com/devcontainers/cpp Docker tag to v2 (#8375) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * manual merge stale bot config (#8392) --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .devcontainer/Dockerfile | 2 +- bin/web.version | 2 +- platformio.ini | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 30af24bd2..a2c56fad9 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,7 +1,7 @@ # trunk-ignore-all(terrascan/AC_DOCKER_0002): Known terrascan issue # trunk-ignore-all(hadolint/DL3008): Do not pin apt package versions # trunk-ignore-all(hadolint/DL3013): Do not pin pip package versions -FROM mcr.microsoft.com/devcontainers/cpp:1-debian-12 +FROM mcr.microsoft.com/devcontainers/cpp:2-debian-12 USER root diff --git a/bin/web.version b/bin/web.version index 952f449f1..ba5c9fca6 100644 --- a/bin/web.version +++ b/bin/web.version @@ -1 +1 @@ -2.6.6 \ No newline at end of file +2.6.7 \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index 376f6e5a8..7c63ad7ad 100644 --- a/platformio.ini +++ b/platformio.ini @@ -164,7 +164,7 @@ lib_deps = # renovate: datasource=custom.pio depName=QMC5883L Compass packageName=mprograms/library/QMC5883LCompass mprograms/QMC5883LCompass@1.2.3 # renovate: datasource=custom.pio depName=DFRobot_RTU packageName=dfrobot/library/DFRobot_RTU - dfrobot/DFRobot_RTU@1.0.3 + dfrobot/DFRobot_RTU@1.0.6 # renovate: datasource=git-refs depName=DFRobot_RainfallSensor packageName=https://github.com/DFRobot/DFRobot_RainfallSensor gitBranch=master https://github.com/DFRobot/DFRobot_RainfallSensor/archive/38fea5e02b40a5430be6dab39a99a6f6347d667e.zip # renovate: datasource=custom.pio depName=INA226 packageName=robtillaart/library/INA226 From af8407aca9476398de49db15e723088de257275f Mon Sep 17 00:00:00 2001 From: "Daniel.Cao" <144674500+DanielCao0@users.noreply.github.com> Date: Sun, 19 Oct 2025 05:53:39 +0800 Subject: [PATCH 07/14] Board support: RAK3401+RAK13302 1-watt (#8140) * Add RAK3401 variant files * Add SPI configuration for RAK3401 and RAK13302 variants * Refactor SPI pin configuration and clean up variant definitions for RAK3401 * Add TX_GAIN_LORA for RAK13302 Power Amp * Fix merge --------- Co-authored-by: Ben Meadors Co-authored-by: Tom Fifield Co-authored-by: Manuel <71137295+mverch67@users.noreply.github.com> --- src/configuration.h | 5 + src/main.cpp | 7 + .../nrf52840/rak3401_1watt/platformio.ini | 31 +++ variants/nrf52840/rak3401_1watt/variant.cpp | 45 ++++ variants/nrf52840/rak3401_1watt/variant.h | 226 ++++++++++++++++++ 5 files changed, 314 insertions(+) create mode 100644 variants/nrf52840/rak3401_1watt/platformio.ini create mode 100644 variants/nrf52840/rak3401_1watt/variant.cpp create mode 100644 variants/nrf52840/rak3401_1watt/variant.h diff --git a/src/configuration.h b/src/configuration.h index c6c8d673c..baf24a636 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -126,6 +126,11 @@ along with this program. If not, see . #define TX_GAIN_LORA 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 9, 8, 7 #endif +#ifdef RAK13302 +#define NUM_PA_POINTS 22 +#define TX_GAIN_LORA 7, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 8 +#endif + // Default system gain to 0 if not defined #ifndef TX_GAIN_LORA #define TX_GAIN_LORA 0 diff --git a/src/main.cpp b/src/main.cpp index bb97a1aa6..3801f6f9f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -841,7 +841,14 @@ void setup() SPI.begin(); } #elif !defined(ARCH_ESP32) // ARCH_RP2040 +#if defined(RAK3401) || defined(RAK13302) + pinMode(WB_IO2, OUTPUT); + digitalWrite(WB_IO2, HIGH); + SPI1.setPins(LORA_MISO, LORA_SCK, LORA_MOSI); + SPI1.begin(); +#else SPI.begin(); +#endif #else // ESP32 #if defined(HW_SPI1_DEVICE) diff --git a/variants/nrf52840/rak3401_1watt/platformio.ini b/variants/nrf52840/rak3401_1watt/platformio.ini new file mode 100644 index 000000000..1a915a6b3 --- /dev/null +++ b/variants/nrf52840/rak3401_1watt/platformio.ini @@ -0,0 +1,31 @@ +; The very slick RAK wireless RAK 4631 / 4630 board - Unified firmware for 5005/19003, with or without OLED RAK 1921 +[env:rak3401-1watt] +extends = nrf52840_base +board = wiscore_rak4631 +board_check = true +build_flags = ${nrf52840_base.build_flags} + -Ivariants/nrf52840/rak3401_1watt + -D RAK_4631 +; -DGPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. + -D RAK3401 + -D RAK13302 ; RAK 1Watt Power Amplifier + -DRADIOLIB_EXCLUDE_SX128X=1 + -DRADIOLIB_EXCLUDE_SX127X=1 + -DRADIOLIB_EXCLUDE_LR11X0=1 +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak3401_1watt> + +lib_deps = + ${nrf52840_base.lib_deps} + ${networking_base.lib_deps} + melopero/Melopero RV3028@^1.1.0 + rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2 + beegee-tokyo/RAK12035_SoilMoisture@^1.0.4 + https://github.com/RAKWireless/RAK12034-BMX160/archive/dcead07ffa267d3c906e9ca4a1330ab989e957e2.zip + +; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) +; Note: as of 6/2013 the serial/bootloader based programming takes approximately 30 seconds +;upload_protocol = jlink + +; Allows programming and debug via the RAK NanoDAP as the default debugger tool for the RAK4631 (it is only $10!) +; programming time is about the same as the bootloader version. +; For information on this see the meshtastic developers documentation for "Development on the NRF52" + diff --git a/variants/nrf52840/rak3401_1watt/variant.cpp b/variants/nrf52840/rak3401_1watt/variant.cpp new file mode 100644 index 000000000..e84b60b3b --- /dev/null +++ b/variants/nrf52840/rak3401_1watt/variant.cpp @@ -0,0 +1,45 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + + // P1 + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; + +void initVariant() +{ + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); + + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); + + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); +} diff --git a/variants/nrf52840/rak3401_1watt/variant.h b/variants/nrf52840/rak3401_1watt/variant.h new file mode 100644 index 000000000..d4bb1a175 --- /dev/null +++ b/variants/nrf52840/rak3401_1watt/variant.h @@ -0,0 +1,226 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef _VARIANT_RAK3401_ +#define _VARIANT_RAK3401_ + +#define RAK4630 + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF +// define USE_LFRC // Board uses RC for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED1 (35) +#define PIN_LED2 (36) + +#define LED_BUILTIN PIN_LED1 +#define LED_CONN PIN_LED2 + +#define LED_GREEN PIN_LED1 +#define LED_BLUE PIN_LED2 + +#define LED_STATE_ON 1 // State when LED is litted + +/* + * Analog pins + */ +#define PIN_A0 (5) +#define PIN_A1 (31) +#define PIN_A2 (28) +#define PIN_A3 (29) +#define PIN_A4 (30) +#define PIN_A5 (31) +#define PIN_A6 (0xff) +#define PIN_A7 (0xff) + +static const uint8_t A0 = PIN_A0; +static const uint8_t A1 = PIN_A1; +static const uint8_t A2 = PIN_A2; +static const uint8_t A3 = PIN_A3; +static const uint8_t A4 = PIN_A4; +static const uint8_t A5 = PIN_A5; +static const uint8_t A6 = PIN_A6; +static const uint8_t A7 = PIN_A7; +#define ADC_RESOLUTION 14 + +// Other pins +#define WB_I2C1_SDA (13) // SENSOR_SLOT IO_SLOT +#define WB_I2C1_SCL (14) // SENSOR_SLOT IO_SLOT + +#define PIN_AREF (2) +#define PIN_NFC1 (9) +#define WB_IO5 PIN_NFC1 +#define WB_IO4 (4) +#define PIN_NFC2 (10) + +static const uint8_t AREF = PIN_AREF; + +/* + * Serial interfaces + */ +#define PIN_SERIAL1_RX (15) +#define PIN_SERIAL1_TX (16) + +// Connected to Jlink CDC +#define PIN_SERIAL2_RX (8) +#define PIN_SERIAL2_TX (6) + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 2 + +#define PIN_SPI_MISO (45) +#define PIN_SPI_MOSI (44) +#define PIN_SPI_SCK (43) + +#define PIN_SPI1_MISO (29) // (0 + 29) +#define PIN_SPI1_MOSI (30) // (0 + 30) +#define PIN_SPI1_SCK (3) // (0 + 3) + +static const uint8_t SS = 42; +static const uint8_t MOSI = PIN_SPI_MOSI; +static const uint8_t MISO = PIN_SPI_MISO; +static const uint8_t SCK = PIN_SPI_SCK; + +/* + * eink display pins + */ +#define PIN_EINK_CS (0 + 26) +#define PIN_EINK_BUSY (0 + 4) +#define PIN_EINK_DC (0 + 17) +#define PIN_EINK_RES (-1) +#define PIN_EINK_SCLK (0 + 3) +#define PIN_EINK_MOSI (0 + 30) // also called SDI + +/* + * Wire Interfaces + */ +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (WB_I2C1_SDA) +#define PIN_WIRE_SCL (WB_I2C1_SCL) + +// QSPI Pins +#define PIN_QSPI_SCK 3 +#define PIN_QSPI_CS 26 +#define PIN_QSPI_IO0 30 +#define PIN_QSPI_IO1 29 +#define PIN_QSPI_IO2 28 +#define PIN_QSPI_IO3 2 + +// On-board QSPI Flash +#define EXTERNAL_FLASH_DEVICES IS25LP080D +#define EXTERNAL_FLASH_USE_QSPI + +// 1watt sx1262 RAK13302 +#define HW_SPI1_DEVICE 1 + +#define LORA_SCK PIN_SPI1_SCK +#define LORA_MISO PIN_SPI1_MISO +#define LORA_MOSI PIN_SPI1_MOSI +#define LORA_CS 26 + +#define USE_SX1262 +#define SX126X_CS (26) +#define SX126X_DIO1 (10) +#define SX126X_BUSY (9) +#define SX126X_RESET (4) + +#define SX126X_POWER_EN (21) +// DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3 +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +// Testing USB detection +#define NRF_APM +// If using a power chip like the INA3221 you can override the default battery voltage channel below +// and comment out NRF_APM to use the INA3221 instead of the USB detection for charging +// #define INA3221_BAT_CH INA3221_CH2 +// #define INA3221_ENV_CH INA3221_CH1 + +// enables 3.3V periphery like GPS or IO Module +// Do not toggle this for GPS power savings +#define PIN_3V3_EN (34) +#define WB_IO2 PIN_3V3_EN + +// RAK1910 GPS module +// If using the wisblock GPS module and pluged into Port A on WisBlock base +// IO1 is hooked to PPS (pin 12 on header) = gpio 17 +// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power (1 is on). +// Therefore must be 1 to keep peripherals powered +// Power is on the controllable 3V3_S rail +// #define PIN_GPS_RESET (34) +// #define PIN_GPS_EN PIN_3V3_EN +#define PIN_GPS_PPS (17) // Pulse per second input from the GPS + +#define GPS_RX_PIN PIN_SERIAL1_RX +#define GPS_TX_PIN PIN_SERIAL1_TX + +// Define pin to enable GPS toggle (set GPIO to LOW) via user button triple press + +// RAK12002 RTC Module +#define RV3028_RTC (uint8_t)0b1010010 + +// RAK18001 Buzzer in Slot C +// #define PIN_BUZZER 21 // IO3 is PWM2 +// NEW: set this via protobuf instead! + +// Battery +// The battery sense is hooked to pin A0 (5) +#define BATTERY_PIN PIN_A0 +// and has 12 bit resolution +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER 1.73 + +#define HAS_RTC 1 + +#define RAK_4631 1 + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif From 0283e0658b2f615991f369307a240a7479da5632 Mon Sep 17 00:00:00 2001 From: Tom Fifield Date: Sun, 19 Oct 2025 08:54:56 +1100 Subject: [PATCH 08/14] Master --> develop (#8394) * Update exempt labels for stale bot workflow Adds triaged and backlog to the list of exempt labels. * Update meshtastic/web to v2.6.7 (#8381) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update DFRobot_RTU to v1.0.6 (#8387) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Update mcr.microsoft.com/devcontainers/cpp Docker tag to v2 (#8375) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * manual merge stale bot config (#8392) * Board support: RAK3401+RAK13302 1-watt (#8140) * Add RAK3401 variant files * Add SPI configuration for RAK3401 and RAK13302 variants * Refactor SPI pin configuration and clean up variant definitions for RAK3401 * Add TX_GAIN_LORA for RAK13302 Power Amp * Fix merge --------- Co-authored-by: Ben Meadors Co-authored-by: Tom Fifield Co-authored-by: Manuel <71137295+mverch67@users.noreply.github.com> --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel.Cao <144674500+DanielCao0@users.noreply.github.com> Co-authored-by: Ben Meadors Co-authored-by: Manuel <71137295+mverch67@users.noreply.github.com> --- src/configuration.h | 5 + src/main.cpp | 7 + .../nrf52840/rak3401_1watt/platformio.ini | 31 +++ variants/nrf52840/rak3401_1watt/variant.cpp | 45 ++++ variants/nrf52840/rak3401_1watt/variant.h | 226 ++++++++++++++++++ 5 files changed, 314 insertions(+) create mode 100644 variants/nrf52840/rak3401_1watt/platformio.ini create mode 100644 variants/nrf52840/rak3401_1watt/variant.cpp create mode 100644 variants/nrf52840/rak3401_1watt/variant.h diff --git a/src/configuration.h b/src/configuration.h index c6c8d673c..baf24a636 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -126,6 +126,11 @@ along with this program. If not, see . #define TX_GAIN_LORA 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 10, 10, 9, 9, 8, 7 #endif +#ifdef RAK13302 +#define NUM_PA_POINTS 22 +#define TX_GAIN_LORA 7, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 8 +#endif + // Default system gain to 0 if not defined #ifndef TX_GAIN_LORA #define TX_GAIN_LORA 0 diff --git a/src/main.cpp b/src/main.cpp index bb97a1aa6..3801f6f9f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -841,7 +841,14 @@ void setup() SPI.begin(); } #elif !defined(ARCH_ESP32) // ARCH_RP2040 +#if defined(RAK3401) || defined(RAK13302) + pinMode(WB_IO2, OUTPUT); + digitalWrite(WB_IO2, HIGH); + SPI1.setPins(LORA_MISO, LORA_SCK, LORA_MOSI); + SPI1.begin(); +#else SPI.begin(); +#endif #else // ESP32 #if defined(HW_SPI1_DEVICE) diff --git a/variants/nrf52840/rak3401_1watt/platformio.ini b/variants/nrf52840/rak3401_1watt/platformio.ini new file mode 100644 index 000000000..1a915a6b3 --- /dev/null +++ b/variants/nrf52840/rak3401_1watt/platformio.ini @@ -0,0 +1,31 @@ +; The very slick RAK wireless RAK 4631 / 4630 board - Unified firmware for 5005/19003, with or without OLED RAK 1921 +[env:rak3401-1watt] +extends = nrf52840_base +board = wiscore_rak4631 +board_check = true +build_flags = ${nrf52840_base.build_flags} + -Ivariants/nrf52840/rak3401_1watt + -D RAK_4631 +; -DGPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. + -D RAK3401 + -D RAK13302 ; RAK 1Watt Power Amplifier + -DRADIOLIB_EXCLUDE_SX128X=1 + -DRADIOLIB_EXCLUDE_SX127X=1 + -DRADIOLIB_EXCLUDE_LR11X0=1 +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/rak3401_1watt> + +lib_deps = + ${nrf52840_base.lib_deps} + ${networking_base.lib_deps} + melopero/Melopero RV3028@^1.1.0 + rakwireless/RAKwireless NCP5623 RGB LED library@^1.0.2 + beegee-tokyo/RAK12035_SoilMoisture@^1.0.4 + https://github.com/RAKWireless/RAK12034-BMX160/archive/dcead07ffa267d3c906e9ca4a1330ab989e957e2.zip + +; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) +; Note: as of 6/2013 the serial/bootloader based programming takes approximately 30 seconds +;upload_protocol = jlink + +; Allows programming and debug via the RAK NanoDAP as the default debugger tool for the RAK4631 (it is only $10!) +; programming time is about the same as the bootloader version. +; For information on this see the meshtastic developers documentation for "Development on the NRF52" + diff --git a/variants/nrf52840/rak3401_1watt/variant.cpp b/variants/nrf52840/rak3401_1watt/variant.cpp new file mode 100644 index 000000000..e84b60b3b --- /dev/null +++ b/variants/nrf52840/rak3401_1watt/variant.cpp @@ -0,0 +1,45 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + + // P1 + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; + +void initVariant() +{ + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); + + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); + + // 3V3 Power Rail + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); +} diff --git a/variants/nrf52840/rak3401_1watt/variant.h b/variants/nrf52840/rak3401_1watt/variant.h new file mode 100644 index 000000000..d4bb1a175 --- /dev/null +++ b/variants/nrf52840/rak3401_1watt/variant.h @@ -0,0 +1,226 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef _VARIANT_RAK3401_ +#define _VARIANT_RAK3401_ + +#define RAK4630 + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF +// define USE_LFRC // Board uses RC for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED1 (35) +#define PIN_LED2 (36) + +#define LED_BUILTIN PIN_LED1 +#define LED_CONN PIN_LED2 + +#define LED_GREEN PIN_LED1 +#define LED_BLUE PIN_LED2 + +#define LED_STATE_ON 1 // State when LED is litted + +/* + * Analog pins + */ +#define PIN_A0 (5) +#define PIN_A1 (31) +#define PIN_A2 (28) +#define PIN_A3 (29) +#define PIN_A4 (30) +#define PIN_A5 (31) +#define PIN_A6 (0xff) +#define PIN_A7 (0xff) + +static const uint8_t A0 = PIN_A0; +static const uint8_t A1 = PIN_A1; +static const uint8_t A2 = PIN_A2; +static const uint8_t A3 = PIN_A3; +static const uint8_t A4 = PIN_A4; +static const uint8_t A5 = PIN_A5; +static const uint8_t A6 = PIN_A6; +static const uint8_t A7 = PIN_A7; +#define ADC_RESOLUTION 14 + +// Other pins +#define WB_I2C1_SDA (13) // SENSOR_SLOT IO_SLOT +#define WB_I2C1_SCL (14) // SENSOR_SLOT IO_SLOT + +#define PIN_AREF (2) +#define PIN_NFC1 (9) +#define WB_IO5 PIN_NFC1 +#define WB_IO4 (4) +#define PIN_NFC2 (10) + +static const uint8_t AREF = PIN_AREF; + +/* + * Serial interfaces + */ +#define PIN_SERIAL1_RX (15) +#define PIN_SERIAL1_TX (16) + +// Connected to Jlink CDC +#define PIN_SERIAL2_RX (8) +#define PIN_SERIAL2_TX (6) + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 2 + +#define PIN_SPI_MISO (45) +#define PIN_SPI_MOSI (44) +#define PIN_SPI_SCK (43) + +#define PIN_SPI1_MISO (29) // (0 + 29) +#define PIN_SPI1_MOSI (30) // (0 + 30) +#define PIN_SPI1_SCK (3) // (0 + 3) + +static const uint8_t SS = 42; +static const uint8_t MOSI = PIN_SPI_MOSI; +static const uint8_t MISO = PIN_SPI_MISO; +static const uint8_t SCK = PIN_SPI_SCK; + +/* + * eink display pins + */ +#define PIN_EINK_CS (0 + 26) +#define PIN_EINK_BUSY (0 + 4) +#define PIN_EINK_DC (0 + 17) +#define PIN_EINK_RES (-1) +#define PIN_EINK_SCLK (0 + 3) +#define PIN_EINK_MOSI (0 + 30) // also called SDI + +/* + * Wire Interfaces + */ +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (WB_I2C1_SDA) +#define PIN_WIRE_SCL (WB_I2C1_SCL) + +// QSPI Pins +#define PIN_QSPI_SCK 3 +#define PIN_QSPI_CS 26 +#define PIN_QSPI_IO0 30 +#define PIN_QSPI_IO1 29 +#define PIN_QSPI_IO2 28 +#define PIN_QSPI_IO3 2 + +// On-board QSPI Flash +#define EXTERNAL_FLASH_DEVICES IS25LP080D +#define EXTERNAL_FLASH_USE_QSPI + +// 1watt sx1262 RAK13302 +#define HW_SPI1_DEVICE 1 + +#define LORA_SCK PIN_SPI1_SCK +#define LORA_MISO PIN_SPI1_MISO +#define LORA_MOSI PIN_SPI1_MOSI +#define LORA_CS 26 + +#define USE_SX1262 +#define SX126X_CS (26) +#define SX126X_DIO1 (10) +#define SX126X_BUSY (9) +#define SX126X_RESET (4) + +#define SX126X_POWER_EN (21) +// DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3 +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +// Testing USB detection +#define NRF_APM +// If using a power chip like the INA3221 you can override the default battery voltage channel below +// and comment out NRF_APM to use the INA3221 instead of the USB detection for charging +// #define INA3221_BAT_CH INA3221_CH2 +// #define INA3221_ENV_CH INA3221_CH1 + +// enables 3.3V periphery like GPS or IO Module +// Do not toggle this for GPS power savings +#define PIN_3V3_EN (34) +#define WB_IO2 PIN_3V3_EN + +// RAK1910 GPS module +// If using the wisblock GPS module and pluged into Port A on WisBlock base +// IO1 is hooked to PPS (pin 12 on header) = gpio 17 +// IO2 is hooked to GPS RESET = gpio 34, but it can not be used to this because IO2 is ALSO used to control 3V3_S power (1 is on). +// Therefore must be 1 to keep peripherals powered +// Power is on the controllable 3V3_S rail +// #define PIN_GPS_RESET (34) +// #define PIN_GPS_EN PIN_3V3_EN +#define PIN_GPS_PPS (17) // Pulse per second input from the GPS + +#define GPS_RX_PIN PIN_SERIAL1_RX +#define GPS_TX_PIN PIN_SERIAL1_TX + +// Define pin to enable GPS toggle (set GPIO to LOW) via user button triple press + +// RAK12002 RTC Module +#define RV3028_RTC (uint8_t)0b1010010 + +// RAK18001 Buzzer in Slot C +// #define PIN_BUZZER 21 // IO3 is PWM2 +// NEW: set this via protobuf instead! + +// Battery +// The battery sense is hooked to pin A0 (5) +#define BATTERY_PIN PIN_A0 +// and has 12 bit resolution +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER 1.73 + +#define HAS_RTC 1 + +#define RAK_4631 1 + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif From 30022c93774f5bd1b363a2aaebf1c49ef7a0757c Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sat, 18 Oct 2025 18:00:14 -0400 Subject: [PATCH 09/14] Fixe battery voltage to show missing decimals (#8386) --- src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp index 7876276a8..09f76ed46 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp @@ -709,7 +709,7 @@ void InkHUD::MenuApplet::drawSystemInfoPanel(int16_t left, int16_t top, uint16_t // Voltage float voltage = powerStatus->getBatteryVoltageMv() / 1000.0; char voltageStr[6]; // "XX.XV" - sprintf(voltageStr, "%.1fV", voltage); + sprintf(voltageStr, "%.2fV", voltage); printAt(colC[0], labelT, "Bat", CENTER, TOP); printAt(colC[0], valT, voltageStr, CENTER, TOP); From b4dea63f4499273b32565a8451b43d83a135ac1b Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sat, 18 Oct 2025 18:00:35 -0400 Subject: [PATCH 10/14] Gatting off BaseUI code from screenless devices and InkHUD (#8384) --- src/graphics/SharedUIDisplay.cpp | 5 ++++- src/graphics/VirtualKeyboard.cpp | 4 +++- src/graphics/draw/CompassRenderer.cpp | 3 +++ src/graphics/emotes.cpp | 3 +++ src/modules/Telemetry/EnvironmentTelemetry.cpp | 2 ++ src/modules/Telemetry/PowerTelemetry.cpp | 2 ++ 6 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index dcaa5d69b..8e1299f51 100644 --- a/src/graphics/SharedUIDisplay.cpp +++ b/src/graphics/SharedUIDisplay.cpp @@ -1,6 +1,8 @@ -#include "graphics/SharedUIDisplay.h" +#include "configuration.h" +#if HAS_SCREEN #include "RTC.h" #include "graphics/ScreenFonts.h" +#include "graphics/SharedUIDisplay.h" #include "graphics/draw/UIRenderer.h" #include "main.h" #include "meshtastic/config.pb.h" @@ -423,3 +425,4 @@ std::string sanitizeString(const std::string &input) } } // namespace graphics +#endif \ No newline at end of file diff --git a/src/graphics/VirtualKeyboard.cpp b/src/graphics/VirtualKeyboard.cpp index 84d5551cb..8062a0338 100644 --- a/src/graphics/VirtualKeyboard.cpp +++ b/src/graphics/VirtualKeyboard.cpp @@ -1,5 +1,6 @@ -#include "VirtualKeyboard.h" #include "configuration.h" +#if HAS_SCREEN +#include "VirtualKeyboard.h" #include "graphics/Screen.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" @@ -736,3 +737,4 @@ bool VirtualKeyboard::isTimedOut() const } } // namespace graphics +#endif \ No newline at end of file diff --git a/src/graphics/draw/CompassRenderer.cpp b/src/graphics/draw/CompassRenderer.cpp index 0e5a1d727..629949ffd 100644 --- a/src/graphics/draw/CompassRenderer.cpp +++ b/src/graphics/draw/CompassRenderer.cpp @@ -1,3 +1,5 @@ +#include "configuration.h" +#if HAS_SCREEN #include "CompassRenderer.h" #include "NodeDB.h" #include "UIRenderer.h" @@ -135,3 +137,4 @@ uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight) } // namespace CompassRenderer } // namespace graphics +#endif \ No newline at end of file diff --git a/src/graphics/emotes.cpp b/src/graphics/emotes.cpp index e1a105d20..a0227d365 100644 --- a/src/graphics/emotes.cpp +++ b/src/graphics/emotes.cpp @@ -1,3 +1,5 @@ +#include "configuration.h" +#if HAS_SCREEN #include "emotes.h" namespace graphics @@ -275,3 +277,4 @@ const unsigned char bell_icon[] PROGMEM = { #endif } // namespace graphics +#endif \ No newline at end of file diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index 95947560d..2337af808 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -361,6 +361,7 @@ bool EnvironmentTelemetryModule::wantUIFrame() return moduleConfig.telemetry.environment_screen_enabled; } +#if HAS_SCREEN void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { // === Setup display === @@ -510,6 +511,7 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt currentY += rowHeight; } } +#endif bool EnvironmentTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) { diff --git a/src/modules/Telemetry/PowerTelemetry.cpp b/src/modules/Telemetry/PowerTelemetry.cpp index 479861a2e..e69ee3931 100644 --- a/src/modules/Telemetry/PowerTelemetry.cpp +++ b/src/modules/Telemetry/PowerTelemetry.cpp @@ -108,6 +108,7 @@ bool PowerTelemetryModule::wantUIFrame() return moduleConfig.telemetry.power_screen_enabled; } +#if HAS_SCREEN void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { display->clear(); @@ -165,6 +166,7 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s drawLine("Ch3", m.ch3_voltage, m.ch3_current); } } +#endif bool PowerTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_Telemetry *t) { From e1c259ae32ade6524f43898d65fdc6c2282befe4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 18 Oct 2025 18:21:24 -0500 Subject: [PATCH 11/14] Update protobufs (#8396) Co-authored-by: fifieldt <1287116+fifieldt@users.noreply.github.com> --- protobufs | 2 +- src/mesh/generated/meshtastic/telemetry.pb.h | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/protobufs b/protobufs index 38638f19f..4a618380a 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 38638f19f84ad886222b484d6bf5a8459aed8c7e +Subproject commit 4a618380a0d80b476bb7f278008f811f456e0a5f diff --git a/src/mesh/generated/meshtastic/telemetry.pb.h b/src/mesh/generated/meshtastic/telemetry.pb.h index a8ca96e95..dec89ba15 100644 --- a/src/mesh/generated/meshtastic/telemetry.pb.h +++ b/src/mesh/generated/meshtastic/telemetry.pb.h @@ -101,7 +101,9 @@ typedef enum _meshtastic_TelemetrySensorType { /* SEN5X PM SENSORS */ meshtastic_TelemetrySensorType_SEN5X = 43, /* TSL2561 light sensor */ - meshtastic_TelemetrySensorType_TSL2561 = 44 + meshtastic_TelemetrySensorType_TSL2561 = 44, + /* BH1750 light sensor */ + meshtastic_TelemetrySensorType_BH1750 = 45 } meshtastic_TelemetrySensorType; /* Struct definitions */ @@ -438,8 +440,8 @@ extern "C" { /* Helper constants for enums */ #define _meshtastic_TelemetrySensorType_MIN meshtastic_TelemetrySensorType_SENSOR_UNSET -#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_TSL2561 -#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_TSL2561+1)) +#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_BH1750 +#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_BH1750+1)) From 05c176c16a1377991bc3de04f6969d2e2d3324fa Mon Sep 17 00:00:00 2001 From: igorka48 <10116759+igorka48@users.noreply.github.com> Date: Sun, 19 Oct 2025 12:48:06 +0300 Subject: [PATCH 12/14] Added support for SugarCube device (#8187) * Added support for SugarCube device * Update variants/esp32/sugarcube/platformio.ini Co-authored-by: Austin * added buzzer pin * Apply PR comments * Fix MR comments --------- Co-authored-by: Austin --- variants/esp32/tlora_v2_1_16/platformio.ini | 9 +++++++++ variants/esp32/tlora_v2_1_16/variant.h | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/variants/esp32/tlora_v2_1_16/platformio.ini b/variants/esp32/tlora_v2_1_16/platformio.ini index 6967bb480..8d5bdab9e 100644 --- a/variants/esp32/tlora_v2_1_16/platformio.ini +++ b/variants/esp32/tlora_v2_1_16/platformio.ini @@ -5,3 +5,12 @@ board_check = true build_flags = ${esp32_base.build_flags} -D TLORA_V2_1_16 -I variants/esp32/tlora_v2_1_16 upload_speed = 115200 + +[env:sugarcube] +extends = env:tlora-v2-1-1_6 +board_level = extra +build_flags = + ${env:tlora-v2-1-1_6.build_flags} + -DBUTTON_PIN=0 + -DPIN_BUZZER=25 + -DLED_PIN=-1 \ No newline at end of file diff --git a/variants/esp32/tlora_v2_1_16/variant.h b/variants/esp32/tlora_v2_1_16/variant.h index 48c069ab7..9584dd68b 100644 --- a/variants/esp32/tlora_v2_1_16/variant.h +++ b/variants/esp32/tlora_v2_1_16/variant.h @@ -8,7 +8,11 @@ #define I2C_SDA 21 // I2C pins for this board #define I2C_SCL 22 +#if defined(LED_PIN) && LED_PIN == -1 +#undef LED_PIN +#else #define LED_PIN 25 // If defined we will blink this LED +#endif #define USE_RF95 #define LORA_DIO0 26 // a No connect on the SX1262 module From f2a63faddd43752decf087a7cda003c809033bb1 Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Sun, 19 Oct 2025 06:32:58 -0400 Subject: [PATCH 13/14] Fix NimbleBluetooth reliability and performance (#8385) * Initial work to get NimbleBluetooth working reliably, and cross-task mutexes cleaned up * Pre-fill toPhoneQueue when safe (during config/nodeinfo): runOnceToPhoneCanPreloadNextPacket * Handle 0-byte responses breaking clients during initial config phases * requestLowerPowerConnection * PhoneAPI: onConfigStart and onConfigComplete callbacks for subclasses * NimbleBluetooth: switch to high-throughput BLE mode during config, then lower-power BLE mode for steady-state * Add some documentation to NimbleBluetooth.cpp * make cppcheck happier * Allow runOnceHandleToPhoneQueue to tell runOnce to shouldBreakAndRetryLater, so we don't busy-loop forever in runOnce * Gating some logging behind DEBUG_NIMBLE_ON_READ_TIMING ifdef again; bump retry count * Add check for connected state in NimBLE onRead() --------- Co-authored-by: Jonathan Bennett --- src/mesh/PhoneAPI.cpp | 31 +- src/mesh/PhoneAPI.h | 6 + src/nimble/NimbleBluetooth.cpp | 536 +++++++++++++++++++++++++++++---- 3 files changed, 512 insertions(+), 61 deletions(-) diff --git a/src/mesh/PhoneAPI.cpp b/src/mesh/PhoneAPI.cpp index 51a2bc148..d1e342c80 100644 --- a/src/mesh/PhoneAPI.cpp +++ b/src/mesh/PhoneAPI.cpp @@ -57,6 +57,9 @@ void PhoneAPI::handleStartConfig() #endif } + // Allow subclasses to prepare for high-throughput config traffic + onConfigStart(); + // even if we were already connected - restart our state machine if (config_nonce == SPECIAL_NONCE_ONLY_NODES) { // If client only wants node info, jump directly to sending nodes @@ -71,7 +74,7 @@ void PhoneAPI::handleStartConfig() spiLock->unlock(); LOG_DEBUG("Got %d files in manifest", filesManifest.size()); - LOG_INFO("Start API client config"); + LOG_INFO("Start API client config millis=%u", millis()); // Protect against concurrent BLE callbacks: they run in NimBLE's FreeRTOS task and also touch nodeInfoQueue. { concurrency::LockGuard guard(&nodeInfoMutex); @@ -453,7 +456,10 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf) break; case STATE_SEND_OTHER_NODEINFOS: { - LOG_DEBUG("Send known nodes"); + if (readIndex == 2) { // readIndex==2 will be true for the first non-us node + LOG_INFO("Start sending nodeinfos millis=%u", millis()); + } + meshtastic_NodeInfo infoToSend = {}; { concurrency::LockGuard guard(&nodeInfoMutex); @@ -470,13 +476,22 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf) if (infoToSend.num != 0) { // Just in case we stored a different user.id in the past, but should never happen going forward sprintf(infoToSend.user.id, "!%08x", infoToSend.num); - LOG_DEBUG("nodeinfo: num=0x%x, lastseen=%u, id=%s, name=%s", infoToSend.num, infoToSend.last_heard, - infoToSend.user.id, infoToSend.user.long_name); + + // Logging this really slows down sending nodes on initial connection because the serial console is so slow, so only + // uncomment if you really need to: + // LOG_INFO("nodeinfo: num=0x%x, lastseen=%u, id=%s, name=%s", nodeInfoForPhone.num, nodeInfoForPhone.last_heard, + // nodeInfoForPhone.user.id, nodeInfoForPhone.user.long_name); + + // Occasional progress logging. (readIndex==2 will be true for the first non-us node) + if (readIndex == 2 || readIndex % 20 == 0) { + LOG_DEBUG("nodeinfo: %d/%d", readIndex, nodeDB->getNumMeshNodes()); + } + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_node_info_tag; fromRadioScratch.node_info = infoToSend; prefetchNodeInfos(); } else { - LOG_DEBUG("Done sending nodeinfo"); + LOG_DEBUG("Done sending %d of %d nodeinfos millis=%u", readIndex, nodeDB->getNumMeshNodes(), millis()); concurrency::LockGuard guard(&nodeInfoMutex); nodeInfoQueue.clear(); state = STATE_SEND_FILEMANIFEST; @@ -558,11 +573,15 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf) void PhoneAPI::sendConfigComplete() { - LOG_INFO("Config Send Complete"); + LOG_INFO("Config Send Complete millis=%u", millis()); fromRadioScratch.which_payload_variant = meshtastic_FromRadio_config_complete_id_tag; fromRadioScratch.config_complete_id = config_nonce; config_nonce = 0; state = STATE_SEND_PACKETS; + + // Allow subclasses to know we've entered steady-state so they can lower power consumption + onConfigComplete(); + pauseBluetoothLogging = false; } diff --git a/src/mesh/PhoneAPI.h b/src/mesh/PhoneAPI.h index a8d0faa28..d6682684f 100644 --- a/src/mesh/PhoneAPI.h +++ b/src/mesh/PhoneAPI.h @@ -136,6 +136,7 @@ class PhoneAPI bool available(); bool isConnected() { return state != STATE_SEND_NOTHING; } + bool isSendingPackets() { return state == STATE_SEND_PACKETS; } protected: /// Our fromradio packet while it is being assembled @@ -158,6 +159,11 @@ class PhoneAPI */ virtual void onNowHasData(uint32_t fromRadioNum) {} + /// Subclasses can use these lifecycle hooks for transport-specific behavior around config/steady-state + /// (i.e. BLE connection params) + virtual void onConfigStart() {} + virtual void onConfigComplete() {} + /// begin a new connection void handleStartConfig(); diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp index eb1d909f1..9accf23c6 100644 --- a/src/nimble/NimbleBluetooth.cpp +++ b/src/nimble/NimbleBluetooth.cpp @@ -3,12 +3,15 @@ #include "BluetoothCommon.h" #include "NimbleBluetooth.h" #include "PowerFSM.h" +#include "StaticPointerQueue.h" +#include "concurrency/OSThread.h" #include "main.h" #include "mesh/PhoneAPI.h" #include "mesh/mesh-pb-constants.h" #include "sleep.h" #include +#include #include #ifdef NIMBLE_TWO @@ -32,45 +35,288 @@ constexpr uint16_t kPreferredBleTxTimeUs = (kPreferredBleTxOctets + 14) * 8; } // namespace #endif +// Debugging options: careful, they slow things down quite a bit! +// #define DEBUG_NIMBLE_ON_READ_TIMING // uncomment to time onRead duration +// #define DEBUG_NIMBLE_ON_WRITE_TIMING // uncomment to time onWrite duration +// #define DEBUG_NIMBLE_NOTIFY // uncomment to enable notify logging + +#define NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE 3 +#define NIMBLE_BLUETOOTH_FROM_PHONE_QUEUE_SIZE 3 + NimBLECharacteristic *fromNumCharacteristic; NimBLECharacteristic *BatteryCharacteristic; NimBLECharacteristic *logRadioCharacteristic; NimBLEServer *bleServer; static bool passkeyShowing; +static std::atomic nimbleBluetoothConnHandle{-1}; // actual handles are uint16_t, so -1 means "no connection" class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread { + /* + CAUTION: There's a lot going on here and lots of room to break things. + + This NimbleBluetooth.cpp file does some tricky synchronization between the NimBLE FreeRTOS task (which runs the onRead and + onWrite callbacks) and the main task (which runs runOnce and the rest of PhoneAPI). + + The main idea is to add a little bit of synchronization here to make it so that the rest of the codebase doesn't have to + know about concurrency and mutexes, and can just run happily ever after as a cooperative multitasking OSThread system, where + locking isn't something that anyone has to worry about too much! :) + + We achieve this by having some queues and mutexes in this file only, and ensuring that all calls to getFromRadio and + handleToRadio are only made from the main FreeRTOS task. This way, the rest of the codebase doesn't have to worry about + being run concurrently, which would make everything else much much much more complicated. + + PHONE -> RADIO: + - [NimBLE FreeRTOS task:] onWrite callback holds fromPhoneMutex and pushes received packets into fromPhoneQueue. + - [Main task:] runOnceHandleFromPhoneQueue in main task holds fromPhoneMutex, pulls packets from fromPhoneQueue, and calls + handleToRadio **in main task**. + + RADIO -> PHONE: + - [NimBLE FreeRTOS task:] onRead callback sets onReadCallbackIsWaitingForData flag and polls in a busy loop. (unless + there's already a packet waiting in toPhoneQueue) + - [Main task:] runOnceHandleToPhoneQueue sees onReadCallbackIsWaitingForData flag, calls getFromRadio **in main task** to + get packets from radio, holds toPhoneMutex, pushes the packet into toPhoneQueue, and clears the + onReadCallbackIsWaitingForData flag. + - [NimBLE FreeRTOS task:] onRead callback sees that the onReadCallbackIsWaitingForData flag cleared, holds toPhoneMutex, + pops the packet from toPhoneQueue, and returns it to NimBLE. + + MUTEXES: + - fromPhoneMutex protects fromPhoneQueue and fromPhoneQueueSize + - toPhoneMutex protects toPhoneQueue, toPhoneQueueByteSizes, and toPhoneQueueSize + + ATOMICS: + - fromPhoneQueueSize is only increased by onWrite, and only decreased by runOnceHandleFromPhoneQueue (or onDisconnect). + - toPhoneQueueSize is only increased by runOnceHandleToPhoneQueue, and only decreased by onRead (or onDisconnect). + - onReadCallbackIsWaitingForData is a flag. It's only set by onRead, and only cleared by runOnceHandleToPhoneQueue (or + onDisconnect). + + PRELOADING: see comments in runOnceToPhoneCanPreloadNextPacket about when it's safe to preload packets from getFromRadio. + + BLE CONNECTION PARAMS: + - During config, we request a high-throughput, low-latency BLE connection for speed. + - After config, we switch to a lower-power BLE connection for steady-state use to extend battery life. + + MEMORY MANAGEMENT: + - We keep packets on the stack and do not allocate heap. + - We use std::array for fromPhoneQueue and toPhoneQueue to avoid mallocs and frees across FreeRTOS tasks. + - Yes, we have to do some copy operations on pop because of this, but it's worth it to avoid cross-task memory management. + + NOTIFY IS BROKEN: + - Adding NIMBLE_PROPERTY::NOTIFY to FromRadioCharacteristic appears to break things. It is NOT backwards compatible. + + ZERO-SIZE READS: + - Returning a zero-size read from onRead breaks some clients during the config phase. So we have to block onRead until we + have data. + - During the STATE_SEND_PACKETS phase, it's totally OK to return zero-size reads, as clients are expected to do reads + until they get a 0-byte response. + + CROSS-TASK WAKEUP: + - If you call: bluetoothPhoneAPI->setIntervalFromNow(0); to schedule immediate processing of new data, + - Then you should also call: concurrency::mainDelay.interrupt(); to wake up the main loop if it's sleeping. + - Otherwise, you're going to wait ~100ms or so until the main loop wakes up from some other cause. + */ + public: - BluetoothPhoneAPI() : concurrency::OSThread("NimbleBluetooth") { nimble_queue.resize(3); } - std::vector nimble_queue; - std::mutex nimble_mutex; - uint8_t queue_size = 0; - uint8_t fromRadioBytes[meshtastic_FromRadio_size] = {0}; - size_t numBytes = 0; - bool hasChecked = false; - bool phoneWants = false; + BluetoothPhoneAPI() : concurrency::OSThread("NimbleBluetooth") {} + + /* Packets from phone (BLE onWrite callback) */ + std::mutex fromPhoneMutex; + std::atomic fromPhoneQueueSize{0}; + // We use array here (and pay the cost of memcpy) to avoid dynamic memory allocations and frees across FreeRTOS tasks. + std::array fromPhoneQueue{}; + + /* Packets to phone (BLE onRead callback) */ + std::mutex toPhoneMutex; + std::atomic toPhoneQueueSize{0}; + // We use array here (and pay the cost of memcpy) to avoid dynamic memory allocations and frees across FreeRTOS tasks. + std::array, NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE> toPhoneQueue{}; + std::array toPhoneQueueByteSizes{}; + // The onReadCallbackIsWaitingForData flag provides synchronization between the NimBLE task's onRead callback and our main + // task's runOnce. It's only set by onRead, and only cleared by runOnce. + std::atomic onReadCallbackIsWaitingForData{false}; + + /* Statistics/logging helpers */ + std::atomic readCount{0}; + std::atomic notifyCount{0}; + std::atomic writeCount{0}; protected: virtual int32_t runOnce() override { - std::lock_guard guard(nimble_mutex); - if (queue_size > 0) { - for (uint8_t i = 0; i < queue_size; i++) { - handleToRadio(nimble_queue.at(i).data(), nimble_queue.at(i).length()); + bool shouldBreakAndRetryLater = false; + + while (runOnceHasWorkToDo()) { + // Important that we service onRead first, because the onRead callback blocks NimBLE until we clear + // onReadCallbackIsWaitingForData. + shouldBreakAndRetryLater = runOnceHandleToPhoneQueue(); // push data from getFromRadio to onRead + runOnceHandleFromPhoneQueue(); // pull data from onWrite to handleToRadio + + if (shouldBreakAndRetryLater) { + // onRead still wants data, but it's not available yet. Return so we can try again when a packet may be ready. +#ifdef DEBUG_NIMBLE_ON_READ_TIMING + LOG_INFO("BLE runOnce breaking to retry later (leaving onRead waiting)"); +#endif + return 100; // try again in 100ms } - LOG_DEBUG("Queue_size %u", queue_size); - queue_size = 0; - } - if (!hasChecked && phoneWants) { - // Pull fresh data while we're outside of the NimBLE callback context. - numBytes = getFromRadio(fromRadioBytes); - hasChecked = true; } // the run is triggered via NimbleBluetoothToRadioCallback and NimbleBluetoothFromRadioCallback return INT32_MAX; } + + virtual void onConfigStart() override + { + LOG_INFO("BLE onConfigStart"); + + // Prefer high throughput during config/setup, at the cost of high power consumption (for a few seconds) + if (bleServer && isConnected()) { + int32_t conn_handle = nimbleBluetoothConnHandle.load(); + if (conn_handle != -1) { + requestHighThroughputConnection(static_cast(conn_handle)); + } + } + } + + virtual void onConfigComplete() override + { + LOG_INFO("BLE onConfigComplete"); + + // Switch to lower power consumption BLE connection params for steady-state use after config/setup is complete + if (bleServer && isConnected()) { + int32_t conn_handle = nimbleBluetoothConnHandle.load(); + if (conn_handle != -1) { + requestLowerPowerConnection(static_cast(conn_handle)); + } + } + } + + bool runOnceHasWorkToDo() { return runOnceHasWorkToPhone() || runOnceHasWorkFromPhone(); } + + bool runOnceHasWorkToPhone() { return onReadCallbackIsWaitingForData || runOnceToPhoneCanPreloadNextPacket(); } + + bool runOnceToPhoneCanPreloadNextPacket() + { + /* + * PRELOADING getFromRadio RESPONSES: + * + * It's not safe to preload packets if we're in STATE_SEND_PACKETS, because there may be a while between the time we call + * getFromRadio and when the client actually reads it. If the connection drops in that time, we might lose that packet + * forever. In STATE_SEND_PACKETS, if we wait for onRead before we call getFromRadio, we minimize the time window where + * the client might disconnect before completing the read. + * + * However, if we're in the setup states (sending config, nodeinfo, etc), it's safe and beneficial to preload packets into + * toPhoneQueue because the client will just reconnect after a disconnect, losing nothing. + */ + + if (!isConnected()) { + return false; + } else if (isSendingPackets()) { + // If we're in STATE_SEND_PACKETS, we must wait for onRead before calling getFromRadio. + return false; + } else { + // In other states, we can preload as long as there's space in the toPhoneQueue. + return toPhoneQueueSize < NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE; + } + } + + bool runOnceHandleToPhoneQueue() + { + // Returns false normally. + // Returns true if we should break out of runOnce and retry later, such as setup states where getFromRadio returns 0 + // bytes. + + // Stack buffer for getFromRadio packet + uint8_t fromRadioBytes[meshtastic_FromRadio_size] = {0}; + size_t numBytes = 0; + + if (onReadCallbackIsWaitingForData || runOnceToPhoneCanPreloadNextPacket()) { + numBytes = getFromRadio(fromRadioBytes); + + if (numBytes == 0) { + // Client expected a read, but we have nothing to send. + // Returning a 0-byte packet breaks clients during the config phase, so we have to block onRead until there's a + // packet ready. + if (isSendingPackets()) { + // In STATE_SEND_PACKETS, it is 100% OK to return a 0-byte response, as we expect clients to do read beyond + // notifies regularly, to make sure they have nothing else to read. +#ifdef DEBUG_NIMBLE_ON_READ_TIMING + LOG_DEBUG("BLE getFromRadio returned numBytes=0, but in STATE_SEND_PACKETS, so clearing " + "onReadCallbackIsWaitingForData flag"); +#endif + } else { + // In other states, this breaks clients. + // Return early, leaving onReadCallbackIsWaitingForData==true so onRead knows to try again. + // This gives runOnce a chance to handleToRadio and produce a response. +#ifdef DEBUG_NIMBLE_ON_READ_TIMING + LOG_DEBUG("BLE getFromRadio returned numBytes=0. Blocking onRead until we have data"); +#endif + + // Return true to tell runOnce to shouldBreakAndRetryLater, so we don't busy-loop in runOnce even though + // onRead is still waiting! + return true; + } + } else { + // Push to toPhoneQueue, protected by toPhoneMutex. Hold the mutex as briefly as possible. + if (toPhoneQueueSize < NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE) { + // Note: the comparison above is safe without a mutex because we are the only method that *increases* + // toPhoneQueueSize. (It's okay if toPhoneQueueSize *decreases* in the NimBLE task meanwhile.) + + { // scope for toPhoneMutex mutex + std::lock_guard guard(toPhoneMutex); + size_t storeAtIndex = toPhoneQueueSize.load(); + memcpy(toPhoneQueue[storeAtIndex].data(), fromRadioBytes, numBytes); + toPhoneQueueByteSizes[storeAtIndex] = numBytes; + toPhoneQueueSize++; + } +#ifdef DEBUG_NIMBLE_ON_READ_TIMING + LOG_DEBUG("BLE getFromRadio returned numBytes=%u, pushed toPhoneQueueSize=%u", numBytes, + toPhoneQueueSize.load()); +#endif + } else { + // Shouldn't happen because the onRead callback shouldn't be waiting if the queue is full! + LOG_ERROR("Shouldn't happen! Drop FromRadio packet, toPhoneQueue full (%u bytes)", numBytes); + } + } + + // Clear the onReadCallbackIsWaitingForData flag so onRead knows it can proceed. + onReadCallbackIsWaitingForData = false; // only clear this flag AFTER the push + } + + return false; + } + + bool runOnceHasWorkFromPhone() { return fromPhoneQueueSize > 0; } + + void runOnceHandleFromPhoneQueue() + { + // Handle packets we received from onWrite from the phone. + if (fromPhoneQueueSize > 0) { + // Note: the comparison above is safe without a mutex because we are the only method that *decreases* + // fromPhoneQueueSize. (It's okay if fromPhoneQueueSize *increases* in the NimBLE task meanwhile.) + + LOG_DEBUG("NimbleBluetooth: handling ToRadio packet, fromPhoneQueueSize=%u", fromPhoneQueueSize.load()); + + // Pop the front of fromPhoneQueue, holding the mutex only briefly while we pop. + NimBLEAttValue val; + { // scope for fromPhoneMutex mutex + std::lock_guard guard(fromPhoneMutex); + val = fromPhoneQueue[0]; + + // Shift the rest of the queue down + for (uint8_t i = 1; i < fromPhoneQueueSize; i++) { + fromPhoneQueue[i - 1] = fromPhoneQueue[i]; + } + + // Safe decrement due to onDisconnect + if (fromPhoneQueueSize > 0) + fromPhoneQueueSize--; + } + + handleToRadio(val.data(), val.length()); + } + } + /** * Subclasses can use this as a hook to provide custom notifications for their transport (i.e. bluetooth notifies) */ @@ -78,14 +324,22 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread { PhoneAPI::onNowHasData(fromRadioNum); + int currentNotifyCount = notifyCount.fetch_add(1); + uint8_t cc = bleServer->getConnectedCount(); - LOG_DEBUG("BLE notify fromNum: %d connections: %d", fromRadioNum, cc); + +#ifdef DEBUG_NIMBLE_NOTIFY + // This logging slows things down when there are lots of packets going to the phone, like initial connection: + LOG_DEBUG("BLE notify(%d) fromNum: %d connections: %d", currentNotifyCount, fromRadioNum, cc); +#endif uint8_t val[4]; put_le32(val, fromRadioNum); fromNumCharacteristic->setValue(val, sizeof(val)); #ifdef NIMBLE_TWO + // NOTE: I don't have any NIMBLE_TWO devices, but this line makes me suspicious, and I suspect it needs to just be + // notify(). fromNumCharacteristic->notify(val, sizeof(val), BLE_HS_CONN_HANDLE_NONE); #else fromNumCharacteristic->notify(); @@ -94,6 +348,54 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread /// Check the current underlying physical link to see if the client is currently connected virtual bool checkIsConnected() { return bleServer && bleServer->getConnectedCount() > 0; } + + void requestHighThroughputConnection(uint16_t conn_handle) + { + /* Request a lower-latency, higher-throughput BLE connection. + + This comes at the cost of higher power consumption, so we may want to only use this for initial setup, and then switch to + a slower mode. + + See https://developer.apple.com/library/archive/qa/qa1931/_index.html for formulas to calculate values, iOS/macOS + constraints, and recommendations. (Android doesn't have specific constraints, but seems to be compatible with the Apple + recommendations.) + + Selected settings: + minInterval (units of 1.25ms): 7.5ms = 6 (lower than the Apple recommended minimum, but allows faster when the client + supports it.) + maxInterval (units of 1.25ms): 15ms = 12 + latency: 0 (don't allow peripheral to skip any connection events) + timeout (units of 10ms): 6 seconds = 600 (supervision timeout) + + These are intentionally aggressive to prioritize speed over power consumption, but are only used for a few seconds at + setup. Not worth adjusting much. + */ + LOG_INFO("BLE requestHighThroughputConnection"); + bleServer->updateConnParams(conn_handle, 6, 12, 0, 600); + } + + void requestLowerPowerConnection(uint16_t conn_handle) + { + /* Request a lower power consumption (but higher latency, lower throughput) BLE connection. + + This is suitable for steady-state operation after initial setup is complete. + + See https://developer.apple.com/library/archive/qa/qa1931/_index.html for formulas to calculate values, iOS/macOS + constraints, and recommendations. (Android doesn't have specific constraints, but seems to be compatible with the Apple + recommendations.) + + Selected settings: + minInterval (units of 1.25ms): 30ms = 24 + maxInterval (units of 1.25ms): 50ms = 40 + latency: 2 (allow peripheral to skip up to 2 consecutive connection events to save power) + timeout (units of 10ms): 6 seconds = 600 (supervision timeout) + + There's an opportunity for tuning here if anyone wants to do some power measurements, but these should allow 10-20 packets + per second. + */ + LOG_INFO("BLE requestLowerPowerConnection"); + bleServer->updateConnParams(conn_handle, 24, 40, 2, 600); + } }; static BluetoothPhoneAPI *bluetoothPhoneAPI; @@ -113,18 +415,45 @@ class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks #endif { + // CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's runOnce. + // Assumption: onWrite is serialized by NimBLE, so we don't need to lock here against multiple concurrent onWrite calls. + + int currentWriteCount = bluetoothPhoneAPI->writeCount.fetch_add(1); + +#ifdef DEBUG_NIMBLE_ON_WRITE_TIMING + int startMillis = millis(); + LOG_DEBUG("BLE onWrite(%d): start millis=%d", currentWriteCount, startMillis); +#endif + auto val = pCharacteristic->getValue(); if (memcmp(lastToRadio, val.data(), val.length()) != 0) { - if (bluetoothPhoneAPI->queue_size < 3) { + if (bluetoothPhoneAPI->fromPhoneQueueSize < NIMBLE_BLUETOOTH_FROM_PHONE_QUEUE_SIZE) { + // Note: the comparison above is safe without a mutex because we are the only method that *increases* + // fromPhoneQueueSize. (It's okay if fromPhoneQueueSize *decreases* in the main task meanwhile.) memcpy(lastToRadio, val.data(), val.length()); - std::lock_guard guard(bluetoothPhoneAPI->nimble_mutex); - bluetoothPhoneAPI->nimble_queue.at(bluetoothPhoneAPI->queue_size) = val; - bluetoothPhoneAPI->queue_size++; + + { // scope for fromPhoneMutex mutex + // Append to fromPhoneQueue, protected by fromPhoneMutex. Hold the mutex as briefly as possible. + std::lock_guard guard(bluetoothPhoneAPI->fromPhoneMutex); + bluetoothPhoneAPI->fromPhoneQueue.at(bluetoothPhoneAPI->fromPhoneQueueSize) = val; + bluetoothPhoneAPI->fromPhoneQueueSize++; + } + + // After releasing the mutex, schedule immediate processing of the new packet. bluetoothPhoneAPI->setIntervalFromNow(0); + concurrency::mainDelay.interrupt(); // wake up main loop if sleeping + +#ifdef DEBUG_NIMBLE_ON_WRITE_TIMING + int finishMillis = millis(); + LOG_DEBUG("BLE onWrite(%d): append to fromPhoneQueue took %u ms. numBytes=%d", currentWriteCount, + finishMillis - startMillis, val.length()); +#endif + } else { + LOG_WARN("BLE onWrite(%d): Drop ToRadio packet, fromPhoneQueue full (%u bytes)", currentWriteCount, val.length()); } } else { - LOG_DEBUG("Drop duplicate ToRadio packet (%u bytes)", val.length()); + LOG_DEBUG("BLE onWrite(%d): Drop duplicate ToRadio packet (%u bytes)", currentWriteCount, val.length()); } } }; @@ -137,32 +466,111 @@ class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks virtual void onRead(NimBLECharacteristic *pCharacteristic) #endif { - bluetoothPhoneAPI->phoneWants = true; - bluetoothPhoneAPI->setIntervalFromNow(0); - std::lock_guard guard(bluetoothPhoneAPI->nimble_mutex); // BLE callbacks run in NimBLE task + // In some cases, it seems a new connection starts with a read. + // The API has no bytes to send, leading to a timeout. This short-circuits this problem. + if (!bluetoothPhoneAPI->isConnected()) + return; + // CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's runOnce. - if (!bluetoothPhoneAPI->hasChecked) { - // Fetch payload on demand; prefetch keeps this fast for the first read. - bluetoothPhoneAPI->numBytes = bluetoothPhoneAPI->getFromRadio(bluetoothPhoneAPI->fromRadioBytes); - bluetoothPhoneAPI->hasChecked = true; - } + int currentReadCount = bluetoothPhoneAPI->readCount.fetch_add(1); + int tries = 0; + int startMillis = millis(); - pCharacteristic->setValue(bluetoothPhoneAPI->fromRadioBytes, bluetoothPhoneAPI->numBytes); - - if (bluetoothPhoneAPI->numBytes != 0) { -#ifdef NIMBLE_TWO - // Notify immediately so subscribed clients see the packet without an extra read. - pCharacteristic->notify(bluetoothPhoneAPI->fromRadioBytes, bluetoothPhoneAPI->numBytes, BLE_HS_CONN_HANDLE_NONE); -#else - pCharacteristic->notify(); +#ifdef DEBUG_NIMBLE_ON_READ_TIMING + LOG_DEBUG("BLE onRead(%d): start millis=%d", currentReadCount, startMillis); #endif + + // Is there a packet ready to go, or do we have to ask the main task to get one for us? + if (bluetoothPhoneAPI->toPhoneQueueSize > 0) { + // Note: the comparison above is safe without a mutex because we are the only method that *decreases* + // toPhoneQueueSize. (It's okay if toPhoneQueueSize *increases* in the main task meanwhile.) + + // There's already a packet queued. Great! We don't need to wait for onReadCallbackIsWaitingForData. +#ifdef DEBUG_NIMBLE_ON_READ_TIMING + LOG_DEBUG("BLE onRead(%d): packet already waiting, no need to set onReadCallbackIsWaitingForData", currentReadCount); +#endif + } else { + // Tell the main task that we'd like a packet. + bluetoothPhoneAPI->onReadCallbackIsWaitingForData = true; + + // Wait for the main task to produce a packet for us, up to about 20 seconds. + // It normally takes just a few milliseconds, but at initial startup, etc, the main task can get blocked for longer + // doing various setup tasks. + while (bluetoothPhoneAPI->onReadCallbackIsWaitingForData && tries < 4000) { + // Schedule the main task runOnce to run ASAP. + bluetoothPhoneAPI->setIntervalFromNow(0); + concurrency::mainDelay.interrupt(); // wake up main loop if sleeping + + if (!bluetoothPhoneAPI->onReadCallbackIsWaitingForData) { + // we may be able to break even before a delay, if the call to interrupt woke up the main loop and it ran + // already +#ifdef DEBUG_NIMBLE_ON_READ_TIMING + LOG_DEBUG("BLE onRead(%d): broke before delay after %u ms, %d tries", currentReadCount, + millis() - startMillis, tries); +#endif + break; + } + + // This delay happens in the NimBLE FreeRTOS task, which really can't do anything until we get a value back. + // No harm in polling pretty frequently. + delay(tries < 20 ? 1 : 5); + tries++; + + if (tries == 4000) { + LOG_WARN( + "BLE onRead(%d): timeout waiting for data after %u ms, %d tries, giving up and returning 0-size response", + currentReadCount, millis() - startMillis, tries); + } + } } - if (bluetoothPhoneAPI->numBytes != 0) // if we did send something, queue it up right away to reload + // Pop from toPhoneQueue, protected by toPhoneMutex. Hold the mutex as briefly as possible. + uint8_t fromRadioBytes[meshtastic_FromRadio_size] = {0}; // Stack buffer for getFromRadio packet + size_t numBytes = 0; + { // scope for toPhoneMutex mutex + std::lock_guard guard(bluetoothPhoneAPI->toPhoneMutex); + size_t toPhoneQueueSize = bluetoothPhoneAPI->toPhoneQueueSize.load(); + if (toPhoneQueueSize > 0) { + // Copy from the front of the toPhoneQueue + memcpy(fromRadioBytes, bluetoothPhoneAPI->toPhoneQueue[0].data(), bluetoothPhoneAPI->toPhoneQueueByteSizes[0]); + numBytes = bluetoothPhoneAPI->toPhoneQueueByteSizes[0]; + + // Shift the rest of the queue down + for (uint8_t i = 1; i < toPhoneQueueSize; i++) { + memcpy(bluetoothPhoneAPI->toPhoneQueue[i - 1].data(), bluetoothPhoneAPI->toPhoneQueue[i].data(), + bluetoothPhoneAPI->toPhoneQueueByteSizes[i]); + // The above line is similar to: + // bluetoothPhoneAPI->toPhoneQueue[i - 1] = bluetoothPhoneAPI->toPhoneQueue[i] + // but is usually faster because it doesn't have to copy all the trailing bytes beyond + // toPhoneQueueByteSizes[i]. + // + // We deliberately use an array here (and pay the CPU cost of some memcpy) to avoid synchronizing dynamic + // memory allocations and frees across FreeRTOS tasks. + + bluetoothPhoneAPI->toPhoneQueueByteSizes[i - 1] = bluetoothPhoneAPI->toPhoneQueueByteSizes[i]; + } + + // Safe decrement due to onDisconnect + if (bluetoothPhoneAPI->toPhoneQueueSize > 0) + bluetoothPhoneAPI->toPhoneQueueSize--; + } else { + // nothing in the toPhoneQueue; that's fine, and we'll just have numBytes=0. + } + } + +#ifdef DEBUG_NIMBLE_ON_READ_TIMING + int finishMillis = millis(); + LOG_DEBUG("BLE onRead(%d): onReadCallbackIsWaitingForData took %u ms, %d tries. numBytes=%d", currentReadCount, + finishMillis - startMillis, tries, numBytes); +#endif + + pCharacteristic->setValue(fromRadioBytes, numBytes); + + // If we sent something, wake up the main loop if it's sleeping in case there are more packets ready to enqueue. + if (numBytes != 0) { bluetoothPhoneAPI->setIntervalFromNow(0); - bluetoothPhoneAPI->numBytes = 0; - bluetoothPhoneAPI->hasChecked = false; - bluetoothPhoneAPI->phoneWants = false; + concurrency::mainDelay.interrupt(); // wake up main loop if sleeping + } } }; @@ -244,6 +652,13 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks if (screen) screen->endAlert(); } + + // Store the connection handle for future use +#ifdef NIMBLE_TWO + nimbleBluetoothConnHandle = connInfo.getConnHandle(); +#else + nimbleBluetoothConnHandle = desc->conn_handle; +#endif } #ifdef NIMBLE_TWO @@ -290,16 +705,29 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks bluetoothStatus->updateStatus(&newStatus); if (bluetoothPhoneAPI) { - std::lock_guard guard(bluetoothPhoneAPI->nimble_mutex); bluetoothPhoneAPI->close(); - bluetoothPhoneAPI->numBytes = 0; - bluetoothPhoneAPI->queue_size = 0; - bluetoothPhoneAPI->hasChecked = false; - bluetoothPhoneAPI->phoneWants = false; + + { // scope for fromPhoneMutex mutex + std::lock_guard guard(bluetoothPhoneAPI->fromPhoneMutex); + bluetoothPhoneAPI->fromPhoneQueueSize = 0; + } + + bluetoothPhoneAPI->onReadCallbackIsWaitingForData = false; + { // scope for toPhoneMutex mutex + std::lock_guard guard(bluetoothPhoneAPI->toPhoneMutex); + bluetoothPhoneAPI->toPhoneQueueSize = 0; + } + + bluetoothPhoneAPI->readCount = 0; + bluetoothPhoneAPI->notifyCount = 0; + bluetoothPhoneAPI->writeCount = 0; } // Clear the last ToRadio packet buffer to avoid rejecting first packet from new connection memset(lastToRadio, 0, sizeof(lastToRadio)); + + nimbleBluetoothConnHandle = -1; // -1 means "no connection" + #ifdef NIMBLE_TWO // Restart Advertising ble->startAdvertising(); @@ -436,17 +864,15 @@ void NimbleBluetooth::setupService() if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) { ToRadioCharacteristic = bleService->createCharacteristic(TORADIO_UUID, NIMBLE_PROPERTY::WRITE); // Allow notifications so phones can stream FromRadio without polling. - FromRadioCharacteristic = - bleService->createCharacteristic(FROMRADIO_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY); + FromRadioCharacteristic = bleService->createCharacteristic(FROMRADIO_UUID, NIMBLE_PROPERTY::READ); fromNumCharacteristic = bleService->createCharacteristic(FROMNUM_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ); logRadioCharacteristic = bleService->createCharacteristic(LOGRADIO_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ, 512U); } else { ToRadioCharacteristic = bleService->createCharacteristic( TORADIO_UUID, NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_AUTHEN | NIMBLE_PROPERTY::WRITE_ENC); - FromRadioCharacteristic = - bleService->createCharacteristic(FROMRADIO_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_AUTHEN | - NIMBLE_PROPERTY::READ_ENC | NIMBLE_PROPERTY::NOTIFY); + FromRadioCharacteristic = bleService->createCharacteristic( + FROMRADIO_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_AUTHEN | NIMBLE_PROPERTY::READ_ENC); fromNumCharacteristic = bleService->createCharacteristic(FROMNUM_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_AUTHEN | NIMBLE_PROPERTY::READ_ENC); From ffb168be00a576de79adf115f1c4a6581431959c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 19 Oct 2025 05:53:06 -0500 Subject: [PATCH 14/14] Update protobufs (#8398) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --- protobufs | 2 +- src/mesh/generated/meshtastic/mesh.pb.h | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/protobufs b/protobufs index 4a618380a..bf149bbdc 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 4a618380a0d80b476bb7f278008f811f456e0a5f +Subproject commit bf149bbdcce45ba7cd8643db7cb25e5c8815072b diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index d8d2f2e8a..059af57ae 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -282,6 +282,8 @@ typedef enum _meshtastic_HardwareModel { meshtastic_HardwareModel_HELTEC_WIRELESS_TRACKER_V2 = 113, /* LilyGo T-Watch Ultra */ meshtastic_HardwareModel_T_WATCH_ULTRA = 114, + /* Elecrow ThinkNode M3 */ + meshtastic_HardwareModel_THINKNODE_M3 = 115, /* ------------------------------------------------------------------------------------------------------------------------------------------ Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits. ------------------------------------------------------------------------------------------------------------------------------------------ */