Merge meshcore-dev dev with routing, WiFi and power-policy compatibility

Integrate upstream 1.18.0, delayed direct ACK forwarding, zero-scope routing, Waveshare LR1121 and dynamic WiFi variants. Preserve fork chemistry-aware power management and bounded multi-interface UI; retain retry/dedup safety while adding upstream ACK delay. Qualify LR1121 band power and HP-only Morse handling with native regressions.
This commit is contained in:
mikecarper committed 2026-10-15 13:30:41 -07:00
commit 285267e3a1
32 files changed
+765 -27

No files matched your search

+6
View File
@@ -87,6 +87,12 @@ struct Chip {
standby(); if (power_result) return power_result;
power=value; rfo=force_rfo; ramp_time=4; return 0;
}
int checkOutputPower(int8_t value, int8_t* clipped, bool force_hp) {
const int minimum=frequency>1000 ? -18 : force_hp || value>14 ? -9 : -17;
const int maximum=frequency>1000 ? 13 : force_hp || value>14 ? 22 : 14;
if (clipped) *clipped=value<minimum ? minimum : value>maximum ? maximum : value;
return value<minimum || value>maximum ? -1 : 0;
}
int setOutputPower(int8_t value, bool force_rfo, uint16_t ramp) {
++ramp_power_calls;
const int result=setOutputPower(value,force_rfo);
@@ -76,6 +76,7 @@ public:
bool forward_packets = false, reject_primary_groups = false;
bool bind_secondary_groups = false, packet_group_search = false;
uint32_t direct_forward_delay = 0;
uint8_t extra_acks = 0;
unsigned group_admissions = 0, group_searches = 0, received_groups = 0;
uint8_t last_search_origin = 0xFF;
mesh::GroupChannel primary_group, secondary_group, received_group;
@@ -88,6 +89,7 @@ public:
bool canMutateContacts() const override { return writable; }
bool allowPacketForward(const mesh::Packet*) override { return forward_packets; }
uint32_t getDirectRetransmitDelay(const mesh::Packet*) override { return direct_forward_delay; }
uint8_t getExtraAckTransmitCount() const override { return extra_acks; }
bool allowGroupPacketOnRadio(mesh::Packet* packet) override {
++group_admissions;
// Stub only the application's policy, not the core admission/order path.
@@ -500,6 +502,72 @@ TEST_F(ContactImport, GroupAdmissionBindsDirectRelayBeforeCrossProfileFanout) {
EXPECT_EQ(relayed->path[0], 0x93);
EXPECT_EQ(pool.getFreeCount(), 7);
}
TEST_F(ContactImport, RelayedAckHonorsInitialDirectForwardDelay) {
chat.forward_packets = true;
chat.direct_forward_delay = 175;
mesh::Packet packet;
packet.header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_ACK << PH_TYPE_SHIFT);
packet.setPathHashSizeAndCount(1, 2);
packet.path[0] = chat.self_id.pub_key[0];
packet.path[1] = 0x93;
packet.payload_len = 4;
memset(packet.payload, 0x37, packet.payload_len);
chat.receive(packet);
ASSERT_EQ(pool.getOutboundTotal(), 1);
EXPECT_EQ(pool.getOutboundCount(174), 0);
EXPECT_EQ(pool.getOutboundCount(175), 1);
EXPECT_EQ(pool.getOutboundByIdx(0)->getPathHashCount(), 1);
EXPECT_EQ(pool.getOutboundByIdx(0)->path[0], 0x93);
EXPECT_FALSE(pool.getOutboundByIdx(0)->radio_reply);
}
TEST_F(ContactImport, ExtraForwardedAcksKeepInitialDelayAndThreeHundredMsSpacing) {
chat.forward_packets = true;
chat.direct_forward_delay = 175;
chat.extra_acks = 3;
mesh::Packet packet;
packet.header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_ACK << PH_TYPE_SHIFT);
packet.setPathHashSizeAndCount(1, 1);
packet.path[0] = chat.self_id.pub_key[0];
packet.payload_len = 4;
memset(packet.payload, 0x47, packet.payload_len);
chat.receive(packet);
ASSERT_EQ(pool.getOutboundTotal(), 4);
for (uint32_t index = 0; index < 4; ++index) {
const uint32_t due = 175 + 300 * index;
EXPECT_EQ(pool.getNextOutbound(due - 1), nullptr);
auto* ack = pool.getNextOutbound(due);
ASSERT_NE(ack, nullptr);
EXPECT_EQ(ack->getPayloadType(), index < 3 ? PAYLOAD_TYPE_MULTIPART : PAYLOAD_TYPE_ACK);
EXPECT_EQ(ack->getPathHashCount(), 0);
EXPECT_FALSE(ack->radio_reply);
pool.free(ack);
}
EXPECT_EQ(pool.getFreeCount(), 8);
}
TEST_F(ContactImport, MultipartAckAddsInitialDelayWithoutLosingDuplicateProtection) {
chat.forward_packets = true;
chat.direct_forward_delay = 175;
tables.deduplicate = true;
mesh::Packet packet;
packet.header = ROUTE_TYPE_DIRECT | (PAYLOAD_TYPE_MULTIPART << PH_TYPE_SHIFT);
packet.setPathHashSizeAndCount(1, 2);
packet.path[0] = chat.self_id.pub_key[0];
packet.path[1] = 0x93;
packet.payload_len = 5;
packet.payload[0] = (2 << 4) | PAYLOAD_TYPE_ACK;
memset(packet.payload + 1, 0x57, packet.payload_len - 1);
const auto duplicate = packet;
chat.receive(packet);
ASSERT_EQ(pool.getOutboundTotal(), 1);
EXPECT_EQ(pool.getOutboundCount(1074), 0);
EXPECT_EQ(pool.getOutboundCount(1075), 1);
auto second = duplicate;
chat.receive(second);
EXPECT_EQ(pool.getOutboundTotal(), 1);
EXPECT_EQ(tables.seen_marks, 1u);
}
} // namespace
int main(int argc, char** argv) {
+182
View File
@@ -0,0 +1,182 @@
"""Execute merged LR1121 band/power and Morse PA selection without hardware."""
from pathlib import Path
import os
import shutil
import subprocess
import tempfile
import unittest
from test_radio_receive_contract import method
ROOT = Path(__file__).resolve().parents[1]
HARNESS = r'''
#include <cassert>
#include <cstdint>
#include <algorithm>
constexpr int16_t RADIOLIB_ERR_NONE=0;
constexpr int16_t RADIOLIB_ERR_INVALID_FREQUENCY=-705;
constexpr int8_t LORA_TX_POWER=22;
class LR1121 {
public:
float frequency=909.5f;
int power_calls=0,frequency_calls=0,checks=0,overload_calls=0;
int16_t power_result=0,frequency_result=0;
int8_t applied_power=0;
bool forced_hp=false;
uint32_t applied_ramp=0;
virtual ~LR1121()=default;
virtual int16_t setOutputPower(int8_t power) { return setOutputPower(power,false); }
int16_t setOutputPower(int8_t power,bool hp,uint32_t ramp=48) {
++power_calls;applied_power=power;forced_hp=hp;applied_ramp=ramp;return power_result;
}
int16_t checkOutputPower(int8_t power,int8_t* clipped,bool hp) {
++checks;
const int minimum=frequency>1000 ? -18 : hp || power>14 ? -9 : -17;
const int maximum=frequency>1000 ? 13 : hp || power>14 ? 22 : 14;
if(clipped) *clipped=std::max(minimum,std::min(maximum,int(power)));
return power<minimum || power>maximum ? -1 : 0;
}
virtual int16_t setFrequency(float value) {
++frequency_calls;if(frequency_result==0)frequency=value;return frequency_result;
}
int16_t setFrequency(float value,bool,float=4) {
++overload_calls;return LR1121::setFrequency(value);
}
};
class CustomLR1121:public LR1121 {
int8_t _txPower=0;
bool _txPowerSet=false;
public:
using LR1121::setOutputPower;
using LR1121::setFrequency;
@POWER@
@FREQUENCY@
};
struct Board {
bool frequency_ok=true;
bool prepareRadioFrequency(float) { return frequency_ok; }
};
struct CarrierBase {
virtual ~CarrierBase()=default;
virtual int16_t prepareMorseCarrier(float)=0;
virtual bool restoreCarrierWaveModem()=0;
};
class MorseWrapper:public CarrierBase {
public:
LR1121* _radio;
Board* _board;
bool _dbm_valid=true,_morse_rx_fsk_mode=false;
int8_t _cur_dbm=22;
float _cur_freq=909.5f;
MorseWrapper(LR1121* radio,Board* board):_radio(radio),_board(board) {}
bool restoreAfterDeepInit() { return true; }
@CARRIER@
@RESTORE@
};
int main() {
#ifdef LR1121_FORCE_HP_PA
const bool expected_hp=true;
#else
const bool expected_hp=false;
#endif
CustomLR1121 radio;
assert(radio.setFrequency(909.5f)==0 && radio.power_calls==0);
assert(radio.setOutputPower(22)==0 && radio.applied_power==22);
assert(radio.forced_hp==expected_hp);
assert(radio.setFrequency(2400.0f)==0 && radio.applied_power==13);
assert(radio.setFrequency(909.5f)==0 && radio.applied_power==22);
// A failed explicit power write must not replace the last successful intent.
radio.power_result=-77;
assert(radio.setOutputPower(7)==-77);
radio.power_result=0;
assert(radio.setFrequency(2400.0f)==0 && radio.applied_power==13);
assert(radio.setFrequency(909.5f)==0 && radio.applied_power==22);
// A failed tune must not touch the PA. A failed PA reapply must propagate.
const int calls=radio.power_calls;
radio.frequency_result=-78;
assert(radio.setFrequency(2400.0f)==-78 && radio.power_calls==calls);
radio.frequency_result=0;radio.power_result=-79;
assert(radio.setFrequency(2400.0f)==-79);
radio.power_result=0;
assert(radio.setFrequency(909.5f)==0 && radio.applied_power==22);
assert(radio.setOutputPower(2)==0 && radio.forced_hp==expected_hp);
// The existing explicit calibration/ramp overloads used by Morse stay visible.
assert(radio.setFrequency(902.1f,true)==0 && radio.overload_calls==1);
assert(radio.setOutputPower(2,false,304)==0);
Board board;
MorseWrapper morse(&radio,&board);
morse._cur_dbm=2;
assert(morse.prepareMorseCarrier(909.5f)==0);
assert(radio.applied_power==2 && radio.forced_hp==expected_hp && radio.applied_ramp==304);
morse._cur_dbm=22;
assert(morse.prepareMorseCarrier(2400.0f)==0);
assert(radio.applied_power==13 && radio.applied_ramp==304);
assert(morse.restoreCarrierWaveModem());
assert(radio.frequency==909.5f && radio.applied_power==22 && radio.forced_hp==expected_hp);
morse._dbm_valid=false;
assert(morse.prepareMorseCarrier(2400.0f)==0 && radio.applied_power==13);
assert(morse.restoreCarrierWaveModem() && radio.applied_power==22);
board.frequency_ok=false;
const int tunes=radio.frequency_calls;
assert(morse.prepareMorseCarrier(2400.0f)==RADIOLIB_ERR_INVALID_FREQUENCY);
assert(radio.frequency_calls==tunes);
board.frequency_ok=true;radio.frequency_result=-80;
assert(morse.prepareMorseCarrier(2400.0f)==-80);
assert(!morse.restoreCarrierWaveModem());
radio.frequency_result=0;radio.power_result=-81;
assert(morse.prepareMorseCarrier(2400.0f)==-81);
}
'''
class UpstreamLR1121Test(unittest.TestCase):
def compile(self, *flags):
wrapper = (ROOT / "src/helpers/radiolib/CustomLR1121.h").read_text()
source = HARNESS.replace("@POWER@", method(wrapper, "int16_t setOutputPower(int8_t power)"))
source = source.replace("@FREQUENCY@", method(wrapper, "int16_t setFrequency(float freq)"))
carrier = (ROOT / "src/helpers/radiolib/CustomLR1121Wrapper.h").read_text()
source = source.replace("@CARRIER@", method(carrier, "int16_t prepareMorseCarrier("))
source = source.replace("@RESTORE@", method(carrier, "bool restoreCarrierWaveModem("))
compiler = os.environ.get("CXX") or shutil.which("g++")
self.assertIsNotNone(compiler)
with tempfile.TemporaryDirectory(prefix="upstream-lr1121-") as directory:
folder = Path(directory)
source_file = folder / "test.cpp"
source_file.write_text(source)
binary = folder / ("test.exe" if os.name == "nt" else "test")
sanitizers = [] if os.name == "nt" else [
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie"]
built = subprocess.run([
compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror", *sanitizers,
*flags, str(source_file), "-o", str(binary)
], capture_output=True, text=True, timeout=60)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
ran = subprocess.run([str(binary)], capture_output=True, text=True, timeout=15)
self.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr)
def test_band_changes_restore_original_power_and_propagate_failures(self):
self.compile()
def test_hp_only_boards_keep_force_high_power(self):
self.compile("-DLR1121_FORCE_HP_PA=1")
def test_merge_preserves_morse_switch_and_bounded_companion_ui(self):
radio = (ROOT / "src/helpers/radiolib/CustomLR1121.h").read_text()
self.assertIn("LR11x0MorseRfSwitch _morse_rf_switch;", radio)
self.assertIn("_morse_rf_switch.capture", method(radio, "void setRfSwitchTable("))
self.assertIn("_morse_rf_switch.restore", method(radio, "int16_t beginMorseReceive("))
self.assertIn("using LR1121::setOutputPower;", radio)
self.assertIn("using LR1121::setFrequency;", radio)
ui = (ROOT / "examples/companion_radio/ui-new/UITask.cpp").read_text()
self.assertIn("makeLargeCompanionHomeLayout", ui)
self.assertIn("drawTextCenteredEllipsized", ui)
self.assertIn("isCompanionWiFiConnected()", ui)
self.assertIn("shouldDisplayBluetoothPairingPin", ui)
self.assertNotIn('display.drawTextCentered(display.width() / 2, 32, tmp)', ui)
if __name__ == "__main__":
unittest.main()
+150
View File
@@ -0,0 +1,150 @@
#!/usr/bin/env python3
"""Keep chemistry-aware nRF52 safety through upstream bootlock-stopgap merges.
This parses the actual recipes without PlatformIO and executes the production
voltage rules on the host. It does not claim physical ADC/low-voltage testing.
"""
import configparser
from pathlib import Path
import os
import re
import shutil
import subprocess
import sys
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
BOARDS = {
"gat562_30s_mesh_kit": "GAT562_30S_Mesh_Kit",
"gat562_mesh_evb_pro": "GAT562_Mesh_EVB_Pro",
"gat562_mesh_tracker_pro": "GAT562_Mesh_Tracker_Pro",
"gat562_mesh_watch13": "GAT562_Mesh_Watch13",
"heltec_t096": "Heltec_t096",
"heltec_t114": "Heltec_t114",
"muziworks_r1_neo": "R1Neo",
"rak3401": "rak3401",
"rak4631": "rak4631",
"sensecap_solar": "SenseCap_Solar",
"xiao_nrf52": "Xiao_nrf52",
}
class Recipes:
def __init__(self):
self.config = configparser.ConfigParser(interpolation=None,
inline_comment_prefixes=(";",))
self.config.read([ROOT / "platformio.ini",
*sorted(ROOT.glob("variants/*/platformio.ini"))])
def value(self, section, option, seen=()):
pair = (section, option)
if pair in seen:
raise AssertionError(f"recipe interpolation cycle: {pair}")
seen = (*seen, pair)
if not self.config.has_option(section, option):
for parent in self.config.get(section, "extends", fallback="").split(","):
if parent.strip():
value = self.value(parent.strip(), option, seen)
if value:
return value
return ""
raw = self.config.get(section, option)
return re.sub(r"\$\{([^}.]+)\.([^}]+)\}",
lambda match: self.value(match[1], match[2], seen), raw)
class UpstreamPowerPolicyMergeTests(unittest.TestCase):
def test_all_eleven_board_bases_keep_chemistry_power_policy(self):
recipes = Recipes()
for directory, section in BOARDS.items():
with self.subTest(board=directory):
self.assertTrue(recipes.config.has_section(section))
flags = recipes.value(section, "build_flags")
directives = re.findall(
r"(?:^|\s)-([DU])\s*NRF52_POWER_MANAGEMENT(?=$|\s|=)", flags)
self.assertTrue(directives, f"{directory}: voltage policy compiled out")
self.assertEqual(directives[-1], "D", f"{directory}: policy later undefined")
self.assertNotIn("NRF52_POWER_MANAGEMENT",
recipes.value(section, "build_unflags"))
def test_affected_board_roles_inherit_chemistry_policy(self):
recipes = Recipes()
roles = 0
for directory in BOARDS:
local = configparser.ConfigParser(interpolation=None)
local.read(ROOT / "variants" / directory / "platformio.ini")
for section in local.sections():
if not section.startswith("env:"):
continue
with self.subTest(role=section):
flags = recipes.value(section, "build_flags")
directives = re.findall(
r"(?:^|\s)-([DU])\s*NRF52_POWER_MANAGEMENT(?=$|\s|=)", flags)
self.assertTrue(directives)
self.assertEqual(directives[-1], "D")
self.assertNotIn("NRF52_POWER_MANAGEMENT",
recipes.value(section, "build_unflags"))
roles += 1
self.assertGreater(roles, 30)
def test_voltage_policy_still_loads_saved_chemistry_and_uses_safe_adc(self):
board = (ROOT / "src/helpers/NRF52Board.cpp").read_text()
policy = (ROOT / "src/helpers/NRF52VoltagePolicy.cpp").read_text()
header = (ROOT / "src/helpers/NRF52VoltagePolicy.h").read_text()
self.assertIn("defined(NRF52_POWER_MANAGEMENT)", header)
self.assertIn("handleVoltagePolicyCommand", header)
self.assertIn("loadVoltagePolicyAtBoot(config->voltage_bootlock)", board)
self.assertIn("medianVoltage(samples[0], samples[1], samples[2])", board)
self.assertIn("if (threshold == 0 || external) return true;", board)
self.assertIn("lifepo4", policy)
self.assertIn("configuredBootlock()", policy)
self.assertIn("configuredCutoff()", policy)
self.assertIn("AtomicFileWriter", policy)
def test_native_voltage_rules_preserve_lifepo4_cutoff_and_boot_validation(self):
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
if not compiler:
self.skipTest("a host C++17 compiler is required")
source = r'''
#include <helpers/NRF52VoltagePolicy.h>
#include <helpers/PowerManagementUtils.h>
#include <cassert>
#include <type_traits>
int main() {
using namespace mesh::power;
static_assert(std::is_same<decltype(&configuredBootlock), uint16_t(*)()>::value, "boot API");
static_assert(std::is_same<decltype(&configuredCutoff), uint16_t(*)()>::value, "cutoff API");
assert(validThresholdPair(3300, 0, false));
assert(validThresholdPair(3000, 2700, false));
assert(!validThresholdPair(0, 0, false));
assert(validThresholdPair(0, 0, true));
assert(!validThresholdPair(3000, 3100, false));
assert(batteryPercent(3550, 2700, 3550) == 100);
assert(batteryPercent(2700, 2700, 3550) == 0);
assert(medianVoltage(999, 3500, 3510) == 3500);
assert(!shouldBootLock(999, 3300, false));
assert(!shouldBootLock(3000, 3300, true));
assert(shouldBootLock(3000, 3300, false));
assert(nextLowVoltageCount(2, 2600, 2700, false) == 3);
assert(nextLowVoltageCount(2, 2600, 2700, true) == 0);
}
'''
with tempfile.TemporaryDirectory(prefix="meshcore-power-merge-") as directory:
cpp = Path(directory) / "test.cpp"
binary = Path(directory) / "test.exe"
cpp.write_text(source)
sanitizers = (["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
"-fno-pie", "-no-pie"] if sys.platform.startswith("linux") else [])
built = subprocess.run(
[compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror", *sanitizers,
"-DNRF52_PLATFORM=1", "-DNRF52_POWER_MANAGEMENT=1",
f"-I{ROOT / 'src'}", str(cpp), "-o", str(binary)],
capture_output=True, text=True)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
run = subprocess.run([str(binary)], capture_output=True, text=True)
self.assertEqual(run.returncode, 0, run.stdout + run.stderr)
if __name__ == "__main__":
unittest.main()
+77
View File
@@ -0,0 +1,77 @@
"""Check upstream zero-scope semantics without dropping fork safety rules."""
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
class UpstreamRoutingMergeTests(unittest.TestCase):
def test_zero_transport_code_matches_wildcard_without_key_lookup(self):
method = extract_braced((ROOT / "src/helpers/RegionMap.cpp").read_text(),
"RegionEntry* RegionMap::findMatch(")
compiler = shutil.which("g++") or shutil.which("c++")
if not compiler:
self.skipTest("native C++ compiler required")
source = r'''
#include <cassert>
#include <cstdint>
#include <cstddef>
namespace mesh { struct Packet { uint16_t transport_codes[2] = {}; }; }
struct RegionEntry { uint8_t flags; };
struct TransportKey {
uint16_t code = 0x1234;
uint16_t calcTransportCode(const mesh::Packet*) const { return code; }
};
class RegionMap {
public:
RegionEntry wildcard{0}, regions[1]{{0}};
int num_regions = 1, lookups = 0;
int getTransportKeysFor(const RegionEntry&, TransportKey keys[], int) {
++lookups; keys[0].code = 0x1234; return 1;
}
RegionEntry* findMatch(const mesh::Packet*, uint8_t);
};
@METHOD@
int main() {
RegionMap regions;
mesh::Packet packet;
const mesh::Packet* input = &packet;
assert(regions.findMatch(input, 1) == &regions.wildcard);
assert(regions.lookups == 0);
packet.transport_codes[1] = 0x9999;
assert(regions.findMatch(input, 1) == &regions.wildcard);
packet.transport_codes[0] = 0x1234;
assert(regions.findMatch(input, 1) == &regions.regions[0]);
regions.regions[0].flags = 1;
assert(regions.findMatch(input, 1) == nullptr);
regions.regions[0].flags = 0;
packet.transport_codes[0] = 0x9999;
assert(regions.findMatch(input, 1) == nullptr);
}
'''.replace("@METHOD@", method)
with tempfile.TemporaryDirectory(prefix="mesh-upstream-routing-") as directory:
cpp = Path(directory) / "routing.cpp"
exe = Path(directory) / "routing"
cpp.write_text(source)
subprocess.run([compiler, "-std=c++11", "-Wall", "-Wextra", "-Werror",
str(cpp), "-o", str(exe)], check=True, timeout=30,
capture_output=True)
subprocess.run([str(exe)], check=True, timeout=10, capture_output=True)
def test_room_repeat_deprecation_keeps_full_rules_and_clock_sync(self):
source = (ROOT / "examples/simple_room_server/MyMesh.cpp").read_text()
handler = extract_braced(source, "void MyMesh::handleCommand(")
self.assertIn('strcmp(command, "set repeat on") == 0', handler)
self.assertIn('"ERR - repeat now deprecated"', handler)
self.assertIn("flood_rules.handleCommand(command, reply)", handler)
self.assertIn("_clock_sync.handleCommand(command, reply)", handler)
self.assertIn("_local_cli_output.owns", handler)
if __name__ == "__main__":
unittest.main()