From 2b754d4295330f1699e7152384def49a8c8e4408 Mon Sep 17 00:00:00 2001 From: Matthias Wientapper Date: Sat, 31 Jan 2026 23:17:48 +0100 Subject: [PATCH 01/40] cli_commands.md: `region` available via remote cli in 1.12.0 changed with #1476 --- docs/cli_commands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 6b4f6157..c316bd6c 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -642,7 +642,7 @@ **Usage:** - `region` -**Serial Only:** Yes +**Serial Only:** For firmware older than 1.12.0 --- From 23b4baa0665cdd762b1a909d1f1ce9f0a22d4fd8 Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Sat, 7 Feb 2026 16:04:01 +0100 Subject: [PATCH 02/40] Enable register patch heltec tracker v2 --- variants/heltec_tracker_v2/platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/variants/heltec_tracker_v2/platformio.ini b/variants/heltec_tracker_v2/platformio.ini index 36de671e..25d16f2f 100644 --- a/variants/heltec_tracker_v2/platformio.ini +++ b/variants/heltec_tracker_v2/platformio.ini @@ -26,6 +26,7 @@ build_flags = -D SX126X_DIO3_TCXO_VOLTAGE=1.8 -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 + -D SX126X_REGISTER_PATCH=1 -D PIN_BOARD_SDA=5 -D PIN_BOARD_SCL=6 -D PIN_USER_BTN=0 From 3ff1394dd231a1320a8cdc67e7b5ddbcb1ead31c Mon Sep 17 00:00:00 2001 From: Thane Gill Date: Sun, 8 Feb 2026 14:49:03 -0800 Subject: [PATCH 03/40] build.sh: add list and -l to list firmwares available to build. --- build.sh | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/build.sh b/build.sh index b7f95dd7..2b7c4824 100755 --- a/build.sh +++ b/build.sh @@ -7,6 +7,7 @@ sh build.sh [target] Commands: help|usage|-h|--help: Shows this message. + list|-l: List firmwares available to build. build-firmware : Build the firmware for the given build target. build-firmwares: Build all firmwares for all targets. build-matching-firmwares : Build all firmwares for build targets containing the string given for . @@ -46,20 +47,24 @@ $ sh build.sh build-firmware RAK_4631_repeater EOF } +# get a list of pio env names that start with "env:" +get_pio_envs() { + pio project config | grep 'env:' | sed 's/env://' +} + # Catch cries for help before doing anything else. case $1 in help|usage|-h|--help) global_usage exit 1 ;; + list|-l) + get_pio_envs + exit 0 + ;; esac -# get a list of pio env names that start with "env:" -get_pio_envs() { - echo $(pio project config | grep 'env:' | sed 's/env://') -} - # $1 should be the string to find (case insensitive) get_pio_envs_containing_string() { shopt -s nocasematch From 810fd561d232e4a2d344ffdc7346ce2d370ea177 Mon Sep 17 00:00:00 2001 From: Snayler <11491485+Snayler@users.noreply.github.com> Date: Mon, 9 Feb 2026 23:20:29 +0000 Subject: [PATCH 04/40] Enable TX LED for LilyGo LoRa32 V2.1_1.6 Working on my device, green TX LED starts blinking every time I transmit --- variants/lilygo_tlora_v2_1/platformio.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/variants/lilygo_tlora_v2_1/platformio.ini b/variants/lilygo_tlora_v2_1/platformio.ini index f27f57fd..c28f9001 100644 --- a/variants/lilygo_tlora_v2_1/platformio.ini +++ b/variants/lilygo_tlora_v2_1/platformio.ini @@ -18,7 +18,7 @@ build_flags = -D P_LORA_SCLK=5 ; SPI clock -D P_LORA_MISO=19 ; SPI MISO -D P_LORA_MOSI=27 ; SPI MOSI - -D P_LORA_TX_LED=2 ; LED pin for TX indication + -D P_LORA_TX_LED=25 ; LED pin for TX indication -D PIN_BOARD_SDA=21 -D PIN_BOARD_SCL=22 -D PIN_VBAT_READ=35 ; Battery voltage reading (analog pin) @@ -191,4 +191,4 @@ build_flags = ; -D CORE_DEBUG_LEVEL=3 lib_deps = ${LilyGo_TLora_V2_1_1_6.lib_deps} - ${esp32_ota.lib_deps} \ No newline at end of file + ${esp32_ota.lib_deps} From bafa2ccd2220fc194f9df64d26521458b711394a Mon Sep 17 00:00:00 2001 From: liamcottle Date: Tue, 10 Feb 2026 17:01:30 +1300 Subject: [PATCH 05/40] fix estimated timeout for multi byte path traces --- examples/companion_radio/MyMesh.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 96716091..a9ac1cf0 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1568,7 +1568,7 @@ void MyMesh::handleCmdFrame(size_t len) { sendDirect(pkt, &cmd_frame[10], path_len); uint32_t t = _radio->getEstAirtimeFor(pkt->payload_len + pkt->path_len + 2); - uint32_t est_timeout = calcDirectTimeoutMillisFor(t, path_len); + uint32_t est_timeout = calcDirectTimeoutMillisFor(t, path_len >> path_sz); out_frame[0] = RESP_CODE_SENT; out_frame[1] = 0; From f720338c03b25a01a9c14ec0dd293243f290cc4c Mon Sep 17 00:00:00 2001 From: dylan <1577856435@qq.com> Date: Wed, 11 Feb 2026 14:12:48 +0800 Subject: [PATCH 06/40] Fix WioTrackerL1 BLE companion: route sensors to Grove I2C bus (Wire1) Sensors connected via the Grove I2C connector (D18/D17) were not detected because the firmware scanned the OLED I2C bus (Wire, D14/D15) by default. Adding ENV_PIN_SDA/SCL flags directs EnvironmentSensorManager to use Wire1, matching the physical Grove connector pinout. Co-Authored-By: Claude Opus 4.6 --- variants/wio-tracker-l1/platformio.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/variants/wio-tracker-l1/platformio.ini b/variants/wio-tracker-l1/platformio.ini index 75651d69..da760b51 100644 --- a/variants/wio-tracker-l1/platformio.ini +++ b/variants/wio-tracker-l1/platformio.ini @@ -96,6 +96,8 @@ build_flags = ${WioTrackerL1.build_flags} -D PIN_BUZZER=12 -D QSPIFLASH=1 -D ADVERT_NAME='"@@MAC"' + -D ENV_PIN_SDA=PIN_WIRE1_SDA + -D ENV_PIN_SCL=PIN_WIRE1_SCL ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${WioTrackerL1.build_src_filter} From beff18c53bf98f63dd72825f14001962f2cb6a6b Mon Sep 17 00:00:00 2001 From: Rastislav Vysoky Date: Wed, 11 Feb 2026 09:34:41 +0100 Subject: [PATCH 07/40] fix usb and build for rak 3112 --- variants/rak3112/platformio.ini | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/variants/rak3112/platformio.ini b/variants/rak3112/platformio.ini index bc43ee04..d030e749 100644 --- a/variants/rak3112/platformio.ini +++ b/variants/rak3112/platformio.ini @@ -7,6 +7,7 @@ build_flags = -I variants/rak3112 -D RAK_3112=1 -D ESP32_CPU_FREQ=80 + -D ARDUINO_USB_CDC_ON_BOOT=1 -D P_LORA_DIO_1=47 -D P_LORA_NSS=7 -D P_LORA_RESET=8 @@ -131,14 +132,14 @@ lib_deps = extends = rak3112 build_flags = ${rak3112.build_flags} - -I examples/companion_radio/ui-new + -I examples/companion_radio/ui-orig -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 ; NOTE: DO NOT ENABLE --> -D MESH_PACKET_LOGGING=1 ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${rak3112.build_src_filter} +<../examples/companion_radio/*.cpp> - +<../examples/companion_radio/ui-new/*.cpp> + +<../examples/companion_radio/ui-orig/*.cpp> lib_deps = ${rak3112.lib_deps} densaugeo/base64 @ ~1.4.0 @@ -147,7 +148,7 @@ lib_deps = extends = rak3112 build_flags = ${rak3112.build_flags} - -I examples/companion_radio/ui-new + -I examples/companion_radio/ui-orig -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 ; dynamic, random PIN @@ -159,7 +160,7 @@ build_flags = build_src_filter = ${rak3112.build_src_filter} + +<../examples/companion_radio/*.cpp> - +<../examples/companion_radio/ui-new/*.cpp> + +<../examples/companion_radio/ui-orig/*.cpp> lib_deps = ${rak3112.lib_deps} densaugeo/base64 @ ~1.4.0 @@ -168,7 +169,7 @@ lib_deps = extends = rak3112 build_flags = ${rak3112.build_flags} - -I examples/companion_radio/ui-new + -I examples/companion_radio/ui-orig -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D WIFI_DEBUG_LOGGING=1 @@ -180,7 +181,7 @@ build_flags = build_src_filter = ${rak3112.build_src_filter} + +<../examples/companion_radio/*.cpp> - +<../examples/companion_radio/ui-new/*.cpp> + +<../examples/companion_radio/ui-orig/*.cpp> lib_deps = ${rak3112.lib_deps} densaugeo/base64 @ ~1.4.0 From fb025fb67e7d23b6eaaa3118b89d19f9e5c36b2a Mon Sep 17 00:00:00 2001 From: Leah Date: Wed, 11 Feb 2026 09:51:28 +0100 Subject: [PATCH 08/40] Add muted icon to show when buzzer is muted --- examples/companion_radio/ui-new/UITask.cpp | 8 ++++++++ examples/companion_radio/ui-new/UITask.h | 8 ++++++++ examples/companion_radio/ui-new/icons.h | 4 ++++ 3 files changed, 20 insertions(+) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index ae2d9375..265532be 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -131,6 +131,14 @@ class HomeScreen : public UIScreen { // fill the battery based on the percentage int fillWidth = (batteryPercentage * (iconWidth - 4)) / 100; display.fillRect(iconX + 2, iconY + 2, fillWidth, iconHeight - 4); + + // show muted icon if buzzer is muted +#ifdef PIN_BUZZER + if (_task->isBuzzerQuiet()) { + display.setColor(DisplayDriver::RED); + display.drawXbm(iconX - 9, iconY + 1, muted_icon, 8, 8); + } +#endif } CayenneLPP sensors_lpp; diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 02c3cafb..a77ad6e7 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -78,6 +78,14 @@ public: bool hasDisplay() const { return _display != NULL; } bool isButtonPressed() const; + bool isBuzzerQuiet() { +#ifdef PIN_BUZZER + return buzzer.isQuiet(); +#else + return true; +#endif + } + void toggleBuzzer(); bool getGPSState(); void toggleGPS(); diff --git a/examples/companion_radio/ui-new/icons.h b/examples/companion_radio/ui-new/icons.h index 5220f409..cbe23790 100644 --- a/examples/companion_radio/ui-new/icons.h +++ b/examples/companion_radio/ui-new/icons.h @@ -115,4 +115,8 @@ static const uint8_t advert_icon[] = { 0x38, 0x00, 0x00, 0x1C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x30, 0x04, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +static const uint8_t muted_icon[] = { + 0x20, 0x6a, 0xea, 0xe4, 0xe4, 0xea, 0x6a, 0x20 }; \ No newline at end of file From 77675ab4966a6efaf40e046585b4cd682f0aed11 Mon Sep 17 00:00:00 2001 From: taco Date: Fri, 6 Feb 2026 14:59:49 +1100 Subject: [PATCH 09/40] add -D ESP32_PLATFORM to esp32_base --- platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/platformio.ini b/platformio.ini index 69883271..c47e757e 100644 --- a/platformio.ini +++ b/platformio.ini @@ -59,6 +59,7 @@ platform = platformio/espressif32@6.11.0 monitor_filters = esp32_exception_decoder extra_scripts = merge-bin.py build_flags = ${arduino_base.build_flags} + -D ESP32_PLATFORM ; -D ESP32_CPU_FREQ=80 ; change it to your need build_src_filter = ${arduino_base.build_src_filter} From 5df139f3d634a9666e3b2d4d9fb40116ac3e0960 Mon Sep 17 00:00:00 2001 From: taco Date: Fri, 13 Feb 2026 12:43:04 +1100 Subject: [PATCH 10/40] update build.sh to support RP2040 and STM32 --- build.sh | 55 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/build.sh b/build.sh index 2b7c4824..313c4c47 100755 --- a/build.sh +++ b/build.sh @@ -64,6 +64,8 @@ case $1 in ;; esac +# cache project config json for use in get_platform_for_env() +PIO_CONFIG_JSON=$(pio project config --json-output) # $1 should be the string to find (case insensitive) get_pio_envs_containing_string() { @@ -87,6 +89,25 @@ get_pio_envs_ending_with_string() { done } +# get platform flag for a given environment +# $1 should be the environment name +get_platform_for_env() { + local env_name=$1 + echo "$PIO_CONFIG_JSON" | python3 -c " +import sys, json, re +data = json.load(sys.stdin) +for section, options in data: + if section == 'env:$env_name': + for key, value in options: + if key == 'build_flags': + for flag in value: + match = re.search(r'(ESP32_PLATFORM|NRF52_PLATFORM|STM32_PLATFORM|RP2040_PLATFORM)', flag) + if match: + print(match.group(1)) + sys.exit(0) +" +} + # disable all debug logging flags if DISABLE_DEBUG=1 is set disable_debug_flags() { if [ "$DISABLE_DEBUG" == "1" ]; then @@ -96,6 +117,8 @@ disable_debug_flags() { # build firmware for the provided pio env in $1 build_firmware() { + # get env platform for post build actions + ENV_PLATFORM=($(get_platform_for_env $1)) # get git commit sha COMMIT_HASH=$(git rev-parse --short HEAD) @@ -126,27 +149,31 @@ build_firmware() { # build firmware target pio run -e $1 - # build merge-bin for esp32 fresh install - if [ -f .pio/build/$1/firmware.bin ]; then + # build merge-bin for esp32 fresh install, copy .bins to out folder (e.g: Heltec_v3_room_server-v1.0.0-SHA.bin) + if [ "$ENV_PLATFORM" == "ESP32_PLATFORM" ]; then pio run -t mergebin -e $1 + cp .pio/build/$1/firmware.bin out/${FIRMWARE_FILENAME}.bin 2>/dev/null || true + cp .pio/build/$1/firmware-merged.bin out/${FIRMWARE_FILENAME}-merged.bin 2>/dev/null || true fi - # build .uf2 for nrf52 boards - if [[ -f .pio/build/$1/firmware.zip && -f .pio/build/$1/firmware.hex ]]; then + # build .uf2 for nrf52 boards, copy .uf2 and .zip to out folder (e.g: RAK_4631_Repeater-v1.0.0-SHA.uf2) + if [ "$ENV_PLATFORM" == "NRF52_PLATFORM" ]; then python3 bin/uf2conv/uf2conv.py .pio/build/$1/firmware.hex -c -o .pio/build/$1/firmware.uf2 -f 0xADA52840 + cp .pio/build/$1/firmware.uf2 out/${FIRMWARE_FILENAME}.uf2 2>/dev/null || true + cp .pio/build/$1/firmware.zip out/${FIRMWARE_FILENAME}.zip 2>/dev/null || true fi - # copy .bin, .uf2, and .zip to out folder - # e.g: Heltec_v3_room_server-v1.0.0-SHA.bin - # e.g: RAK_4631_Repeater-v1.0.0-SHA.uf2 + # for stm32, copy .bin and .hex to out folder + if [ "$ENV_PLATFORM" == "STM32_PLATFORM" ]; then + cp .pio/build/$1/firmware.bin out/${FIRMWARE_FILENAME}.bin 2>/dev/null || true + cp .pio/build/$1/firmware.hex out/${FIRMWARE_FILENAME}.hex 2>/dev/null || true + fi - # copy .bin for esp32 boards - cp .pio/build/$1/firmware.bin out/${FIRMWARE_FILENAME}.bin 2>/dev/null || true - cp .pio/build/$1/firmware-merged.bin out/${FIRMWARE_FILENAME}-merged.bin 2>/dev/null || true - - # copy .zip and .uf2 of nrf52 boards - cp .pio/build/$1/firmware.uf2 out/${FIRMWARE_FILENAME}.uf2 2>/dev/null || true - cp .pio/build/$1/firmware.zip out/${FIRMWARE_FILENAME}.zip 2>/dev/null || true + # for rp2040, copy .bin and .uf2 to out folder + if [ "$ENV_PLATFORM" == "RP2040_PLATFORM" ]; then + cp .pio/build/$1/firmware.bin out/${FIRMWARE_FILENAME}.bin 2>/dev/null || true + cp .pio/build/$1/firmware.uf2 out/${FIRMWARE_FILENAME}.uf2 2>/dev/null || true + fi } From 564a19d125a332bfe8cf20be8d66c792af88c2b2 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sat, 14 Feb 2026 15:50:06 +1100 Subject: [PATCH 11/40] * companion client repeat mode support --- examples/companion_radio/DataStore.cpp | 4 +-- examples/companion_radio/MyMesh.cpp | 42 +++++++++++++++++++++++++- examples/companion_radio/MyMesh.h | 4 ++- examples/companion_radio/NodePrefs.h | 1 + 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index c0f2c021..1239ea3d 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -212,7 +212,7 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no 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(pad, 1); // 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 @@ -247,7 +247,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_ 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(pad, 1); // 62 + 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 diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index a9ac1cf0..03a55cd8 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -56,6 +56,7 @@ #define CMD_SEND_ANON_REQ 57 #define CMD_SET_AUTOADD_CONFIG 58 #define CMD_GET_AUTOADD_CONFIG 59 +#define CMD_GET_ALLOWED_REPEAT_FREQ 60 // Stats sub-types for CMD_GET_STATS #define STATS_TYPE_CORE 0 @@ -88,6 +89,7 @@ #define RESP_CODE_TUNING_PARAMS 23 #define RESP_CODE_STATS 24 // v8+, second byte is stats type #define RESP_CODE_AUTOADD_CONFIG 25 +#define RESP_ALLOWED_REPEAT_FREQ 26 #define SEND_TIMEOUT_BASE_MILLIS 500 #define FLOOD_SEND_TIMEOUT_FACTOR 16.0f @@ -455,6 +457,10 @@ bool MyMesh::filterRecvFloodPacket(mesh::Packet* packet) { return false; } +bool MyMesh::allowPacketForward(const mesh::Packet* packet) { + return _prefs.client_repeat != 0; +} + void MyMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) { // TODO: dynamic send_scope, depending on recipient and current 'home' Region if (send_scope.isNull()) { @@ -881,6 +887,24 @@ uint32_t MyMesh::getBLEPin() { return _active_ble_pin; } +struct FreqRange { + uint32_t lower_freq, upper_freq; +}; + +static FreqRange repeat_freq_ranges[] = { + { 433000, 433000 }, + { 869000, 869000 }, + { 918000, 918000 } +}; + +bool MyMesh::isValidClientRepeatFreq(uint32_t f) const { + for (int i = 0; i < sizeof(repeat_freq_ranges)/sizeof(repeat_freq_ranges[0]); i++) { + auto r = &repeat_freq_ranges[i]; + if (f >= r->lower_freq && f <= r->upper_freq) return true; + } + return false; +} + void MyMesh::startInterface(BaseSerialInterface &serial) { _serial = &serial; serial.enable(); @@ -1208,13 +1232,20 @@ void MyMesh::handleCmdFrame(size_t len) { i += 4; uint8_t sf = cmd_frame[i++]; uint8_t cr = cmd_frame[i++]; + uint8_t repeat = 0; // default - false + if (len > i) { + repeat = cmd_frame[i++]; // FIRMWARE_VER_CODE 9+ + } - if (freq >= 300000 && freq <= 2500000 && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7000 && + if (repeat && !isValidClientRepeatFreq(freq)) { + writeErrFrame(ERR_CODE_ILLEGAL_ARG); + } else if (freq >= 300000 && freq <= 2500000 && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7000 && bw <= 500000) { _prefs.sf = sf; _prefs.cr = cr; _prefs.freq = (float)freq / 1000.0; _prefs.bw = (float)bw / 1000.0; + _prefs.client_repeat = repeat; savePrefs(); radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); @@ -1741,6 +1772,15 @@ void MyMesh::handleCmdFrame(size_t len) { out_frame[i++] = RESP_CODE_AUTOADD_CONFIG; out_frame[i++] = _prefs.autoadd_config; _serial->writeFrame(out_frame, i); + } else if (cmd_frame[0] == CMD_GET_ALLOWED_REPEAT_FREQ) { + int i = 0; + out_frame[i++] = RESP_ALLOWED_REPEAT_FREQ; + for (int k = 0; k < sizeof(repeat_freq_ranges)/sizeof(repeat_freq_ranges[0]) && i + 8 < sizeof(out_frame); k++) { + auto r = &repeat_freq_ranges[k]; + memcpy(&out_frame[i], &r->lower_freq, 4); i += 4; + memcpy(&out_frame[i], &r->upper_freq, 4); i += 4; + } + _serial->writeFrame(out_frame, i); } else { writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); MESH_DEBUG_PRINTLN("ERROR: unknown command: %02X", cmd_frame[0]); diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 95265a19..ff549771 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -5,7 +5,7 @@ #include "AbstractUITask.h" /*------------ Frame Protocol --------------*/ -#define FIRMWARE_VER_CODE 8 +#define FIRMWARE_VER_CODE 9 #ifndef FIRMWARE_BUILD_DATE #define FIRMWARE_BUILD_DATE "29 Jan 2026" @@ -108,6 +108,7 @@ protected: int calcRxDelay(float score, uint32_t air_time) const override; uint8_t getExtraAckTransmitCount() const override; bool filterRecvFloodPacket(mesh::Packet* packet) override; + bool allowPacketForward(const mesh::Packet* packet) override; void sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis=0) override; void sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis=0) override; @@ -176,6 +177,7 @@ private: void checkCLIRescueCmd(); void checkSerialInterface(); + bool isValidClientRepeatFreq(uint32_t f) const; // helpers, short-cuts void saveChannels() { _store->saveChannels(this); } diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index d7ddd92a..f2a52f41 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -28,4 +28,5 @@ struct NodePrefs { // persisted to file 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 client_repeat; }; \ No newline at end of file From 0abac357445ff790e7b9b58d1987a14d24e13728 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sat, 14 Feb 2026 16:45:41 +1100 Subject: [PATCH 12/40] * client_repeat state now in _DEVICE_INFO response --- examples/companion_radio/MyMesh.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 03a55cd8..87d3091a 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -928,6 +928,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+ _serial->writeFrame(out_frame, i); } else if (cmd_frame[0] == CMD_APP_START && len >= 8) { // sent when app establishes connection, respond with node ID From e2571accbec7a24cd73e71bdd01c2ca1f091f6a8 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Sun, 15 Feb 2026 17:24:37 +1100 Subject: [PATCH 13/40] * ver 1.13.0 --- examples/companion_radio/MyMesh.h | 4 ++-- examples/simple_repeater/MyMesh.h | 4 ++-- examples/simple_room_server/MyMesh.h | 4 ++-- examples/simple_sensor/SensorMesh.h | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index ff549771..1c5813eb 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -8,11 +8,11 @@ #define FIRMWARE_VER_CODE 9 #ifndef FIRMWARE_BUILD_DATE -#define FIRMWARE_BUILD_DATE "29 Jan 2026" +#define FIRMWARE_BUILD_DATE "15 Feb 2026" #endif #ifndef FIRMWARE_VERSION -#define FIRMWARE_VERSION "v1.12.0" +#define FIRMWARE_VERSION "v1.13.0" #endif #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 7a51b4a9..8388e29c 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -69,11 +69,11 @@ struct NeighbourInfo { }; #ifndef FIRMWARE_BUILD_DATE - #define FIRMWARE_BUILD_DATE "29 Jan 2026" + #define FIRMWARE_BUILD_DATE "15 Feb 2026" #endif #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.12.0" + #define FIRMWARE_VERSION "v1.13.0" #endif #define FIRMWARE_ROLE "repeater" diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index b4529e77..d21e225f 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -26,11 +26,11 @@ /* ------------------------------ Config -------------------------------- */ #ifndef FIRMWARE_BUILD_DATE - #define FIRMWARE_BUILD_DATE "29 Jan 2026" + #define FIRMWARE_BUILD_DATE "15 Feb 2026" #endif #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.12.0" + #define FIRMWARE_VERSION "v1.13.0" #endif #ifndef LORA_FREQ diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 4bc0d784..7131db75 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -33,11 +33,11 @@ #define PERM_RECV_ALERTS_HI (1 << 7) // high priority alerts #ifndef FIRMWARE_BUILD_DATE - #define FIRMWARE_BUILD_DATE "29 Jan 2026" + #define FIRMWARE_BUILD_DATE "15 Feb 2026" #endif #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.12.0" + #define FIRMWARE_VERSION "v1.13.0" #endif #define FIRMWARE_ROLE "sensor" From cafc212bb2171b316a34f47e229d4dc42dfbbb65 Mon Sep 17 00:00:00 2001 From: recrof Date: Sun, 15 Feb 2026 11:25:27 +0100 Subject: [PATCH 14/40] fix M5Stack Unit M6L build errors --- src/helpers/esp32/SerialBLEInterface.cpp | 1 + variants/m5stack_unit_c6l/platformio.ini | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/helpers/esp32/SerialBLEInterface.cpp b/src/helpers/esp32/SerialBLEInterface.cpp index eccfeca6..dcfa0e1e 100644 --- a/src/helpers/esp32/SerialBLEInterface.cpp +++ b/src/helpers/esp32/SerialBLEInterface.cpp @@ -1,4 +1,5 @@ #include "SerialBLEInterface.h" +#include "esp_mac.h" // See the following for generating UUIDs: // https://www.uuidgenerator.net/ diff --git a/variants/m5stack_unit_c6l/platformio.ini b/variants/m5stack_unit_c6l/platformio.ini index bbfdb4a1..a2b8b087 100644 --- a/variants/m5stack_unit_c6l/platformio.ini +++ b/variants/m5stack_unit_c6l/platformio.ini @@ -5,7 +5,7 @@ board_build.partitions = min_spiffs.csv ; get around 4mb flash limit build_flags = ${esp32c6_base.build_flags} ${sensor_base.build_flags} - -I variants/M5Stack_Unit_C6L + -I variants/m5stack_unit_c6l -D P_LORA_TX_LED=15 -D P_LORA_SCLK=20 -D P_LORA_MISO=22 From e8785dd9b0fd4ceace644f19d68d42ff8c8a8cbc Mon Sep 17 00:00:00 2001 From: realtag Date: Mon, 16 Feb 2026 22:35:20 +0000 Subject: [PATCH 15/40] discover sends a single repeater discovery request and populates the neighbor list; self is excluded --- examples/simple_repeater/MyMesh.cpp | 40 +++++++++++++++++++++++++++++ examples/simple_repeater/MyMesh.h | 1 + 2 files changed, 41 insertions(+) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 65e0cee5..e84ce08e 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -738,6 +738,37 @@ void MyMesh::onControlDataRecv(mesh::Packet* packet) { sendZeroHop(resp, getRetransmitDelay(resp)*4); // apply random delay (widened x4), as multiple nodes can respond to this } } + } else if (type == CTL_TYPE_NODE_DISCOVER_RESP && packet->payload_len >= 6) { + uint8_t node_type = packet->payload[0] & 0x0F; + if (node_type != ADV_TYPE_REPEATER) { + return; + } + if (packet->payload_len < 6 + PUB_KEY_SIZE) { + MESH_DEBUG_PRINTLN("onControlDataRecv: DISCOVER_RESP pubkey too short: %d", (uint32_t)packet->payload_len); + return; + } + + mesh::Identity id(&packet->payload[6]); + if (id.matches(self_id)) { + return; + } + putNeighbour(id, rtc_clock.getCurrentTime(), packet->getSNR()); + } +} + +void MyMesh::sendNodeDiscoverReq() { + if (_prefs.disable_fwd) return; + + uint8_t data[10]; + data[0] = CTL_TYPE_NODE_DISCOVER_REQ; // prefix_only=0 + data[1] = (1 << ADV_TYPE_REPEATER); + getRNG()->random(&data[2], 4); // tag + uint32_t since = 0; + memcpy(&data[6], &since, 4); + + auto pkt = createControlData(data, sizeof(data)); + if (pkt) { + sendZeroHop(pkt); } } @@ -1168,6 +1199,15 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } else { strcpy(reply, "Err - ??"); } + } else if (memcmp(command, "discover", 8) == 0) { + const char* sub = command + 8; + while (*sub == ' ') sub++; + if (*sub != 0) { + strcpy(reply, "Err - discover has no options"); + } else { + sendNodeDiscoverReq(); + strcpy(reply, "OK - Discover sent"); + } } else{ _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands } diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 8388e29c..6cded9cf 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -116,6 +116,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { #endif void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr); + void sendNodeDiscoverReq(); uint8_t handleLoginReq(const mesh::Identity& sender, const uint8_t* secret, uint32_t sender_timestamp, const uint8_t* data, bool is_flood); uint8_t handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data); uint8_t handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data); From 87c78a98bdef070b60f694eb8c4f43fb6bd57d83 Mon Sep 17 00:00:00 2001 From: realtag Date: Tue, 17 Feb 2026 01:04:14 +0000 Subject: [PATCH 16/40] discover.neighbors sends a tagged repeater discovery request and only accepts matching repeater responses --- examples/simple_repeater/MyMesh.cpp | 21 ++++++++++++++++++--- examples/simple_repeater/MyMesh.h | 2 ++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index e84ce08e..aec4ff3e 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -748,6 +748,16 @@ void MyMesh::onControlDataRecv(mesh::Packet* packet) { return; } + if (pending_discover_tag == 0 || millisHasNowPassed(pending_discover_until)) { + pending_discover_tag = 0; + return; + } + uint32_t tag; + memcpy(&tag, &packet->payload[2], 4); + if (tag != pending_discover_tag) { + return; + } + mesh::Identity id(&packet->payload[6]); if (id.matches(self_id)) { return; @@ -763,6 +773,8 @@ void MyMesh::sendNodeDiscoverReq() { data[0] = CTL_TYPE_NODE_DISCOVER_REQ; // prefix_only=0 data[1] = (1 << ADV_TYPE_REPEATER); getRNG()->random(&data[2], 4); // tag + memcpy(&pending_discover_tag, &data[2], 4); + pending_discover_until = futureMillis(30000); uint32_t since = 0; memcpy(&data[6], &since, 4); @@ -832,6 +844,9 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.advert_loc_policy = ADVERT_LOC_PREFS; _prefs.adc_multiplier = 0.0f; // 0.0f means use default board multiplier + + pending_discover_tag = 0; + pending_discover_until = 0; } void MyMesh::begin(FILESYSTEM *fs) { @@ -1199,11 +1214,11 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply } else { strcpy(reply, "Err - ??"); } - } else if (memcmp(command, "discover", 8) == 0) { - const char* sub = command + 8; + } else if (memcmp(command, "discover.neighbors", 18) == 0) { + const char* sub = command + 18; while (*sub == ' ') sub++; if (*sub != 0) { - strcpy(reply, "Err - discover has no options"); + strcpy(reply, "Err - discover.neighbors has no options"); } else { sendNodeDiscoverReq(); strcpy(reply, "OK - Discover sent"); diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 6cded9cf..f0e7cc10 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -97,6 +97,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { RegionEntry* load_stack[8]; RegionEntry* recv_pkt_region; RateLimiter discover_limiter, anon_limiter; + uint32_t pending_discover_tag; + unsigned long pending_discover_until; bool region_load_active; unsigned long dirty_contacts_expiry; #if MAX_NEIGHBOURS From bf9c6cb50f91de89dfbc7dffc24653f68816eda7 Mon Sep 17 00:00:00 2001 From: realtag Date: Tue, 17 Feb 2026 01:22:17 +0000 Subject: [PATCH 17/40] Increased the timeout timer to 60 seconds, up from 30 seconds. --- examples/simple_repeater/MyMesh.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index aec4ff3e..e2bf0330 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -774,7 +774,7 @@ void MyMesh::sendNodeDiscoverReq() { data[1] = (1 << ADV_TYPE_REPEATER); getRNG()->random(&data[2], 4); // tag memcpy(&pending_discover_tag, &data[2], 4); - pending_discover_until = futureMillis(30000); + pending_discover_until = futureMillis(60000); uint32_t since = 0; memcpy(&data[6], &since, 4); From 0770618ee2329efaead80bfb78986ee459fd2db4 Mon Sep 17 00:00:00 2001 From: realtag Date: Tue, 17 Feb 2026 01:39:04 +0000 Subject: [PATCH 18/40] Allow repeater discovery even if repeater mode is disabled on the requesting repeater. --- examples/simple_repeater/MyMesh.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index e2bf0330..20be1010 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -767,8 +767,6 @@ void MyMesh::onControlDataRecv(mesh::Packet* packet) { } void MyMesh::sendNodeDiscoverReq() { - if (_prefs.disable_fwd) return; - uint8_t data[10]; data[0] = CTL_TYPE_NODE_DISCOVER_REQ; // prefix_only=0 data[1] = (1 << ADV_TYPE_REPEATER); From 3e53df5082ec9da139c2ecd1dfd067ceadeff63a Mon Sep 17 00:00:00 2001 From: 3DPGG <3dpgg@protonmail.com> Date: Mon, 16 Feb 2026 17:41:52 -0800 Subject: [PATCH 19/40] Fix LilyGo_TLora_V2_1_1_6_terminal_chat build This change addresses two issues. The first is that the LilyGo_TLora_V2_1_1_6_terminal_chat build would try to compile simple_repeater/MyMesh.cpp. All other examples of terminal chat targets are instead building simple_secure_chat/main.cpp . This change would align this build to the rest of the builds. The second issue, found during the course of investigating the first, stems from simple_repeater/MyMesh.cpp using the MAX_NEIGHBOURS #define to control whether the neighbor list is kept. Repeaters that keep this list must define this value, and if the value is not defined, then all neighbor-related functionality is compiled out. However, the code that replies to REQ_TYPE_GET_NEIGHBOURS did not properly check for this #define, and thus any target that compiles simple_repeater/MyMesh.cpp without defining MAX_NEIGHBOURS would get an undefined variable compilation error. As a practical matter though, there are no targets that compile simple_repeater/MyMesh.cpp AND do not define MAX_NEIGHBOURS, except this build due to the first issue. As a result, the second issue is addressed only as a matter of completeness. The expected behavior with this change is that such a repeater would send a valid reply indicating zero known neighbors. --- examples/simple_repeater/MyMesh.cpp | 4 ++++ variants/lilygo_tlora_v2_1/platformio.ini | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 65e0cee5..edbc2c42 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -292,6 +292,7 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t // create copy of neighbours list, skipping empty entries so we can sort it separately from main list int16_t neighbours_count = 0; +#if MAX_NEIGHBOURS NeighbourInfo* sorted_neighbours[MAX_NEIGHBOURS]; for (int i = 0; i < MAX_NEIGHBOURS; i++) { auto neighbour = &neighbours[i]; @@ -327,6 +328,7 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t return a->snr < b->snr; // asc }); } +#endif // build results buffer int results_count = 0; @@ -341,6 +343,7 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t break; } +#if MAX_NEIGHBOURS // add next neighbour to results auto neighbour = sorted_neighbours[index + offset]; uint32_t heard_seconds_ago = getRTCClock()->getCurrentTime() - neighbour->heard_timestamp; @@ -348,6 +351,7 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t memcpy(&results_buffer[results_offset], &heard_seconds_ago, 4); results_offset += 4; memcpy(&results_buffer[results_offset], &neighbour->snr, 1); results_offset += 1; results_count++; +#endif } diff --git a/variants/lilygo_tlora_v2_1/platformio.ini b/variants/lilygo_tlora_v2_1/platformio.ini index c28f9001..7e1330e6 100644 --- a/variants/lilygo_tlora_v2_1/platformio.ini +++ b/variants/lilygo_tlora_v2_1/platformio.ini @@ -65,7 +65,7 @@ build_flags = ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${LilyGo_TLora_V2_1_1_6.build_src_filter} - +<../examples/simple_repeater> + +<../examples/simple_secure_chat/main.cpp> lib_deps = ${LilyGo_TLora_V2_1_1_6.lib_deps} densaugeo/base64 @ ~1.4.0 From 5de3e1bf32fd1a9f1d1966c92f94b5414ef44b84 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 17 Feb 2026 20:10:13 +1100 Subject: [PATCH 20/40] * repeater: slight increase to default direct.txdelay --- examples/simple_repeater/MyMesh.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index edbc2c42..692fcafe 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -775,7 +775,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.airtime_factor = 1.0; // one half _prefs.rx_delay_base = 0.0f; // turn off by default, was 10.0; _prefs.tx_delay_factor = 0.5f; // was 0.25f - _prefs.direct_tx_delay_factor = 0.2f; // was zero + _prefs.direct_tx_delay_factor = 0.3f; // was 0.2 StrHelper::strncpy(_prefs.node_name, ADVERT_NAME, sizeof(_prefs.node_name)); _prefs.node_lat = ADVERT_LAT; _prefs.node_lon = ADVERT_LON; From 2e0029812883c14485833064ec2e8ed401dc17c9 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 17 Feb 2026 20:25:56 +1100 Subject: [PATCH 21/40] * companion: retransmit delays now hard-coded (only for client repeat mode) --- examples/companion_radio/MyMesh.cpp | 9 +++++++++ examples/companion_radio/MyMesh.h | 2 ++ 2 files changed, 11 insertions(+) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 87d3091a..99b14952 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -257,6 +257,15 @@ int MyMesh::calcRxDelay(float score, uint32_t air_time) const { return (int)((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time); } +uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) { + uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * 0.5f); + return getRNG()->nextInt(0, 5*t + 1); +} +uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) { + uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * 0.2f); + return getRNG()->nextInt(0, 5*t + 1); +} + uint8_t MyMesh::getExtraAckTransmitCount() const { return _prefs.multi_acks; } diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 1c5813eb..e3c10985 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -106,6 +106,8 @@ protected: float getAirtimeBudgetFactor() const override; int getInterferenceThreshold() const override; int calcRxDelay(float score, uint32_t air_time) const override; + uint32_t getRetransmitDelay(const mesh::Packet *packet) override; + uint32_t getDirectRetransmitDelay(const mesh::Packet *packet) override; uint8_t getExtraAckTransmitCount() const override; bool filterRecvFloodPacket(mesh::Packet* packet) override; bool allowPacketForward(const mesh::Packet* packet) override; From ffc9815e9a26a01245f6032fa341d0ba88d8b41d Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Tue, 17 Feb 2026 23:54:33 +0100 Subject: [PATCH 22/40] Fix packet pool leak when rx queue is full MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PacketQueue::add() silently dropped packets when the queue was at capacity. The packet pointer was lost — never enqueued, never returned to the unused pool. Each occurrence permanently shrank the 32-packet pool until allocNew() returned NULL and the node went deaf. Return bool from add() and free the packet back to the pool on failure. --- src/helpers/StaticPoolPacketManager.cpp | 16 +++++++++++----- src/helpers/StaticPoolPacketManager.h | 2 +- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/helpers/StaticPoolPacketManager.cpp b/src/helpers/StaticPoolPacketManager.cpp index 4f28eac6..125efb75 100644 --- a/src/helpers/StaticPoolPacketManager.cpp +++ b/src/helpers/StaticPoolPacketManager.cpp @@ -55,15 +55,15 @@ mesh::Packet* PacketQueue::removeByIdx(int i) { return item; } -void PacketQueue::add(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) { +bool PacketQueue::add(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) { if (_num == _size) { - // TODO: log "FATAL: queue is full!" - return; + return false; } _table[_num] = packet; _pri_table[_num] = priority; _schedule_table[_num] = scheduled_for; _num++; + return true; } StaticPoolPacketManager::StaticPoolPacketManager(int pool_size): unused(pool_size), send_queue(pool_size), rx_queue(pool_size) { @@ -82,7 +82,10 @@ void StaticPoolPacketManager::free(mesh::Packet* packet) { } void StaticPoolPacketManager::queueOutbound(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for) { - send_queue.add(packet, priority, scheduled_for); + if (!send_queue.add(packet, priority, scheduled_for)) { + MESH_DEBUG_PRINTLN("queueOutbound: send queue full, dropping packet"); + free(packet); + } } mesh::Packet* StaticPoolPacketManager::getNextOutbound(uint32_t now) { @@ -106,7 +109,10 @@ mesh::Packet* StaticPoolPacketManager::removeOutboundByIdx(int i) { } void StaticPoolPacketManager::queueInbound(mesh::Packet* packet, uint32_t scheduled_for) { - rx_queue.add(packet, 0, scheduled_for); + if (!rx_queue.add(packet, 0, scheduled_for)) { + MESH_DEBUG_PRINTLN("queueInbound: rx queue full, dropping packet"); + free(packet); + } } mesh::Packet* StaticPoolPacketManager::getNextInbound(uint32_t now) { return rx_queue.get(now); diff --git a/src/helpers/StaticPoolPacketManager.h b/src/helpers/StaticPoolPacketManager.h index bbf4b193..52c299db 100644 --- a/src/helpers/StaticPoolPacketManager.h +++ b/src/helpers/StaticPoolPacketManager.h @@ -11,7 +11,7 @@ class PacketQueue { public: PacketQueue(int max_entries); mesh::Packet* get(uint32_t now); - void add(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for); + bool add(mesh::Packet* packet, uint8_t priority, uint32_t scheduled_for); int count() const { return _num; } int countBefore(uint32_t now) const; mesh::Packet* itemAt(int i) const { return _table[i]; } From 1500a5a9cbfe2df8bbd06ba08e5e58bd5f319f5e Mon Sep 17 00:00:00 2001 From: taco Date: Wed, 18 Feb 2026 15:35:20 +1100 Subject: [PATCH 23/40] add get bootloader.ver command for nrf52 --- src/MeshCore.h | 1 + src/helpers/CommonCLI.cpp | 11 +++++++++++ src/helpers/NRF52Board.cpp | 19 +++++++++++++++++++ src/helpers/NRF52Board.h | 1 + 4 files changed, 32 insertions(+) diff --git a/src/MeshCore.h b/src/MeshCore.h index f194cdeb..70cd0f06 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -55,6 +55,7 @@ public: virtual uint32_t getGpio() { return 0; } virtual void setGpio(uint32_t values) {} virtual uint8_t getStartupReason() const = 0; + virtual bool getBootloaderVersion(char* version, size_t max_len) { return false; } virtual bool startOTAUpdate(const char* id, char reply[]) { return false; } // not supported // Power management interface (boards with power management override these) diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 6dcf7018..263eb665 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -362,6 +362,17 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch } else if (memcmp(config, "bridge.secret", 13) == 0) { sprintf(reply, "> %s", _prefs->bridge_secret); #endif + } else if (memcmp(config, "bootloader.ver", 14) == 0) { + #ifdef NRF52_PLATFORM + char ver[32]; + if (_board->getBootloaderVersion(ver, sizeof(ver))) { + sprintf(reply, "> %s", ver); + } else { + strcpy(reply, "> unknown"); + } + #else + strcpy(reply, "ERROR: unsupported"); + #endif } else if (memcmp(config, "adc.multiplier", 14) == 0) { float adc_mult = _board->getAdcMultiplier(); if (adc_mult == 0.0f) { diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index 1db858f5..2c8753d4 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -297,6 +297,25 @@ float NRF52Board::getMCUTemperature() { return temp * 0.25f; // Convert to *C } +bool NRF52Board::getBootloaderVersion(char* out, size_t max_len) { + static const char BOOTLOADER_MARKER[] = "UF2 Bootloader "; + const uint8_t* flash = (const uint8_t*)0x000FB000; // earliest known info.txt location is 0xFB90B, latest is 0xFCC4B + + for (uint32_t i = 0; i < 0x3000 - (sizeof(BOOTLOADER_MARKER) - 1); i++) { + if (memcmp(&flash[i], BOOTLOADER_MARKER, sizeof(BOOTLOADER_MARKER) - 1) == 0) { + const char* ver = (const char*)&flash[i + sizeof(BOOTLOADER_MARKER) - 1]; + size_t len = 0; + while (len < max_len - 1 && ver[len] != '\0' && ver[len] != ' ' && ver[len] != '\n' && ver[len] != '\r') { + out[len] = ver[len]; + len++; + } + out[len] = '\0'; + return len > 0; // bootloader string is non-empty + } + } + return false; +} + bool NRF52Board::startOTAUpdate(const char *id, char reply[]) { // Config the peripheral connection with maximum bandwidth // more SRAM required by SoftDevice diff --git a/src/helpers/NRF52Board.h b/src/helpers/NRF52Board.h index 0332af07..96f67dc9 100644 --- a/src/helpers/NRF52Board.h +++ b/src/helpers/NRF52Board.h @@ -50,6 +50,7 @@ public: virtual uint8_t getStartupReason() const override { return startup_reason; } virtual float getMCUTemperature() override; virtual void reboot() override { NVIC_SystemReset(); } + virtual bool getBootloaderVersion(char* version, size_t max_len) override; virtual bool startOTAUpdate(const char *id, char reply[]) override; virtual void sleep(uint32_t secs) override; From 3e76161e9cacf739b5a575d3d897a13ce7c0c2e1 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Thu, 19 Feb 2026 14:37:51 +1100 Subject: [PATCH 24/40] * refactor of Contact/Client out_path_len (stored in files), from signed to unsigned byte (+2 squashed commits) Squashed commits: [f326e25] * misc [fa5152e] * new 'path mode' parsing in Dispatcher --- examples/companion_radio/MyMesh.cpp | 15 ++--- examples/simple_repeater/MyMesh.cpp | 61 ++++++++++-------- examples/simple_repeater/MyMesh.h | 1 + examples/simple_room_server/MyMesh.cpp | 43 ++++++------- examples/simple_secure_chat/main.cpp | 5 +- examples/simple_sensor/SensorMesh.cpp | 34 +++++----- examples/simple_sensor/SensorMesh.h | 2 +- src/Dispatcher.cpp | 86 ++++++++++++++------------ src/Dispatcher.h | 1 + src/Identity.h | 4 ++ src/Mesh.cpp | 74 +++++++++++----------- src/Mesh.h | 4 +- src/Packet.cpp | 13 +++- src/Packet.h | 8 +++ src/helpers/BaseChatMesh.cpp | 28 ++++----- src/helpers/ClientACL.cpp | 2 +- src/helpers/ClientACL.h | 4 +- src/helpers/ContactInfo.h | 4 +- 18 files changed, 222 insertions(+), 167 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 99b14952..f0f8e5fc 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -258,11 +258,11 @@ int MyMesh::calcRxDelay(float score, uint32_t air_time) const { } uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) { - uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * 0.5f); + uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * 0.5f); return getRNG()->nextInt(0, 5*t + 1); } uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) { - uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * 0.2f); + uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * 0.2f); return getRNG()->nextInt(0, 5*t + 1); } @@ -678,7 +678,7 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data, } } -bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) { +bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t _in_path_len, uint8_t* out_path, uint8_t _out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) { if (extra_type == PAYLOAD_TYPE_RESPONSE && extra_len > 4) { uint32_t tag; memcpy(&tag, extra, 4); @@ -785,9 +785,10 @@ uint32_t MyMesh::calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const { return SEND_TIMEOUT_BASE_MILLIS + (FLOOD_SEND_TIMEOUT_FACTOR * pkt_airtime_millis); } uint32_t MyMesh::calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const { + uint8_t path_hash_count = path_len & 63; return SEND_TIMEOUT_BASE_MILLIS + ((pkt_airtime_millis * DIRECT_SEND_PERHOP_FACTOR + DIRECT_SEND_PERHOP_EXTRA_MILLIS) * - (path_len + 1)); + (path_hash_count + 1)); } void MyMesh::onSendTimeout() {} @@ -1115,7 +1116,7 @@ void MyMesh::handleCmdFrame(size_t len) { } if (pkt) { if (len >= 2 && cmd_frame[1] == 1) { // optional param (1 = flood, 0 = zero hop) - sendFlood(pkt); + sendFlood(pkt); // TODO: which path_hash_size to use?? } else { sendZeroHop(pkt); } @@ -1127,7 +1128,7 @@ void MyMesh::handleCmdFrame(size_t len) { uint8_t *pub_key = &cmd_frame[1]; ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); if (recipient) { - recipient->out_path_len = -1; + recipient->out_path_len = OUT_PATH_UNKNOWN; // recipient->lastmod = ?? shouldn't be needed, app already has this version of contact dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); writeOKFrame(); @@ -1449,7 +1450,7 @@ void MyMesh::handleCmdFrame(size_t len) { memset(&req_data[2], 0, 3); // reserved getRNG()->random(&req_data[5], 4); // random blob to help make packet-hash unique auto save = recipient->out_path_len; // temporarily force sendRequest() to flood - recipient->out_path_len = -1; + recipient->out_path_len = OUT_PATH_UNKNOWN; int result = sendRequest(*recipient, req_data, sizeof(req_data), tag, est_timeout); recipient->out_path_len = save; if (result == MSG_SEND_FAILED) { diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index b6d855f6..7328f703 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -129,7 +129,7 @@ uint8_t MyMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secr } if (is_flood) { - client->out_path_len = -1; // need to rediscover out_path + client->out_path_len = OUT_PATH_UNKNOWN; // need to rediscover out_path } uint32_t now = getRTCClock()->getCurrentTimeUnique(); @@ -147,9 +147,12 @@ uint8_t MyMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* secr uint8_t MyMesh::handleAnonRegionsReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data) { if (anon_limiter.allow(rtc_clock.getCurrentTime())) { // request data has: {reply-path-len}{reply-path} - reply_path_len = *data++ & 0x3F; - memcpy(reply_path, data, reply_path_len); - // data += reply_path_len; + reply_path_len = *data & 63; + reply_path_hash_size = (*data >> 6) + 1; + data++; + + memcpy(reply_path, data, ((uint8_t)reply_path_len) * reply_path_hash_size); + // data += (uint8_t)reply_path_len * reply_path_hash_size; memcpy(reply_data, &sender_timestamp, 4); // prefix with sender_timestamp, like a tag uint32_t now = getRTCClock()->getCurrentTime(); @@ -163,9 +166,12 @@ uint8_t MyMesh::handleAnonRegionsReq(const mesh::Identity& sender, uint32_t send uint8_t MyMesh::handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data) { if (anon_limiter.allow(rtc_clock.getCurrentTime())) { // request data has: {reply-path-len}{reply-path} - reply_path_len = *data++ & 0x3F; - memcpy(reply_path, data, reply_path_len); - // data += reply_path_len; + reply_path_len = *data & 63; + reply_path_hash_size = (*data >> 6) + 1; + data++; + + memcpy(reply_path, data, ((uint8_t)reply_path_len) * reply_path_hash_size); + // data += (uint8_t)reply_path_len * reply_path_hash_size; memcpy(reply_data, &sender_timestamp, 4); // prefix with sender_timestamp, like a tag uint32_t now = getRTCClock()->getCurrentTime(); @@ -180,9 +186,12 @@ uint8_t MyMesh::handleAnonOwnerReq(const mesh::Identity& sender, uint32_t sender uint8_t MyMesh::handleAnonClockReq(const mesh::Identity& sender, uint32_t sender_timestamp, const uint8_t* data) { if (anon_limiter.allow(rtc_clock.getCurrentTime())) { // request data has: {reply-path-len}{reply-path} - reply_path_len = *data++ & 0x3F; - memcpy(reply_path, data, reply_path_len); - // data += reply_path_len; + reply_path_len = *data & 63; + reply_path_hash_size = (*data >> 6) + 1; + data++; + + memcpy(reply_path, data, ((uint8_t)reply_path_len) * reply_path_hash_size); + // data += (uint8_t)reply_path_len * reply_path_hash_size; memcpy(reply_data, &sender_timestamp, 4); // prefix with sender_timestamp, like a tag uint32_t now = getRTCClock()->getCurrentTime(); @@ -389,7 +398,7 @@ File MyMesh::openAppend(const char *fname) { bool MyMesh::allowPacketForward(const mesh::Packet *packet) { if (_prefs.disable_fwd) return false; - if (packet->isRouteFlood() && packet->path_len >= _prefs.flood_max) return false; + if (packet->isRouteFlood() && packet->getPathHashCount() >= _prefs.flood_max) return false; if (packet->isRouteFlood() && recv_pkt_region == NULL) { MESH_DEBUG_PRINTLN("allowPacketForward: unknown transport code, or wildcard not allowed for FLOOD packet"); return false; @@ -484,11 +493,11 @@ int MyMesh::calcRxDelay(float score, uint32_t air_time) const { } uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) { - uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.tx_delay_factor); + uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.tx_delay_factor); return getRNG()->nextInt(0, 5*t + 1); } uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) { - uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.direct_tx_delay_factor); + uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.direct_tx_delay_factor); return getRNG()->nextInt(0, 5*t + 1); } @@ -538,13 +547,14 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_len, PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); - if (path) sendFlood(path, SERVER_RESPONSE_DELAY); + if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } else if (reply_path_len < 0) { mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len); - if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY); + if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } else { mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len); - if (reply) sendDirect(reply, reply_path, reply_path_len, SERVER_RESPONSE_DELAY); + uint8_t path_len = ((reply_path_hash_size - 1) << 6) | (reply_path_len & 63); + if (reply) sendDirect(reply, reply_path, path_len, SERVER_RESPONSE_DELAY); } } } @@ -613,15 +623,15 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response mesh::Packet *path = createPathReturn(client->id, secret, packet->path, packet->path_len, PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); - if (path) sendFlood(path, SERVER_RESPONSE_DELAY); + if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } else { mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len); if (reply) { - if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT + if (client->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY); } else { - sendFlood(reply, SERVER_RESPONSE_DELAY); + sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } } } @@ -651,8 +661,8 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, mesh::Packet *ack = createAck(ack_hash); if (ack) { - if (client->out_path_len < 0) { - sendFlood(ack, TXT_ACK_DELAY); + if (client->out_path_len == OUT_PATH_UNKNOWN) { + sendFlood(ack, TXT_ACK_DELAY, packet->getPathHashSize()); } else { sendDirect(ack, client->out_path, client->out_path_len, TXT_ACK_DELAY); } @@ -679,8 +689,8 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len); if (reply) { - if (client->out_path_len < 0) { - sendFlood(reply, CLI_REPLY_DELAY_MILLIS); + if (client->out_path_len == OUT_PATH_UNKNOWN) { + sendFlood(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize()); } else { sendDirect(reply, client->out_path, client->out_path_len, CLI_REPLY_DELAY_MILLIS); } @@ -701,7 +711,8 @@ bool MyMesh::onPeerPathRecv(mesh::Packet *packet, int sender_idx, const uint8_t MESH_DEBUG_PRINTLN("PATH to client, path_len=%d", (uint32_t)path_len); auto client = acl.getClientByIdx(i); - memcpy(client->out_path, path, client->out_path_len = path_len); // store a copy of path, for sendDirect() + // store a copy of path, for sendDirect() + client->out_path_len = mesh::Packet::copyPath(client->out_path, path, path_len); client->last_activity = getRTCClock()->getCurrentTime(); } else { MESH_DEBUG_PRINTLN("onPeerPathRecv: invalid peer idx: %d", i); @@ -906,7 +917,7 @@ void MyMesh::sendSelfAdvertisement(int delay_millis, bool flood) { mesh::Packet *pkt = createSelfAdvert(); if (pkt) { if (flood) { - sendFlood(pkt, delay_millis); + sendFlood(pkt, delay_millis); // TODO: which path_hash_size to use?? } else { sendZeroHop(pkt, delay_millis); } diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index f0e7cc10..591f6366 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -92,6 +92,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { uint8_t reply_data[MAX_PACKET_PAYLOAD]; uint8_t reply_path[MAX_PATH_SIZE]; int8_t reply_path_len; + uint8_t reply_path_hash_size; TransportKeyStore key_store; RegionMap region_map, temp_map; RegionEntry* load_stack[8]; diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 598b14de..0b1d3a1b 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -73,13 +73,14 @@ void MyMesh::pushPostToClient(ClientInfo *client, PostInfo &post) { auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, client->shared_secret, reply_data, len); if (reply) { - if (client->out_path_len < 0) { - sendFlood(reply); + if (client->out_path_len == OUT_PATH_UNKNOWN) { + sendFlood(reply); // TODO: which path_hash_size to use? client->extra.room.ack_timeout = futureMillis(PUSH_ACK_TIMEOUT_FLOOD); } else { sendDirect(reply, client->out_path, client->out_path_len); - client->extra.room.ack_timeout = - futureMillis(PUSH_TIMEOUT_BASE + PUSH_ACK_TIMEOUT_FACTOR * (client->out_path_len + 1)); + + uint8_t path_hash_count = client->out_path_len & 63; + client->extra.room.ack_timeout = futureMillis(PUSH_TIMEOUT_BASE + PUSH_ACK_TIMEOUT_FACTOR * (path_hash_count + 1)); } _num_post_pushes++; // stats } else { @@ -264,17 +265,17 @@ const char *MyMesh::getLogDateTime() { } uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) { - uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.tx_delay_factor); + uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.tx_delay_factor); return getRNG()->nextInt(0, 5*t + 1); } uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) { - uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.direct_tx_delay_factor); + uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.direct_tx_delay_factor); return getRNG()->nextInt(0, 5*t + 1); } bool MyMesh::allowPacketForward(const mesh::Packet *packet) { if (_prefs.disable_fwd) return false; - if (packet->isRouteFlood() && packet->path_len >= _prefs.flood_max) return false; + if (packet->isRouteFlood() && packet->getPathHashCount() >= _prefs.flood_max) return false; return true; } @@ -333,7 +334,7 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m } if (packet->isRouteFlood()) { - client->out_path_len = -1; // need to rediscover out_path + client->out_path_len = OUT_PATH_UNKNOWN; // need to rediscover out_path } uint32_t now = getRTCClock()->getCurrentTimeUnique(); @@ -353,14 +354,14 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response mesh::Packet *path = createPathReturn(sender, client->shared_secret, packet->path, packet->path_len, PAYLOAD_TYPE_RESPONSE, reply_data, 13); - if (path) sendFlood(path, SERVER_RESPONSE_DELAY); + if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } else { mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, client->shared_secret, reply_data, 13); if (reply) { - if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT + if (client->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY); } else { - sendFlood(reply, SERVER_RESPONSE_DELAY); + sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } } } @@ -448,9 +449,9 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, uint32_t delay_millis; if (send_ack) { - if (client->out_path_len < 0) { + if (client->out_path_len == OUT_PATH_UNKNOWN) { mesh::Packet *ack = createAck(ack_hash); - if (ack) sendFlood(ack, TXT_ACK_DELAY); + if (ack) sendFlood(ack, TXT_ACK_DELAY, packet->getPathHashSize()); delay_millis = TXT_ACK_DELAY + REPLY_DELAY_MILLIS; } else { uint32_t d = TXT_ACK_DELAY; @@ -482,8 +483,8 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len); if (reply) { - if (client->out_path_len < 0) { - sendFlood(reply, delay_millis + SERVER_RESPONSE_DELAY); + if (client->out_path_len == OUT_PATH_UNKNOWN) { + sendFlood(reply, delay_millis + SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } else { sendDirect(reply, client->out_path, client->out_path_len, delay_millis + SERVER_RESPONSE_DELAY); } @@ -521,7 +522,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, // if client sends too quickly, evict() // RULE: only send keep_alive response DIRECT! - if (client->out_path_len >= 0) { + if (client->out_path_len != OUT_PATH_UNKNOWN) { uint32_t ack_hash; // calc ACK to prove to sender that we got request mesh::Utils::sha256((uint8_t *)&ack_hash, 4, data, 9, client->id.pub_key, PUB_KEY_SIZE); @@ -538,14 +539,14 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx, // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response mesh::Packet *path = createPathReturn(client->id, secret, packet->path, packet->path_len, PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); - if (path) sendFlood(path, SERVER_RESPONSE_DELAY); + if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } else { mesh::Packet *reply = createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len); if (reply) { - if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT + if (client->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY); } else { - sendFlood(reply, SERVER_RESPONSE_DELAY); + sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } } } @@ -563,7 +564,7 @@ bool MyMesh::onPeerPathRecv(mesh::Packet *packet, int sender_idx, const uint8_t if (i >= 0 && i < acl.getNumClients()) { // get from our known_clients table (sender SHOULD already be known in this context) MESH_DEBUG_PRINTLN("PATH to client, path_len=%d", (uint32_t)path_len); auto client = acl.getClientByIdx(i); - memcpy(client->out_path, path, client->out_path_len = path_len); // store a copy of path, for sendDirect() + client->out_path_len = mesh::Packet::copyPath(client->out_path, path, path_len); // store a copy of path, for sendDirect() client->last_activity = getRTCClock()->getCurrentTime(); } else { MESH_DEBUG_PRINTLN("onPeerPathRecv: invalid peer idx: %d", i); @@ -679,7 +680,7 @@ void MyMesh::sendSelfAdvertisement(int delay_millis, bool flood) { mesh::Packet *pkt = createSelfAdvert(); if (pkt) { if (flood) { - sendFlood(pkt, delay_millis); + sendFlood(pkt, delay_millis); // TODO: which path_hash_size to use? } else { sendZeroHop(pkt, delay_millis); } diff --git a/examples/simple_secure_chat/main.cpp b/examples/simple_secure_chat/main.cpp index a389ec74..c1ed710a 100644 --- a/examples/simple_secure_chat/main.cpp +++ b/examples/simple_secure_chat/main.cpp @@ -213,7 +213,7 @@ protected: } void onContactPathUpdated(const ContactInfo& contact) override { - Serial.printf("PATH to: %s, path_len=%d\n", contact.name, (int32_t) contact.out_path_len); + Serial.printf("PATH to: %s, path_len=%d\n", contact.name, (uint32_t) contact.out_path_len); saveContacts(); } @@ -266,8 +266,9 @@ protected: return SEND_TIMEOUT_BASE_MILLIS + (FLOOD_SEND_TIMEOUT_FACTOR * pkt_airtime_millis); } uint32_t calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t path_len) const override { + uint8_t path_hash_count = path_len & 63; return SEND_TIMEOUT_BASE_MILLIS + - ( (pkt_airtime_millis*DIRECT_SEND_PERHOP_FACTOR + DIRECT_SEND_PERHOP_EXTRA_MILLIS) * (path_len + 1)); + ( (pkt_airtime_millis*DIRECT_SEND_PERHOP_FACTOR + DIRECT_SEND_PERHOP_EXTRA_MILLIS) * (path_hash_count + 1)); } void onSendTimeout() override { diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index f05fb245..fc9ef310 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -258,7 +258,7 @@ void SensorMesh::sendAlert(const ClientInfo* c, Trigger* t) { auto pkt = createDatagram(PAYLOAD_TYPE_TXT_MSG, c->id, c->shared_secret, data, 5 + text_len); if (pkt) { - if (c->out_path_len >= 0) { // we have an out_path, so send DIRECT + if (c->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT sendDirect(pkt, c->out_path, c->out_path_len); } else { sendFlood(pkt); @@ -302,7 +302,7 @@ float SensorMesh::getAirtimeBudgetFactor() const { bool SensorMesh::allowPacketForward(const mesh::Packet* packet) { if (_prefs.disable_fwd) return false; - if (packet->isRouteFlood() && packet->path_len >= _prefs.flood_max) return false; + if (packet->isRouteFlood() && packet->getPathHashCount() >= _prefs.flood_max) return false; return true; } @@ -312,11 +312,11 @@ int SensorMesh::calcRxDelay(float score, uint32_t air_time) const { } uint32_t SensorMesh::getRetransmitDelay(const mesh::Packet* packet) { - uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.tx_delay_factor); + uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.tx_delay_factor); return getRNG()->nextInt(0, 6)*t; } uint32_t SensorMesh::getDirectRetransmitDelay(const mesh::Packet* packet) { - uint32_t t = (_radio->getEstAirtimeFor(packet->path_len + packet->payload_len + 2) * _prefs.direct_tx_delay_factor); + uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.direct_tx_delay_factor); return getRNG()->nextInt(0, 6)*t; } int SensorMesh::getInterferenceThreshold() const { @@ -360,7 +360,7 @@ uint8_t SensorMesh::handleLoginReq(const mesh::Identity& sender, const uint8_t* } if (is_flood) { - client->out_path_len = -1; // need to rediscover out_path + client->out_path_len = OUT_PATH_UNKNOWN; // need to rediscover out_path } uint32_t now = getRTCClock()->getCurrentTimeUnique(); @@ -468,10 +468,10 @@ void SensorMesh::onAnonDataRecv(mesh::Packet* packet, const uint8_t* secret, con // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_len, PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); - if (path) sendFlood(path, SERVER_RESPONSE_DELAY); + if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } else { mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len); - if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY); + if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } } } @@ -496,10 +496,10 @@ void SensorMesh::getPeerSharedSecret(uint8_t* dest_secret, int peer_idx) { } } -void SensorMesh::sendAckTo(const ClientInfo& dest, uint32_t ack_hash) { - if (dest.out_path_len < 0) { +void SensorMesh::sendAckTo(const ClientInfo& dest, uint32_t ack_hash, uint8_t path_hash_size) { + if (dest.out_path_len == OUT_PATH_UNKNOWN) { mesh::Packet* ack = createAck(ack_hash); - if (ack) sendFlood(ack, TXT_ACK_DELAY); + if (ack) sendFlood(ack, TXT_ACK_DELAY, path_hash_size); } else { uint32_t d = TXT_ACK_DELAY; if (getExtraAckTransmitCount() > 0) { @@ -537,14 +537,14 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response mesh::Packet* path = createPathReturn(from->id, secret, packet->path, packet->path_len, PAYLOAD_TYPE_RESPONSE, reply_data, reply_len); - if (path) sendFlood(path, SERVER_RESPONSE_DELAY); + if (path) sendFlood(path, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } else { mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from->id, secret, reply_data, reply_len); if (reply) { - if (from->out_path_len >= 0) { // we have an out_path, so send DIRECT + if (from->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT sendDirect(reply, from->out_path, from->out_path_len, SERVER_RESPONSE_DELAY); } else { - sendFlood(reply, SERVER_RESPONSE_DELAY); + sendFlood(reply, SERVER_RESPONSE_DELAY, packet->getPathHashSize()); } } } @@ -569,7 +569,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4); if (path) sendFlood(path, TXT_ACK_DELAY); } else { - sendAckTo(*from, ack_hash); + sendAckTo(*from, ack_hash, packet->getPathHashSize()); } } } else if (flags == TXT_TYPE_CLI_DATA) { @@ -596,8 +596,8 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, from->id, secret, temp, 5 + text_len); if (reply) { - if (from->out_path_len < 0) { - sendFlood(reply, CLI_REPLY_DELAY_MILLIS); + if (from->out_path_len == OUT_PATH_UNKNOWN) { + sendFlood(reply, CLI_REPLY_DELAY_MILLIS, packet->getPathHashSize()); } else { sendDirect(reply, from->out_path, from->out_path_len, CLI_REPLY_DELAY_MILLIS); } @@ -666,7 +666,7 @@ bool SensorMesh::onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint MESH_DEBUG_PRINTLN("PATH to contact, path_len=%d", (uint32_t) path_len); // NOTE: for this impl, we just replace the current 'out_path' regardless, whenever sender sends us a new out_path. // FUTURE: could store multiple out_paths per contact, and try to find which is the 'best'(?) - memcpy(from->out_path, path, from->out_path_len = path_len); // store a copy of path, for sendDirect() + from->out_path_len = mesh::Packet::copyPath(from->out_path, path, path_len); // store a copy of path, for sendDirect() from->last_activity = getRTCClock()->getCurrentTime(); // REVISIT: maybe make ALL out_paths non-persisted to minimise flash writes?? diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 7131db75..b15a400a 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -128,7 +128,7 @@ protected: void onControlDataRecv(mesh::Packet* packet) override; void onAckRecv(mesh::Packet* packet, uint32_t ack_crc) override; virtual bool handleIncomingMsg(ClientInfo& from, uint32_t timestamp, uint8_t* data, uint8_t flags, size_t len); - void sendAckTo(const ClientInfo& dest, uint32_t ack_hash); + void sendAckTo(const ClientInfo& dest, uint32_t ack_hash, uint8_t path_hash_size=1); private: FILESYSTEM* _fs; unsigned long next_local_advert, next_flood_advert; diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index 0a154985..12889fb8 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -108,6 +108,48 @@ void Dispatcher::loop() { checkSend(); } +bool Dispatcher::tryParsePacket(Packet* pkt, const uint8_t* raw, int len) { + int i = 0; + + pkt->header = raw[i++]; + if (pkt->getPayloadVer() > PAYLOAD_VER_1) { + MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): unsupported packet version", getLogDateTime()); + return false; + } + + if (pkt->hasTransportCodes()) { + memcpy(&pkt->transport_codes[0], &raw[i], 2); i += 2; + memcpy(&pkt->transport_codes[1], &raw[i], 2); i += 2; + } else { + pkt->transport_codes[0] = pkt->transport_codes[1] = 0; + } + + pkt->path_len = raw[i++]; + uint8_t path_mode = pkt->path_len >> 6; // upper 2 bits (legacy firmware: 00) + if (path_mode == 3) { // Reserved for future + MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): unsupported path mode: 3", getLogDateTime()); + return false; + } + + uint8_t path_byte_len = (pkt->path_len & 63) * pkt->getPathHashSize(); + if (path_byte_len > MAX_PATH_SIZE || i + path_byte_len > len) { + MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): partial or corrupt packet received, len=%d", getLogDateTime(), len); + return false; + } + + memcpy(pkt->path, &raw[i], path_byte_len); i += path_byte_len; + + pkt->payload_len = len - i; // payload is remainder + if (pkt->payload_len > sizeof(pkt->payload)) { + MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): packet payload too big, payload_len=%d", getLogDateTime(), (uint32_t)pkt->payload_len); + return false; + } + + memcpy(pkt->payload, &raw[i], pkt->payload_len); + + return true; // success +} + void Dispatcher::checkRecv() { Packet* pkt; float score; @@ -122,45 +164,14 @@ void Dispatcher::checkRecv() { if (pkt == NULL) { MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): WARNING: received data, no unused packets available!", getLogDateTime()); } else { - int i = 0; -#ifdef NODE_ID - uint8_t sender_id = raw[i++]; - if (sender_id == NODE_ID - 1 || sender_id == NODE_ID + 1) { // simulate that NODE_ID can only hear NODE_ID-1 or NODE_ID+1, eg. 3 can't hear 1 + if (tryParsePacket(pkt, raw, len)) { + pkt->_snr = _radio->getLastSNR() * 4.0f; + score = _radio->packetScore(_radio->getLastSNR(), len); + air_time = _radio->getEstAirtimeFor(len); + rx_air_time += air_time; } else { - _mgr->free(pkt); // put back into pool - return; - } -#endif - - pkt->header = raw[i++]; - if (pkt->hasTransportCodes()) { - memcpy(&pkt->transport_codes[0], &raw[i], 2); i += 2; - memcpy(&pkt->transport_codes[1], &raw[i], 2); i += 2; - } else { - pkt->transport_codes[0] = pkt->transport_codes[1] = 0; - } - pkt->path_len = raw[i++]; - - if (pkt->path_len > MAX_PATH_SIZE || i + pkt->path_len > len) { - MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): partial or corrupt packet received, len=%d", getLogDateTime(), len); _mgr->free(pkt); // put back into pool pkt = NULL; - } else { - memcpy(pkt->path, &raw[i], pkt->path_len); i += pkt->path_len; - - pkt->payload_len = len - i; // payload is remainder - if (pkt->payload_len > sizeof(pkt->payload)) { - MESH_DEBUG_PRINTLN("%s Dispatcher::checkRecv(): packet payload too big, payload_len=%d", getLogDateTime(), (uint32_t)pkt->payload_len); - _mgr->free(pkt); // put back into pool - pkt = NULL; - } else { - memcpy(pkt->payload, &raw[i], pkt->payload_len); - - pkt->_snr = _radio->getLastSNR() * 4.0f; - score = _radio->packetScore(_radio->getLastSNR(), len); - air_time = _radio->getEstAirtimeFor(len); - rx_air_time += air_time; - } } } } else { @@ -249,9 +260,6 @@ void Dispatcher::checkSend() { int len = 0; uint8_t raw[MAX_TRANS_UNIT]; -#ifdef NODE_ID - raw[len++] = NODE_ID; -#endif raw[len++] = outbound->header; if (outbound->hasTransportCodes()) { memcpy(&raw[len], &outbound->transport_codes[0], 2); len += 2; diff --git a/src/Dispatcher.h b/src/Dispatcher.h index 25a41d82..0a448c40 100644 --- a/src/Dispatcher.h +++ b/src/Dispatcher.h @@ -184,6 +184,7 @@ public: unsigned long futureMillis(int millis_from_now) const; private: + bool tryParsePacket(Packet* pkt, const uint8_t* raw, int len); void checkRecv(); void checkSend(); }; diff --git a/src/Identity.h b/src/Identity.h index c3ffcd75..008f7b5b 100644 --- a/src/Identity.h +++ b/src/Identity.h @@ -20,6 +20,10 @@ public: memcpy(dest, pub_key, PATH_HASH_SIZE); // hash is just prefix of pub_key return PATH_HASH_SIZE; } + int copyHashTo(uint8_t* dest, uint8_t len) const { + memcpy(dest, pub_key, len); // hash is just prefix of pub_key + return len; + } bool isHashMatch(const uint8_t* hash) const { return memcmp(hash, pub_key, PATH_HASH_SIZE) == 0; } diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 0548c907..57fee140 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -39,11 +39,6 @@ int Mesh::searchChannelsByHash(const uint8_t* hash, GroupChannel channels[], int } DispatcherAction Mesh::onRecvPacket(Packet* pkt) { - if (pkt->getPayloadVer() > PAYLOAD_VER_1) { // not supported in this firmware version - MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): unsupported packet version", getLogDateTime()); - return ACTION_RELEASE; - } - if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_TRACE) { if (pkt->path_len < MAX_PATH_SIZE) { uint8_t i = 0; @@ -70,14 +65,14 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { } if (pkt->isRouteDirect() && pkt->getPayloadType() == PAYLOAD_TYPE_CONTROL && (pkt->payload[0] & 0x80) != 0) { - if (pkt->path_len == 0) { + if (pkt->getPathHashCount() == 0) { onControlDataRecv(pkt); } // just zero-hop control packets allowed (for this subset of payloads) return ACTION_RELEASE; } - if (pkt->isRouteDirect() && pkt->path_len >= PATH_HASH_SIZE) { + if (pkt->isRouteDirect() && pkt->getPathHashCount() > 0) { // check for 'early received' ACK if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) { int i = 0; @@ -88,7 +83,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { } } - if (self_id.isHashMatch(pkt->path) && allowPacketForward(pkt)) { + if (self_id.isHashMatch(pkt->path, pkt->getPathHashSize()) && allowPacketForward(pkt)) { if (pkt->getPayloadType() == PAYLOAD_TYPE_MULTIPART) { return forwardMultipartDirect(pkt); } else if (pkt->getPayloadType() == PAYLOAD_TYPE_ACK) { @@ -158,7 +153,9 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { if (pkt->getPayloadType() == PAYLOAD_TYPE_PATH) { int k = 0; uint8_t path_len = data[k++]; - uint8_t* path = &data[k]; k += path_len; + uint8_t hash_size = (path_len >> 6) + 1; + uint8_t hash_count = path_len & 63; + uint8_t* path = &data[k]; k += hash_size*hash_count; uint8_t extra_type = data[k++] & 0x0F; // upper 4 bits reserved for future use uint8_t* extra = &data[k]; uint8_t extra_len = len - k; // remainder of packet (may be padded with zeroes!) @@ -293,8 +290,7 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { if (type == PAYLOAD_TYPE_ACK && pkt->payload_len >= 5) { // a multipart ACK Packet tmp; tmp.header = pkt->header; - tmp.path_len = pkt->path_len; - memcpy(tmp.path, pkt->path, pkt->path_len); + tmp.path_len = Packet::copyPath(tmp.path, pkt->path, pkt->path_len); tmp.payload_len = pkt->payload_len - 1; memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len); @@ -321,27 +317,25 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { void Mesh::removeSelfFromPath(Packet* pkt) { // remove our hash from 'path' - pkt->path_len -= PATH_HASH_SIZE; -#if 0 - memcpy(pkt->path, &pkt->path[PATH_HASH_SIZE], pkt->path_len); -#elif PATH_HASH_SIZE == 1 - for (int k = 0; k < pkt->path_len; k++) { // shuffle bytes by 1 - pkt->path[k] = pkt->path[k + 1]; + pkt->setPathHashCount(pkt->getPathHashCount() - 1); // decrement the count + + uint8_t sz = pkt->getPathHashSize(); + for (int k = 0; k < pkt->getPathHashCount()*sz; k += sz) { // shuffle path by 1 'entry' + memcpy(&pkt->path[k], &pkt->path[k + sz], sz); } -#else - #error "need path remove impl" -#endif } DispatcherAction Mesh::routeRecvPacket(Packet* packet) { + uint8_t n = packet->getPathHashCount(); if (packet->isRouteFlood() && !packet->isMarkedDoNotRetransmit() - && packet->path_len + PATH_HASH_SIZE <= MAX_PATH_SIZE && allowPacketForward(packet)) { + && (n + 1)*packet->getPathHashSize() <= MAX_PATH_SIZE && allowPacketForward(packet)) { // append this node's hash to 'path' - packet->path_len += self_id.copyHashTo(&packet->path[packet->path_len]); + self_id.copyHashTo(&packet->path[n * packet->getPathHashSize()], packet->getPathHashSize()); + packet->setPathHashCount(n + 1); uint32_t d = getRetransmitDelay(packet); // as this propagates outwards, give it lower and lower priority - return ACTION_RETRANSMIT_DELAYED(packet->path_len, d); // give priority to closer sources, than ones further away + return ACTION_RETRANSMIT_DELAYED(packet->getPathHashCount(), d); // give priority to closer sources, than ones further away } return ACTION_RELEASE; } @@ -353,8 +347,7 @@ DispatcherAction Mesh::forwardMultipartDirect(Packet* pkt) { if (type == PAYLOAD_TYPE_ACK && pkt->payload_len >= 5) { // a multipart ACK Packet tmp; tmp.header = pkt->header; - tmp.path_len = pkt->path_len; - memcpy(tmp.path, pkt->path, pkt->path_len); + tmp.path_len = Packet::copyPath(tmp.path, pkt->path, pkt->path_len); tmp.payload_len = pkt->payload_len - 1; memcpy(tmp.payload, &pkt->payload[1], tmp.payload_len); @@ -376,7 +369,7 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) { delay_millis += getDirectRetransmitDelay(packet) + 300; auto a1 = createMultiAck(crc, extra); if (a1) { - memcpy(a1->path, packet->path, a1->path_len = packet->path_len); + a1->path_len = Packet::copyPath(a1->path, packet->path, packet->path_len); a1->header &= ~PH_ROUTE_MASK; a1->header |= ROUTE_TYPE_DIRECT; sendPacket(a1, 0, delay_millis); @@ -386,7 +379,7 @@ void Mesh::routeDirectRecvAcks(Packet* packet, uint32_t delay_millis) { auto a2 = createAck(crc); if (a2) { - memcpy(a2->path, packet->path, a2->path_len = packet->path_len); + a2->path_len = Packet::copyPath(a2->path, packet->path, packet->path_len); a2->header &= ~PH_ROUTE_MASK; a2->header |= ROUTE_TYPE_DIRECT; sendPacket(a2, 0, delay_millis); @@ -439,7 +432,10 @@ Packet* Mesh::createPathReturn(const Identity& dest, const uint8_t* secret, cons } Packet* Mesh::createPathReturn(const uint8_t* dest_hash, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len) { - if (path_len + extra_len + 5 > MAX_COMBINED_PATH) return NULL; // too long!! + uint8_t path_hash_size = (path_len >> 6) + 1; + uint8_t path_hash_count = path_len & 63; + + if (path_hash_count*path_hash_size + extra_len + 5 > MAX_COMBINED_PATH) return NULL; // too long!! Packet* packet = obtainNewPacket(); if (packet == NULL) { @@ -457,7 +453,7 @@ Packet* Mesh::createPathReturn(const uint8_t* dest_hash, const uint8_t* secret, uint8_t data[MAX_PACKET_PAYLOAD]; data[data_len++] = path_len; - memcpy(&data[data_len], path, path_len); data_len += path_len; + memcpy(&data[data_len], path, path_hash_count*path_hash_size); data_len += path_hash_count*path_hash_size; if (extra_len > 0) { data[data_len++] = extra_type; memcpy(&data[data_len], extra, extra_len); data_len += extra_len; @@ -624,15 +620,19 @@ Packet* Mesh::createControlData(const uint8_t* data, size_t len) { return packet; } -void Mesh::sendFlood(Packet* packet, uint32_t delay_millis) { +void Mesh::sendFlood(Packet* packet, uint32_t delay_millis, uint8_t path_hash_size) { if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) { MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): TRACE type not suspported", getLogDateTime()); return; } + if (path_hash_size == 0 || path_hash_size > 3) { + MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): invalid path_hash_size", getLogDateTime()); + return; + } packet->header &= ~PH_ROUTE_MASK; packet->header |= ROUTE_TYPE_FLOOD; - packet->path_len = 0; + packet->setPathHashSizeAndCount(path_hash_size, 0); _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us @@ -647,17 +647,21 @@ void Mesh::sendFlood(Packet* packet, uint32_t delay_millis) { sendPacket(packet, pri, delay_millis); } -void Mesh::sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis) { +void Mesh::sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis, uint8_t path_hash_size) { if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) { MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): TRACE type not suspported", getLogDateTime()); return; } + if (path_hash_size == 0 || path_hash_size > 3) { + MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): invalid path_hash_size", getLogDateTime()); + return; + } packet->header &= ~PH_ROUTE_MASK; packet->header |= ROUTE_TYPE_TRANSPORT_FLOOD; packet->transport_codes[0] = transport_codes[0]; packet->transport_codes[1] = transport_codes[1]; - packet->path_len = 0; + packet->setPathHashSizeAndCount(path_hash_size, 0); _tables->hasSeen(packet); // mark this packet as already sent in case it is rebroadcast back to us @@ -679,13 +683,13 @@ void Mesh::sendDirect(Packet* packet, const uint8_t* path, uint8_t path_len, uin uint8_t pri; if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) { // TRACE packets are different // for TRACE packets, path is appended to end of PAYLOAD. (path is used for SNR's) - memcpy(&packet->payload[packet->payload_len], path, path_len); + memcpy(&packet->payload[packet->payload_len], path, path_len); // NOTE: path_len here can be > 64, and NOT in the new scheme packet->payload_len += path_len; packet->path_len = 0; pri = 5; // maybe make this configurable } else { - memcpy(packet->path, path, packet->path_len = path_len); + packet->path_len = Packet::copyPath(packet->path, path, path_len); if (packet->getPayloadType() == PAYLOAD_TYPE_PATH) { pri = 1; // slightly less priority } else { diff --git a/src/Mesh.h b/src/Mesh.h index 00f7ed00..f9f87863 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -196,13 +196,13 @@ public: /** * \brief send a locally-generated Packet with flood routing */ - void sendFlood(Packet* packet, uint32_t delay_millis=0); + void sendFlood(Packet* packet, uint32_t delay_millis=0, uint8_t path_hash_size=1); /** * \brief send a locally-generated Packet with flood routing * \param transport_codes array of 2 codes to attach to packet */ - void sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis=0); + void sendFlood(Packet* packet, uint16_t* transport_codes, uint32_t delay_millis=0, uint8_t path_hash_size=1); /** * \brief send a locally-generated Packet with Direct routing diff --git a/src/Packet.cpp b/src/Packet.cpp index 2d54ca45..0a75c2b3 100644 --- a/src/Packet.cpp +++ b/src/Packet.cpp @@ -10,8 +10,19 @@ Packet::Packet() { payload_len = 0; } +uint8_t Packet::copyPath(uint8_t* dest, const uint8_t* src, uint8_t path_len) { + uint8_t hash_count = path_len & 63; + uint8_t hash_size = (path_len >> 6) + 1; + if (hash_count*hash_size > MAX_PATH_SIZE) { + MESH_DEBUG_PRINTLN("Packet::copyPath, invalid path_len=%d", (uint32_t)path_len); + return 0; // Error + } + memcpy(dest, src, hash_count*hash_size); + return path_len; +} + int Packet::getRawLength() const { - return 2 + path_len + payload_len + (hasTransportCodes() ? 4 : 0); + return 2 + getPathByteLen() + payload_len + (hasTransportCodes() ? 4 : 0); } void Packet::calculatePacketHash(uint8_t* hash) const { diff --git a/src/Packet.h b/src/Packet.h index 42d73f41..b325fc1c 100644 --- a/src/Packet.h +++ b/src/Packet.h @@ -76,6 +76,14 @@ public: */ uint8_t getPayloadVer() const { return (header >> PH_VER_SHIFT) & PH_VER_MASK; } + uint8_t getPathHashSize() const { return (path_len >> 6) + 1; } + uint8_t getPathHashCount() const { return path_len & 63; } + uint8_t getPathByteLen() const { return getPathHashCount() * getPathHashSize(); } + void setPathHashCount(uint8_t n) { path_len &= ~63; path_len |= n; } + void setPathHashSizeAndCount(uint8_t sz, uint8_t n) { path_len = ((sz - 1) << 6) | (n & 63); } + + static uint8_t copyPath(uint8_t* dest, const uint8_t* src, uint8_t path_len); + void markDoNotRetransmit() { header = 0xFF; } bool isMarkedDoNotRetransmit() const { return header == 0xFF; } diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 6de7469d..5ec678c7 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -39,7 +39,7 @@ mesh::Packet* BaseChatMesh::createSelfAdvert(const char* name, double lat, doubl } void BaseChatMesh::sendAckTo(const ContactInfo& dest, uint32_t ack_hash) { - if (dest.out_path_len < 0) { + if (dest.out_path_len == OUT_PATH_UNKNOWN) { mesh::Packet* ack = createAck(ack_hash); if (ack) sendFloodScoped(dest, ack, TXT_ACK_DELAY); } else { @@ -92,7 +92,7 @@ ContactInfo* BaseChatMesh::allocateContactSlot() { void BaseChatMesh::populateContactFromAdvert(ContactInfo& ci, const mesh::Identity& id, const AdvertDataParser& parser, uint32_t timestamp) { memset(&ci, 0, sizeof(ci)); ci.id = id; - ci.out_path_len = -1; // initially out_path is unknown + ci.out_path_len = OUT_PATH_UNKNOWN; StrHelper::strncpy(ci.name, parser.getName(), sizeof(ci.name)); ci.type = parser.getType(); if (parser.hasLatLon()) { @@ -263,7 +263,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender } else { mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, from.id, secret, temp_buf, reply_len); if (reply) { - if (from.out_path_len >= 0) { // we have an out_path, so send DIRECT + if (from.out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT sendDirect(reply, from.out_path, from.out_path_len, SERVER_RESPONSE_DELAY); } else { sendFloodScoped(from, reply, SERVER_RESPONSE_DELAY); @@ -273,7 +273,7 @@ void BaseChatMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender } } else if (type == PAYLOAD_TYPE_RESPONSE && len > 0) { onContactResponse(from, data, len); - if (packet->isRouteFlood() && from.out_path_len >= 0) { + if (packet->isRouteFlood() && from.out_path_len != OUT_PATH_UNKNOWN) { // we have direct path, but other node is still sending flood response, so maybe they didn't receive reciprocal path properly(?) handleReturnPathRetry(from, packet->path, packet->path_len); } @@ -295,7 +295,7 @@ bool BaseChatMesh::onPeerPathRecv(mesh::Packet* packet, int sender_idx, const ui bool BaseChatMesh::onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) { // NOTE: default impl, we just replace the current 'out_path' regardless, whenever sender sends us a new out_path. // FUTURE: could store multiple out_paths per contact, and try to find which is the 'best'(?) - memcpy(from.out_path, out_path, from.out_path_len = out_path_len); // store a copy of path, for sendDirect() + from.out_path_len = mesh::Packet::copyPath(from.out_path, out_path, out_path_len); // store a copy of path, for sendDirect() from.lastmod = getRTCClock()->getCurrentTime(); onContactPathUpdated(from); @@ -317,7 +317,7 @@ void BaseChatMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) { txt_send_timeout = 0; // matched one we're waiting for, cancel timeout timer packet->markDoNotRetransmit(); // ACK was for this node, so don't retransmit - if (packet->isRouteFlood() && from->out_path_len >= 0) { + if (packet->isRouteFlood() && from->out_path_len != OUT_PATH_UNKNOWN) { // we have direct path, but other node is still sending flood, so maybe they didn't receive reciprocal path properly(?) handleReturnPathRetry(*from, packet->path, packet->path_len); } @@ -386,7 +386,7 @@ int BaseChatMesh::sendMessage(const ContactInfo& recipient, uint32_t timestamp, uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); int rc; - if (recipient.out_path_len < 0) { + if (recipient.out_path_len == OUT_PATH_UNKNOWN) { sendFloodScoped(recipient, pkt); txt_send_timeout = futureMillis(est_timeout = calcFloodTimeoutMillisFor(t)); rc = MSG_SEND_SENT_FLOOD; @@ -412,7 +412,7 @@ int BaseChatMesh::sendCommandData(const ContactInfo& recipient, uint32_t timest uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); int rc; - if (recipient.out_path_len < 0) { + if (recipient.out_path_len == OUT_PATH_UNKNOWN) { sendFloodScoped(recipient, pkt); txt_send_timeout = futureMillis(est_timeout = calcFloodTimeoutMillisFor(t)); rc = MSG_SEND_SENT_FLOOD; @@ -500,7 +500,7 @@ int BaseChatMesh::sendLogin(const ContactInfo& recipient, const char* password, } if (pkt) { uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); - if (recipient.out_path_len < 0) { + if (recipient.out_path_len == OUT_PATH_UNKNOWN) { sendFloodScoped(recipient, pkt); est_timeout = calcFloodTimeoutMillisFor(t); return MSG_SEND_SENT_FLOOD; @@ -525,7 +525,7 @@ int BaseChatMesh::sendAnonReq(const ContactInfo& recipient, const uint8_t* data, } if (pkt) { uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); - if (recipient.out_path_len < 0) { + if (recipient.out_path_len == OUT_PATH_UNKNOWN) { sendFloodScoped(recipient, pkt); est_timeout = calcFloodTimeoutMillisFor(t); return MSG_SEND_SENT_FLOOD; @@ -552,7 +552,7 @@ int BaseChatMesh::sendRequest(const ContactInfo& recipient, const uint8_t* req_ } if (pkt) { uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); - if (recipient.out_path_len < 0) { + if (recipient.out_path_len == OUT_PATH_UNKNOWN) { sendFloodScoped(recipient, pkt); est_timeout = calcFloodTimeoutMillisFor(t); return MSG_SEND_SENT_FLOOD; @@ -579,7 +579,7 @@ int BaseChatMesh::sendRequest(const ContactInfo& recipient, uint8_t req_type, u } if (pkt) { uint32_t t = _radio->getEstAirtimeFor(pkt->getRawLength()); - if (recipient.out_path_len < 0) { + if (recipient.out_path_len == OUT_PATH_UNKNOWN) { sendFloodScoped(recipient, pkt); est_timeout = calcFloodTimeoutMillisFor(t); return MSG_SEND_SENT_FLOOD; @@ -683,7 +683,7 @@ void BaseChatMesh::checkConnections() { MESH_DEBUG_PRINTLN("checkConnections(): Keep_alive contact not found!"); continue; } - if (contact->out_path_len < 0) { + if (contact->out_path_len == OUT_PATH_UNKNOWN) { MESH_DEBUG_PRINTLN("checkConnections(): Keep_alive contact, no out_path!"); continue; } @@ -710,7 +710,7 @@ void BaseChatMesh::checkConnections() { } void BaseChatMesh::resetPathTo(ContactInfo& recipient) { - recipient.out_path_len = -1; + recipient.out_path_len = OUT_PATH_UNKNOWN; } static ContactInfo* table; // pass via global :-( diff --git a/src/helpers/ClientACL.cpp b/src/helpers/ClientACL.cpp index 55b70ca5..12823827 100644 --- a/src/helpers/ClientACL.cpp +++ b/src/helpers/ClientACL.cpp @@ -114,7 +114,7 @@ ClientInfo* ClientACL::putClient(const mesh::Identity& id, uint8_t init_perms) { memset(c, 0, sizeof(*c)); c->permissions = init_perms; c->id = id; - c->out_path_len = -1; // initially out_path is unknown + c->out_path_len = OUT_PATH_UNKNOWN; return c; } diff --git a/src/helpers/ClientACL.h b/src/helpers/ClientACL.h index dfbc3fce..b758f706 100644 --- a/src/helpers/ClientACL.h +++ b/src/helpers/ClientACL.h @@ -10,10 +10,12 @@ #define PERM_ACL_READ_WRITE 2 #define PERM_ACL_ADMIN 3 +#define OUT_PATH_UNKNOWN 0xFF + struct ClientInfo { mesh::Identity id; uint8_t permissions; - int8_t out_path_len; + uint8_t out_path_len; uint8_t out_path[MAX_PATH_SIZE]; uint8_t shared_secret[PUB_KEY_SIZE]; uint32_t last_timestamp; // by THEIR clock (transient) diff --git a/src/helpers/ContactInfo.h b/src/helpers/ContactInfo.h index eff07741..ede977ca 100644 --- a/src/helpers/ContactInfo.h +++ b/src/helpers/ContactInfo.h @@ -3,12 +3,14 @@ #include #include +#define OUT_PATH_UNKNOWN 0xFF + struct ContactInfo { mesh::Identity id; char name[32]; uint8_t type; // on of ADV_TYPE_* uint8_t flags; - int8_t out_path_len; + uint8_t out_path_len; mutable bool shared_secret_valid; // flag to indicate if shared_secret has been calculated uint8_t out_path[MAX_PATH_SIZE]; uint32_t last_advert_timestamp; // by THEIR clock From 3dc14976a03d46f855ce21faddc64866471aa702 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Sun, 22 Feb 2026 14:46:45 +0100 Subject: [PATCH 25/40] add companion usb build target for Heltec Wireless Tracker --- variants/heltec_tracker/platformio.ini | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/variants/heltec_tracker/platformio.ini b/variants/heltec_tracker/platformio.ini index 797eafdc..dba05dcf 100644 --- a/variants/heltec_tracker/platformio.ini +++ b/variants/heltec_tracker/platformio.ini @@ -43,6 +43,31 @@ lib_deps = stevemarple/MicroNMEA @ ^2.0.6 adafruit/Adafruit ST7735 and ST7789 Library @ ^1.11.0 +[env:Heltec_Wireless_Tracker_companion_radio_usb] +extends = Heltec_tracker_base +build_flags = + ${Heltec_tracker_base.build_flags} + -I src/helpers/ui + -I examples/companion_radio/ui-new + -D DISPLAY_ROTATION=1 + -D DISPLAY_CLASS=ST7735Display + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 +; -D BLE_PIN_CODE=123456 ; HWT will use display for pin +; -D OFFLINE_QUEUE_SIZE=256 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${Heltec_tracker_base.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> + + +lib_deps = + ${Heltec_tracker_base.lib_deps} + densaugeo/base64 @ ~1.4.0 + [env:Heltec_Wireless_Tracker_companion_radio_ble] extends = Heltec_tracker_base build_flags = From 011edd3c999ca6c528016d67f3e68ac46830131c Mon Sep 17 00:00:00 2001 From: Daniel Novak Date: Sun, 22 Feb 2026 18:01:30 +0100 Subject: [PATCH 26/40] Fix millis() wraparound in PacketQueue time comparisons PacketQueue::countBefore() and PacketQueue::get() use unsigned comparison (_schedule_table[j] > now) to check if a packet is scheduled for the future. This breaks when millis() wraps around after ~49.7 days: packets scheduled just before the wrap appear to be in the far future and get stuck in the queue. Use signed subtraction instead, matching the approach already used by Dispatcher::millisHasNowPassed(). This correctly handles the wraparound for time differences up to ~24.8 days in either direction, well beyond the maximum queue delay of 32 seconds. --- src/helpers/StaticPoolPacketManager.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/helpers/StaticPoolPacketManager.cpp b/src/helpers/StaticPoolPacketManager.cpp index 125efb75..67d63979 100644 --- a/src/helpers/StaticPoolPacketManager.cpp +++ b/src/helpers/StaticPoolPacketManager.cpp @@ -11,7 +11,7 @@ PacketQueue::PacketQueue(int max_entries) { int PacketQueue::countBefore(uint32_t now) const { int n = 0; for (int j = 0; j < _num; j++) { - if (_schedule_table[j] > now) continue; // scheduled for future... ignore for now + if ((int32_t)(_schedule_table[j] - now) > 0) continue; // scheduled for future... ignore for now n++; } return n; @@ -21,7 +21,7 @@ mesh::Packet* PacketQueue::get(uint32_t now) { uint8_t min_pri = 0xFF; int best_idx = -1; for (int j = 0; j < _num; j++) { - if (_schedule_table[j] > now) continue; // scheduled for future... ignore for now + if ((int32_t)(_schedule_table[j] - now) > 0) continue; // scheduled for future... ignore for now if (_pri_table[j] < min_pri) { // select most important priority amongst non-future entries min_pri = _pri_table[j]; best_idx = j; From 5a885bffe4ad6e9e33771845a7e43e3a0db6d6d9 Mon Sep 17 00:00:00 2001 From: Sam Koucha <23366921+ElectroMW@users.noreply.github.com> Date: Sun, 22 Feb 2026 18:14:39 +0000 Subject: [PATCH 27/40] Make full use of board's 8MB Flash and add companion WiFI target --- boards/t_beams3_supreme.json | 2 +- .../platformio.ini | 28 ++++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/boards/t_beams3_supreme.json b/boards/t_beams3_supreme.json index 6a725247..3eb9c016 100644 --- a/boards/t_beams3_supreme.json +++ b/boards/t_beams3_supreme.json @@ -41,7 +41,7 @@ "name": "LilyGo T-Beam supreme (8MB Flash 8MB PSRAM)", "upload": { "flash_size": "8MB", - "maximum_ram_size": 327680, + "maximum_ram_size": 8388608, "maximum_size": 8388608, "require_upload_port": true, "speed": 460800 diff --git a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini index 2d2a095a..04e4b0bf 100644 --- a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini +++ b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini @@ -26,7 +26,9 @@ build_src_filter = ${esp32_base.build_src_filter} + + + -board_build.partitions = min_spiffs.csv ; get around 4mb flash limit +board_build.partitions = default_8MB.csv +board_upload.flash_size = 8MB +board_upload.maximum_size = 8388608 lib_deps = ${esp32_base.lib_deps} lewisxhe/XPowersLib @ ^0.2.7 @@ -131,3 +133,27 @@ build_src_filter = ${T_Beam_S3_Supreme_SX1262.build_src_filter} lib_deps = ${T_Beam_S3_Supreme_SX1262.lib_deps} densaugeo/base64 @ ~1.4.0 + +[env:T_Beam_S3_Supreme_SX1262_companion_radio_wifi] +extends = T_Beam_S3_Supreme_SX1262 +build_flags = + ${T_Beam_S3_Supreme_SX1262.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D OFFLINE_QUEUE_SIZE=256 + -D WIFI_SSID='"Three_CA7C65"' + -D WIFI_PWD='"8hC45a66265eA3w"' +; -D WIFI_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=8 +; -D MESH_DEBUG=1 +; -D ARDUHAL_LOG_LEVEL=4 +; -D CORE_DEBUG_LEVEL=4 +build_src_filter = ${T_Beam_S3_Supreme_SX1262.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${T_Beam_S3_Supreme_SX1262.lib_deps} + densaugeo/base64 @ ~1.4.0 From a66773bac096252eb557aba1ed8ddafc955042fa Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 23 Feb 2026 14:25:19 +1100 Subject: [PATCH 28/40] * CommonCLI: added "get/set path.hash.mode " --- examples/companion_radio/MyMesh.cpp | 10 ++++------ examples/simple_repeater/MyMesh.cpp | 2 +- examples/simple_room_server/MyMesh.cpp | 2 +- examples/simple_sensor/SensorMesh.cpp | 10 ++++++---- src/Packet.cpp | 19 ++++++++++++++++--- src/Packet.h | 4 +++- src/helpers/CommonCLI.cpp | 19 +++++++++++++++++-- src/helpers/CommonCLI.h | 1 + 8 files changed, 49 insertions(+), 18 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index f0f8e5fc..26beeab9 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -678,7 +678,7 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data, } } -bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t _in_path_len, uint8_t* out_path, uint8_t _out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) { +bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) { if (extra_type == PAYLOAD_TYPE_RESPONSE && extra_len > 4) { uint32_t tag; memcpy(&tag, extra, 4); @@ -686,7 +686,7 @@ bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t _ if (tag == pending_discovery) { // check for matching response tag) pending_discovery = 0; - if (in_path_len > MAX_PATH_SIZE || out_path_len > MAX_PATH_SIZE) { + if (!mesh::Packet::isValidPathLen(in_path_len) || !mesh::Packet::isValidPathLen(out_path_len)) { MESH_DEBUG_PRINTLN("onContactPathRecv, invalid path sizes: %d, %d", in_path_len, out_path_len); } else { int i = 0; @@ -695,11 +695,9 @@ bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t _ memcpy(&out_frame[i], contact.id.pub_key, 6); i += 6; // pub_key_prefix out_frame[i++] = out_path_len; - memcpy(&out_frame[i], out_path, out_path_len); - i += out_path_len; + i += mesh::Packet::writePath(&out_frame[i], out_path, out_path_len); out_frame[i++] = in_path_len; - memcpy(&out_frame[i], in_path, in_path_len); - i += in_path_len; + i += mesh::Packet::writePath(&out_frame[i], in_path, in_path_len); // NOTE: telemetry data in 'extra' is discarded at present _serial->writeFrame(out_frame, i); diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 7328f703..81c1dcb4 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -917,7 +917,7 @@ void MyMesh::sendSelfAdvertisement(int delay_millis, bool flood) { mesh::Packet *pkt = createSelfAdvert(); if (pkt) { if (flood) { - sendFlood(pkt, delay_millis); // TODO: which path_hash_size to use?? + sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); } else { sendZeroHop(pkt, delay_millis); } diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 0b1d3a1b..06d9cc96 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -680,7 +680,7 @@ void MyMesh::sendSelfAdvertisement(int delay_millis, bool flood) { mesh::Packet *pkt = createSelfAdvert(); if (pkt) { if (flood) { - sendFlood(pkt, delay_millis); // TODO: which path_hash_size to use? + sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); } else { sendZeroHop(pkt, delay_millis); } diff --git a/examples/simple_sensor/SensorMesh.cpp b/examples/simple_sensor/SensorMesh.cpp index fc9ef310..68fea474 100644 --- a/examples/simple_sensor/SensorMesh.cpp +++ b/examples/simple_sensor/SensorMesh.cpp @@ -261,7 +261,8 @@ void SensorMesh::sendAlert(const ClientInfo* c, Trigger* t) { if (c->out_path_len != OUT_PATH_UNKNOWN) { // we have an out_path, so send DIRECT sendDirect(pkt, c->out_path, c->out_path_len); } else { - sendFlood(pkt); + unsigned long delay_millis = 0; + sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); } } t->send_expiry = futureMillis(ALERT_ACK_EXPIRY_MILLIS); @@ -567,7 +568,7 @@ void SensorMesh::onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_i // let this sender know path TO here, so they can use sendDirect(), and ALSO encode the ACK mesh::Packet* path = createPathReturn(from->id, secret, packet->path, packet->path_len, PAYLOAD_TYPE_ACK, (uint8_t *) &ack_hash, 4); - if (path) sendFlood(path, TXT_ACK_DELAY); + if (path) sendFlood(path, TXT_ACK_DELAY, packet->getPathHashSize()); } else { sendAckTo(*from, ack_hash, packet->getPathHashSize()); } @@ -791,7 +792,7 @@ void SensorMesh::sendSelfAdvertisement(int delay_millis, bool flood) { mesh::Packet* pkt = createSelfAdvert(); if (pkt) { if (flood) { - sendFlood(pkt, delay_millis); + sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); } else { sendZeroHop(pkt, delay_millis); } @@ -868,7 +869,8 @@ void SensorMesh::loop() { if (next_flood_advert && millisHasNowPassed(next_flood_advert)) { mesh::Packet* pkt = createSelfAdvert(); - if (pkt) sendFlood(pkt); + unsigned long delay_millis = 0; + if (pkt) sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); updateFloodAdvertTimer(); // schedule next flood advert updateAdvertTimer(); // also schedule local advert (so they don't overlap) diff --git a/src/Packet.cpp b/src/Packet.cpp index 0a75c2b3..9fd06395 100644 --- a/src/Packet.cpp +++ b/src/Packet.cpp @@ -10,14 +10,27 @@ Packet::Packet() { payload_len = 0; } -uint8_t Packet::copyPath(uint8_t* dest, const uint8_t* src, uint8_t path_len) { +bool Packet::isValidPathLen(uint8_t path_len) { uint8_t hash_count = path_len & 63; uint8_t hash_size = (path_len >> 6) + 1; - if (hash_count*hash_size > MAX_PATH_SIZE) { + if (hash_size == 4) return false; // Reserved for future + return hash_count*hash_size <= MAX_PATH_SIZE; +} + +size_t Packet::writePath(uint8_t* dest, const uint8_t* src, uint8_t path_len) { + uint8_t hash_count = path_len & 63; + uint8_t hash_size = (path_len >> 6) + 1; + size_t len = hash_count*hash_size; + if (len > MAX_PATH_SIZE) { MESH_DEBUG_PRINTLN("Packet::copyPath, invalid path_len=%d", (uint32_t)path_len); return 0; // Error } - memcpy(dest, src, hash_count*hash_size); + memcpy(dest, src, len); + return len; +} + +uint8_t Packet::copyPath(uint8_t* dest, const uint8_t* src, uint8_t path_len) { + if (writePath(dest, src, path_len) == 0) return 0; return path_len; } diff --git a/src/Packet.h b/src/Packet.h index b325fc1c..78619546 100644 --- a/src/Packet.h +++ b/src/Packet.h @@ -82,7 +82,9 @@ public: void setPathHashCount(uint8_t n) { path_len &= ~63; path_len |= n; } void setPathHashSizeAndCount(uint8_t sz, uint8_t n) { path_len = ((sz - 1) << 6) | (n & 63); } - static uint8_t copyPath(uint8_t* dest, const uint8_t* src, uint8_t path_len); + static uint8_t copyPath(uint8_t* dest, const uint8_t* src, uint8_t path_len); // returns path_len + static size_t writePath(uint8_t* dest, const uint8_t* src, uint8_t path_len); // returns byte length written + static bool isValidPathLen(uint8_t path_len); void markDoNotRetransmit() { header = 0xFF; } bool isMarkedDoNotRetransmit() const { return header == 0xFF; } diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 263eb665..f2f961b9 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -63,7 +63,8 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->multi_acks, sizeof(_prefs->multi_acks)); // 115 file.read((uint8_t *)&_prefs->bw, sizeof(_prefs->bw)); // 116 file.read((uint8_t *)&_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval)); // 120 - file.read(pad, 3); // 121 + file.read((uint8_t *)&_prefs->path_hash_mode, sizeof(_prefs->path_hash_mode)); // 121 + file.read(pad, 2); // 122 file.read((uint8_t *)&_prefs->flood_max, sizeof(_prefs->flood_max)); // 124 file.read((uint8_t *)&_prefs->flood_advert_interval, sizeof(_prefs->flood_advert_interval)); // 125 file.read((uint8_t *)&_prefs->interference_threshold, sizeof(_prefs->interference_threshold)); // 126 @@ -95,6 +96,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { _prefs->tx_power_dbm = constrain(_prefs->tx_power_dbm, -9, 30); _prefs->multi_acks = constrain(_prefs->multi_acks, 0, 1); _prefs->adc_multiplier = constrain(_prefs->adc_multiplier, 0.0f, 10.0f); + _prefs->path_hash_mode = constrain(_prefs->path_hash_mode, 0, 2); // NOTE: mode 3 reserved for future // sanitise bad bridge pref values _prefs->bridge_enabled = constrain(_prefs->bridge_enabled, 0, 1); @@ -147,7 +149,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { 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(pad, 3); // 121 + file.write((uint8_t *)&_prefs->path_hash_mode, sizeof(_prefs->path_hash_mode)); // 121 + file.write(pad, 2); // 122 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 @@ -325,6 +328,8 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch sp++; } *reply = 0; // set null terminator + } else if (memcmp(config, "path.hash.mode", 14) == 0) { + sprintf(reply, "> %d", (uint32_t)_prefs->path_hash_mode); } else if (memcmp(config, "tx", 2) == 0 && (config[2] == 0 || config[2] == ' ')) { sprintf(reply, "> %d", (int32_t) _prefs->tx_power_dbm); } else if (memcmp(config, "freq", 4) == 0) { @@ -556,6 +561,16 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch *dp = 0; savePrefs(); strcpy(reply, "OK"); + } else if (memcmp(config, "path.hash.mode ", 15) == 0) { + config += 15; + uint8_t mode = atoi(config); + if (mode < 3) { + _prefs->path_hash_mode = mode; + savePrefs(); + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error, must be 0,1, or 2"); + } } else if (memcmp(config, "tx ", 3) == 0) { _prefs->tx_power_dbm = atoi(&config[3]); savePrefs(); diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 146e1c6e..1e454ec2 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -52,6 +52,7 @@ struct NodePrefs { // persisted to file uint32_t discovery_mod_timestamp; float adc_multiplier; char owner_info[120]; + uint8_t path_hash_mode; // which path mode to use when sending }; class CommonCLICallbacks { From e52d57c06528a533d7734f886c8016489373e8cb Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 23 Feb 2026 18:26:56 +1100 Subject: [PATCH 29/40] * companion: new pref: path_hash_mode (0..2) * companion: new field in CMD_SET_OTHER_PARAMS, path_hash_mode * companion: CMD_SEND_SELF_ADVERT, cmd_frame[1] now holds the path hash size (0 = zero hop, 1..3 = flood path hash size) --- examples/companion_radio/DataStore.cpp | 6 ++++-- examples/companion_radio/MyMesh.cpp | 17 +++++++++++------ examples/companion_radio/NodePrefs.h | 1 + examples/simple_room_server/MyMesh.cpp | 3 ++- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index 1239ea3d..fba64e8c 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -222,7 +222,8 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no file.read((uint8_t *)&_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base)); // 72 file.read((uint8_t *)&_prefs.advert_loc_policy, sizeof(_prefs.advert_loc_policy)); // 76 file.read((uint8_t *)&_prefs.multi_acks, sizeof(_prefs.multi_acks)); // 77 - file.read(pad, 2); // 78 + file.read((uint8_t *)&_prefs.path_hash_mode, sizeof(_prefs.path_hash_mode)); // 78 + file.read(pad, 1); // 79 file.read((uint8_t *)&_prefs.ble_pin, sizeof(_prefs.ble_pin)); // 80 file.read((uint8_t *)&_prefs.buzzer_quiet, sizeof(_prefs.buzzer_quiet)); // 84 file.read((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled)); // 85 @@ -257,7 +258,8 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_ 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(pad, 2); // 78 + 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 diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 26beeab9..09968ecd 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -473,23 +473,23 @@ bool MyMesh::allowPacketForward(const mesh::Packet* packet) { void MyMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) { // TODO: dynamic send_scope, depending on recipient and current 'home' Region if (send_scope.isNull()) { - sendFlood(pkt, delay_millis); + sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); } else { uint16_t codes[2]; codes[0] = send_scope.calcTransportCode(pkt); codes[1] = 0; // REVISIT: set to 'home' Region, for sender/return region? - sendFlood(pkt, codes, delay_millis); + sendFlood(pkt, codes, delay_millis, _prefs.path_hash_mode + 1); } } void MyMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis) { // TODO: have per-channel send_scope if (send_scope.isNull()) { - sendFlood(pkt, delay_millis); + sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); } else { uint16_t codes[2]; codes[0] = send_scope.calcTransportCode(pkt); codes[1] = 0; // REVISIT: set to 'home' Region, for sender/return region? - sendFlood(pkt, codes, delay_millis); + sendFlood(pkt, codes, delay_millis, _prefs.path_hash_mode + 1); } } @@ -937,6 +937,7 @@ void MyMesh::handleCmdFrame(size_t len) { StrHelper::strzcpy((char *)&out_frame[i], FIRMWARE_VERSION, 20); i += 20; out_frame[i++] = _prefs.client_repeat; // v9+ + out_frame[i++] = _prefs.path_hash_mode; // v10+ _serial->writeFrame(out_frame, i); } else if (cmd_frame[0] == CMD_APP_START && len >= 8) { // sent when app establishes connection, respond with node ID @@ -1113,8 +1114,9 @@ void MyMesh::handleCmdFrame(size_t len) { pkt = createSelfAdvert(_prefs.node_name, sensors.node_lat, sensors.node_lon); } if (pkt) { - if (len >= 2 && cmd_frame[1] == 1) { // optional param (1 = flood, 0 = zero hop) - sendFlood(pkt); // TODO: which path_hash_size to use?? + if (len >= 2 && cmd_frame[1] >= 1 && cmd_frame[1] <= 3) { // optional param (1..3 = flood, 0 = zero hop) + unsigned long delay_millis = 0; + sendFlood(pkt, delay_millis, cmd_frame[1]); } else { sendZeroHop(pkt); } @@ -1306,6 +1308,9 @@ void MyMesh::handleCmdFrame(size_t len) { _prefs.advert_loc_policy = cmd_frame[3]; if (len >= 5) { _prefs.multi_acks = cmd_frame[4]; + if (len >= 6) { + _prefs.path_hash_mode = cmd_frame[5]; + } } } } diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index f2a52f41..ec60c94a 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -29,4 +29,5 @@ 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 client_repeat; + uint8_t path_hash_mode; // which path mode to use when sending }; \ No newline at end of file diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 06d9cc96..3d2b5794 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -74,7 +74,8 @@ void MyMesh::pushPostToClient(ClientInfo *client, PostInfo &post) { auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, client->shared_secret, reply_data, len); if (reply) { if (client->out_path_len == OUT_PATH_UNKNOWN) { - sendFlood(reply); // TODO: which path_hash_size to use? + unsigned long delay_millis = 0; + sendFlood(reply, delay_millis, _prefs.path_hash_mode); client->extra.room.ack_timeout = futureMillis(PUSH_ACK_TIMEOUT_FLOOD); } else { sendDirect(reply, client->out_path, client->out_path_len); From 5b0884ad2d31ede8eae4f08591c69ad186109251 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 23 Feb 2026 21:08:22 +1100 Subject: [PATCH 30/40] * added CMD_SET_PATH_HASH_MODE --- examples/companion_radio/MyMesh.cpp | 12 +++++++++--- examples/companion_radio/MyMesh.h | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 09968ecd..e928e7e0 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -57,6 +57,7 @@ #define CMD_SET_AUTOADD_CONFIG 58 #define CMD_GET_AUTOADD_CONFIG 59 #define CMD_GET_ALLOWED_REPEAT_FREQ 60 +#define CMD_SET_PATH_HASH_MODE 61 // Stats sub-types for CMD_GET_STATS #define STATS_TYPE_CORE 0 @@ -1308,14 +1309,19 @@ void MyMesh::handleCmdFrame(size_t len) { _prefs.advert_loc_policy = cmd_frame[3]; if (len >= 5) { _prefs.multi_acks = cmd_frame[4]; - if (len >= 6) { - _prefs.path_hash_mode = cmd_frame[5]; - } } } } savePrefs(); writeOKFrame(); + } else if (cmd_frame[0] == CMD_SET_PATH_HASH_MODE && cmd_frame[1] == 0 && len >= 3) { + if (cmd_frame[2] >= 3) { + writeErrFrame(ERR_CODE_ILLEGAL_ARG); + } else { + _prefs.path_hash_mode = cmd_frame[2]; + savePrefs(); + writeOKFrame(); + } } else if (cmd_frame[0] == CMD_REBOOT && memcmp(&cmd_frame[1], "reboot", 6) == 0) { if (dirty_contacts_expiry) { // is there are pending dirty contacts write needed? saveContacts(); diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index e3c10985..87e6cf33 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -5,7 +5,7 @@ #include "AbstractUITask.h" /*------------ Frame Protocol --------------*/ -#define FIRMWARE_VER_CODE 9 +#define FIRMWARE_VER_CODE 10 #ifndef FIRMWARE_BUILD_DATE #define FIRMWARE_BUILD_DATE "15 Feb 2026" From 45564bad9b6522a032e40d98cdcf9e83dacae70c Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 23 Feb 2026 23:51:30 +1100 Subject: [PATCH 31/40] * Dispatcher bug fixes --- src/Dispatcher.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index 12889fb8..35eca0a9 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -68,7 +68,7 @@ void Dispatcher::loop() { next_tx_time = futureMillis(t * getAirtimeBudgetFactor()); _radio->onSendFinished(); - logTx(outbound, 2 + outbound->path_len + outbound->payload_len); + logTx(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len); if (outbound->isRouteFlood()) { n_sent_flood++; } else { @@ -80,7 +80,7 @@ void Dispatcher::loop() { MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): WARNING: outbound packed send timed out!", getLogDateTime()); _radio->onSendFinished(); - logTxFail(outbound, 2 + outbound->path_len + outbound->payload_len); + logTxFail(outbound, 2 + outbound->getPathByteLen() + outbound->payload_len); releasePacket(outbound); // return to pool outbound = NULL; @@ -266,7 +266,7 @@ void Dispatcher::checkSend() { memcpy(&raw[len], &outbound->transport_codes[1], 2); len += 2; } raw[len++] = outbound->path_len; - memcpy(&raw[len], outbound->path, outbound->path_len); len += outbound->path_len; + len += Packet::writePath(&raw[len], outbound->path, outbound->path_len); if (len + outbound->payload_len > MAX_TRANS_UNIT) { MESH_DEBUG_PRINTLN("%s Dispatcher::checkSend(): FATAL: Invalid packet queued... too long, len=%d", getLogDateTime(), len + outbound->payload_len); @@ -320,7 +320,7 @@ void Dispatcher::releasePacket(Packet* packet) { } void Dispatcher::sendPacket(Packet* packet, uint8_t priority, uint32_t delay_millis) { - if (packet->path_len > MAX_PATH_SIZE || packet->payload_len > MAX_PACKET_PAYLOAD) { + if (!Packet::isValidPathLen(packet->path_len) || packet->payload_len > MAX_PACKET_PAYLOAD) { MESH_DEBUG_PRINTLN("%s Dispatcher::sendPacket(): ERROR: invalid packet... path_len=%d, payload_len=%d", getLogDateTime(), (uint32_t) packet->path_len, (uint32_t) packet->payload_len); _mgr->free(packet); } else { From 213d085012ab59c24f462fdf80d64a7a20b9903f Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 24 Feb 2026 00:08:13 +1100 Subject: [PATCH 32/40] * revert CMD_SEND_SELF_ADVERT, use _prefs.path_hash_mode --- examples/companion_radio/MyMesh.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index e928e7e0..f23e50b6 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1115,9 +1115,9 @@ void MyMesh::handleCmdFrame(size_t len) { pkt = createSelfAdvert(_prefs.node_name, sensors.node_lat, sensors.node_lon); } if (pkt) { - if (len >= 2 && cmd_frame[1] >= 1 && cmd_frame[1] <= 3) { // optional param (1..3 = flood, 0 = zero hop) + if (len >= 2 && cmd_frame[1] == 1) { // optional param (1 = flood, 0 = zero hop) unsigned long delay_millis = 0; - sendFlood(pkt, delay_millis, cmd_frame[1]); + sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); } else { sendZeroHop(pkt); } From 9d5c4865c346b949a9b172a344b4d80d8c97b074 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 24 Feb 2026 01:08:11 +1100 Subject: [PATCH 33/40] * room server fix --- examples/simple_room_server/MyMesh.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index 3d2b5794..5451505a 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -75,7 +75,7 @@ void MyMesh::pushPostToClient(ClientInfo *client, PostInfo &post) { if (reply) { if (client->out_path_len == OUT_PATH_UNKNOWN) { unsigned long delay_millis = 0; - sendFlood(reply, delay_millis, _prefs.path_hash_mode); + sendFlood(reply, delay_millis, _prefs.path_hash_mode + 1); client->extra.room.ack_timeout = futureMillis(PUSH_ACK_TIMEOUT_FLOOD); } else { sendDirect(reply, client->out_path, client->out_path_len); From 9f4eeeecebc10851180d3d336445090d6b8d570c Mon Sep 17 00:00:00 2001 From: callum5892 Date: Mon, 23 Feb 2026 17:31:18 +0000 Subject: [PATCH 34/40] Added build flags for M5Stack Unit C6L Enabled USB-CDC on boot for M5Stack_Unit_C6L_companion_radio_usb to fix serial connection issues --- variants/m5stack_unit_c6l/platformio.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/variants/m5stack_unit_c6l/platformio.ini b/variants/m5stack_unit_c6l/platformio.ini index a2b8b087..1dd6749a 100644 --- a/variants/m5stack_unit_c6l/platformio.ini +++ b/variants/m5stack_unit_c6l/platformio.ini @@ -94,6 +94,8 @@ build_flags = ${M5Stack_Unit_C6L.build_flags} -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 + -D ARDUINO_USB_CDC_ON_BOOT=1 + -D ARDUINO_USB_MODE=1 build_src_filter = ${M5Stack_Unit_C6L.build_src_filter} + - From b14879ce2d1cfef2710e90ed35161a8d15aa5471 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 24 Feb 2026 14:23:59 +1100 Subject: [PATCH 35/40] * CMD_GET_ADVERT_PATH bug fix --- examples/companion_radio/MyMesh.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index f23e50b6..c96f7e01 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -350,7 +350,7 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path } // add inbound-path to mem cache - if (path && path_len <= sizeof(AdvertPath::path)) { // check path is valid + if (path && mesh::Packet::isValidPathLen(path_len)) { // check path is valid AdvertPath* p = advert_paths; uint32_t oldest = 0xFFFFFFFF; for (int i = 0; i < ADVERT_PATH_TABLE_SIZE; i++) { // check if already in table, otherwise evict oldest @@ -367,8 +367,7 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path memcpy(p->pubkey_prefix, contact.id.pub_key, sizeof(p->pubkey_prefix)); strcpy(p->name, contact.name); p->recv_timestamp = getRTCClock()->getCurrentTime(); - p->path_len = path_len; - memcpy(p->path, path, p->path_len); + p->path_len = mesh::Packet::copyPath(p->path, path, path_len); } if (!is_new) dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // only schedule lazy write for contacts that are in contacts[] @@ -1696,11 +1695,12 @@ void MyMesh::handleCmdFrame(size_t len) { } } if (found) { - out_frame[0] = RESP_CODE_ADVERT_PATH; - memcpy(&out_frame[1], &found->recv_timestamp, 4); - out_frame[5] = found->path_len; - memcpy(&out_frame[6], found->path, found->path_len); - _serial->writeFrame(out_frame, 6 + found->path_len); + int i = 0; + out_frame[i++] = RESP_CODE_ADVERT_PATH; + memcpy(&out_frame[i], &found->recv_timestamp, 4); i += 4; + out_frame[i++] = found->path_len; + i += mesh::Packet::writePath(&out_frame[i], found->path, found->path_len); + _serial->writeFrame(out_frame, i); } else { writeErrFrame(ERR_CODE_NOT_FOUND); } From b777a7c635e2c3ed329f82d45cf27919f949f7f7 Mon Sep 17 00:00:00 2001 From: Rastislav Vysoky Date: Tue, 24 Feb 2026 11:28:23 +0100 Subject: [PATCH 36/40] Update default preset to EU/UK (Narrow) --- platformio.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/platformio.ini b/platformio.ini index c47e757e..ba601c26 100644 --- a/platformio.ini +++ b/platformio.ini @@ -24,9 +24,9 @@ lib_deps = melopero/Melopero RV3028 @ ^1.1.0 electroniccats/CayenneLPP @ 1.6.1 build_flags = -w -DNDEBUG -DRADIOLIB_STATIC_ONLY=1 -DRADIOLIB_GODMODE=1 - -D LORA_FREQ=869.525 - -D LORA_BW=250 - -D LORA_SF=11 + -D LORA_FREQ=869.618 + -D LORA_BW=62.5 + -D LORA_SF=8 -D ENABLE_ADVERT_ON_BOOT=1 -D ENABLE_PRIVATE_KEY_IMPORT=1 ; NOTE: comment these out for more secure firmware -D ENABLE_PRIVATE_KEY_EXPORT=1 From f4748a7f9d4c5f1440c47c0bd52074f4e01e97b9 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Tue, 24 Feb 2026 21:30:04 +1100 Subject: [PATCH 37/40] * misc --- variants/lilygo_tbeam_supreme_SX1262/platformio.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini index 04e4b0bf..1ac622db 100644 --- a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini +++ b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini @@ -142,8 +142,8 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 - -D WIFI_SSID='"Three_CA7C65"' - -D WIFI_PWD='"8hC45a66265eA3w"' + -D WIFI_SSID='"WIFI_SSID"' + -D WIFI_PWD='"Password"' ; -D WIFI_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=8 ; -D MESH_DEBUG=1 From 15cce12efd47c41fe67575a7c36d2e9b9ec4f1ba Mon Sep 17 00:00:00 2001 From: Wessel Nieboer Date: Wed, 25 Feb 2026 02:43:48 +0100 Subject: [PATCH 38/40] Add basic sanity test github PR workflow Build a few generic variants to verify at least those compile. Can't hurt. --- .github/workflows/pr-build-check.yml | 43 ++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/pr-build-check.yml diff --git a/.github/workflows/pr-build-check.yml b/.github/workflows/pr-build-check.yml new file mode 100644 index 00000000..5ba677cd --- /dev/null +++ b/.github/workflows/pr-build-check.yml @@ -0,0 +1,43 @@ +name: PR Build Check + +on: + pull_request: + branches: [main, dev] + paths: + - 'src/**' + - 'examples/**' + - 'variants/**' + - 'platformio.ini' + - '.github/workflows/pr-build-check.yml' + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + environment: + # ESP32-S3 (most common platform) + - Heltec_v3_companion_radio_ble + - Heltec_v3_repeater + - Heltec_v3_room_server + # nRF52 + - RAK_4631_companion_radio_ble + - RAK_4631_repeater + - RAK_4631_room_server + # RP2040 + - PicoW_repeater + # STM32 + - wio-e5-mini_repeater + # ESP32-C6 + - LilyGo_Tlora_C6_repeater_ + + steps: + - name: Clone Repo + uses: actions/checkout@v4 + + - name: Setup Build Environment + uses: ./.github/actions/setup-build-environment + + - name: Build ${{ matrix.environment }} + run: pio run -e ${{ matrix.environment }} From 8737c64fdb47d0813d8bd5b63b45ae9607dd9e45 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Wed, 25 Feb 2026 17:10:31 +1100 Subject: [PATCH 39/40] * Packet::copyPath() fix --- src/Packet.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Packet.cpp b/src/Packet.cpp index 9fd06395..934020b3 100644 --- a/src/Packet.cpp +++ b/src/Packet.cpp @@ -30,7 +30,7 @@ size_t Packet::writePath(uint8_t* dest, const uint8_t* src, uint8_t path_len) { } uint8_t Packet::copyPath(uint8_t* dest, const uint8_t* src, uint8_t path_len) { - if (writePath(dest, src, path_len) == 0) return 0; + writePath(dest, src, path_len); return path_len; } From b67decfba06bbcafbb6909157b46a37c09106930 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Thu, 26 Feb 2026 15:36:21 +1100 Subject: [PATCH 40/40] * bug fix: Packet::writeTo(), Packet::readFrom() --- src/Packet.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Packet.cpp b/src/Packet.cpp index 934020b3..aad3e2f4 100644 --- a/src/Packet.cpp +++ b/src/Packet.cpp @@ -57,7 +57,7 @@ uint8_t Packet::writeTo(uint8_t dest[]) const { memcpy(&dest[i], &transport_codes[1], 2); i += 2; } dest[i++] = path_len; - memcpy(&dest[i], path, path_len); i += path_len; + i += writePath(&dest[i], path, path_len); memcpy(&dest[i], payload, payload_len); i += payload_len; return i; } @@ -72,8 +72,11 @@ bool Packet::readFrom(const uint8_t src[], uint8_t len) { transport_codes[0] = transport_codes[1] = 0; } path_len = src[i++]; - if (path_len > sizeof(path)) return false; // bad encoding - memcpy(path, &src[i], path_len); i += path_len; + if (!isValidPathLen(path_len)) return false; // bad encoding + + uint8_t bl = getPathByteLen(); + memcpy(path, &src[i], bl); i += bl; + if (i >= len) return false; // bad encoding payload_len = len - i; if (payload_len > sizeof(payload)) return false; // bad encoding