Merge upstream companion-v1.18.0

Fixes and features from upstream: MACThenDecrypt / command buffer
overflow fixes, ACK forwarding delay, transport code 0 as unscoped,
hidden nodes don't answer DISCOVER, LR2021/LR1121 fixes, GPS cold-start
detection and waitFor()/drain(), NMEA fix-age counters, ESP32 RTC after a
crash, remote CLI by text message (TXT_TYPE_CLI_COMMAND),
PUSH_CODE_BINARY_RESPONSE, board.loop(), new boards (Wio Tracker L1 1W,
ThinkNode M4/M8, Muzi Base, Waveshare LR1121, Pico W).

Kept ours: NodePrefs, main.cpp, every UI, GxEPDDisplay. The companion CLI
stays MyMeshCli.h's handleCliCommand(); upstream's handleCommand() calls
it. The board's FEM gains go through attachDynamicPrefs() with an adapter
over our NodePrefs fields. Upstream's JSON prefs and MultiSerialInterface
come as their own stages.

Also: the display defaults were inside RADIO_FEM_TXGAIN's #else (a
missing #endif).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-10-15 13:11:48 +02:00
co-authored by Claude Opus 5.5
188 changed files with 6226 additions and 789 deletions
@@ -1,13 +1,6 @@
#include <gtest/gtest.h>
#include "helpers/ConfigSerializer.h"
class NativeFileSystem {
public:
void mkdir(const char*) { }
};
#define FILESYSTEM NativeFileSystem
#include "helpers/CommonCLI.h"
#undef FILESYSTEM
#include "helpers/DynamicConfigSerializer.h"
#define TEST_INT_S "56"
#define TEST_INT 56
@@ -192,28 +185,131 @@ TEST(ConfigSerializer, LoadSerial_IgnoreUnknowns) {
EXPECT_TRUE(match);
}
TEST(NodePrefs, FemGainSettingsRoundTrip) {
NodePrefs saved;
saved.radio_fem_rxgain = 0;
saved.radio_fem_txgain = 1;
class TestNested : public ConfigSerializer {
class Inner : public ConfigSerializer {
protected:
void structure() override { } // no properties, so it writes as '{}'
};
Inner inner;
protected:
void structure() override {
def("age", age);
def("inner", inner);
def("name", name, sizeof(name)); // comes *after* the empty sub-object
}
public:
int32_t age;
char name[16];
};
MockPrintStream output;
ASSERT_TRUE(saved.saveSerial(output));
TEST(ConfigSerializer, LoadSerial_EmptyObject) {
MockInputStream s("{age:" TEST_INT_S ",inner:{},name:\"Scott\"}");
TestNested data;
data.name[0] = 0;
std::string serialised(reinterpret_cast<const char*>(output.getBytes()), output.getLength());
EXPECT_NE(std::string::npos, serialised.find("fem_rxgain:0"));
EXPECT_NE(std::string::npos, serialised.find("fem_txgain:1"));
bool success = data.loadSerial(s);
EXPECT_TRUE(success);
MockInputStream input(serialised.c_str());
NodePrefs loaded;
loaded.radio_fem_rxgain = 1;
loaded.radio_fem_txgain = 0;
ASSERT_TRUE(loaded.loadSerial(input));
EXPECT_EQ(0, loaded.radio_fem_rxgain);
EXPECT_EQ(1, loaded.radio_fem_txgain);
EXPECT_EQ(TEST_INT, data.age);
bool match = strcmp("Scott", data.name) == 0;
EXPECT_TRUE(match); // properties after an empty object must still load
}
TEST(ConfigSerializer, LoadSerial_EmptyObjectWithWhitespace) {
MockInputStream s("{age:" TEST_INT_S ",inner:{ },name:\"Scott\"}");
TestNested data;
data.name[0] = 0;
bool success = data.loadSerial(s);
EXPECT_TRUE(success);
bool match = strcmp("Scott", data.name) == 0;
EXPECT_TRUE(match);
}
TEST(DynamicConfigSerializer, GetSet_Basic) {
DynamicConfigSerializer data;
bool s1 = data.setByKey("age", "11");
bool s2 = data.setByKey("name", "Scott");
EXPECT_TRUE(s1 && s2);
char tmp[32];
bool g1 = data.getByKey("age", tmp, 31);
EXPECT_TRUE(g1);
EXPECT_STREQ("11", tmp);
bool g2 = data.getByKey("name", tmp, 31);
EXPECT_TRUE(g2);
EXPECT_STREQ("Scott", tmp);
}
TEST(DynamicConfigSerializer, Set_Replaces) {
DynamicConfigSerializer data;
bool s1 = data.setByKey("age", "11");
bool s2 = data.setByKey("name", "Scott");
EXPECT_TRUE(s1 && s2);
bool s3 = data.setByKey("age", "333");
EXPECT_TRUE(s3);
char tmp[32];
bool g1 = data.getByKey("age", tmp, 31);
EXPECT_TRUE(g1);
EXPECT_STREQ("333", tmp);
bool g2 = data.getByKey("name", tmp, 31);
EXPECT_TRUE(g2);
EXPECT_STREQ("Scott", tmp);
}
TEST(DynamicConfigSerializer, GetUnknown_Fail) {
DynamicConfigSerializer data;
bool s1 = data.setByKey("age", "11");
EXPECT_TRUE(s1);
char tmp[32];
bool g2 = data.getByKey("name", tmp, 31);
EXPECT_FALSE(g2);
}
TEST(DynamicConfigSerializer, SaveCustom_Basic) {
MockPrintStream s;
DynamicConfigSerializer data;
bool s1 = data.setByKey("age", "11");
bool s2 = data.setByKey("name", "Scott");
EXPECT_TRUE(s1 && s2);
bool success = data.saveSerial(s);
EXPECT_TRUE(success);
auto l = s.getLength();
char tmp[128];
memcpy(tmp, s.getBytes(), l);
tmp[l] = 0;
const char* expect = "{age:\"11\",name:\"Scott\"}";
EXPECT_STREQ(expect, tmp);
}
TEST(DynamicConfigSerializer, LoadCustom_Basic) {
MockInputStream s("{age:\"" TEST_INT_S "\",name:\"Scott\"}");
DynamicConfigSerializer data;
bool success = data.loadSerial(s);
EXPECT_TRUE(success);
char tmp[32];
bool g1 = data.getByKey("age", tmp, 31);
EXPECT_TRUE(g1);
EXPECT_STREQ(TEST_INT_S, tmp);
bool g2 = data.getByKey("name", tmp, 31);
EXPECT_TRUE(g2);
EXPECT_STREQ("Scott", tmp);
}
// ── main ───────────────────────────────────────────────────────