From f99ea79b362b8791ccd9ae3a57c14d8b40168a1c Mon Sep 17 00:00:00 2001 From: Marcel Verdult Date: Sat, 26 Sep 2026 20:39:23 +0200 Subject: [PATCH 01/26] Fix ThinkNode M8 repeater/room server build: compile sensor helpers for all envs --- variants/thinknode_m8/platformio.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/variants/thinknode_m8/platformio.ini b/variants/thinknode_m8/platformio.ini index db19e4466..57f2a938c 100644 --- a/variants/thinknode_m8/platformio.ini +++ b/variants/thinknode_m8/platformio.ini @@ -40,6 +40,7 @@ build_flags = ${nrf52_base.build_flags} -D LORA_TX_POWER=22 build_src_filter = ${nrf52_base.build_src_filter} + + + + +<../variants/thinknode_m8> lib_deps = @@ -110,7 +111,6 @@ build_src_filter = ${ThinkNode_M8.build_src_filter} + + + - + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = @@ -144,7 +144,6 @@ build_src_filter = ${ThinkNode_M8.build_src_filter} + + + - + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = From 9d93a7269322a046dc5ace6998bdfdda2d470a13 Mon Sep 17 00:00:00 2001 From: amdoolittle Date: Fri, 16 Oct 2026 22:39:26 -0700 Subject: [PATCH 02/26] Added WiFi to TFT BLE version of Heltec V4 --- variants/heltec_v4/platformio.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/variants/heltec_v4/platformio.ini b/variants/heltec_v4/platformio.ini index 76e8e714e..c91e31a80 100644 --- a/variants/heltec_v4/platformio.ini +++ b/variants/heltec_v4/platformio.ini @@ -380,12 +380,14 @@ build_flags = -D AUTO_SHUTDOWN_MILLIVOLTS=3400 -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_WIFI_INTERFACE ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${heltec_v4_tft.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = From 85ad886589e29ec1dfb3f41ab00f97f3adc6d730 Mon Sep 17 00:00:00 2001 From: rado <6479266+rbrus@users.noreply.github.com> Date: Sat, 17 Oct 2026 09:36:33 +0200 Subject: [PATCH 03/26] Fix unsigned conversion of negative TX power in CommonRadioPrefs --- examples/companion_radio/NodePrefs.h | 4 +- src/helpers/CommonCLI.h | 4 +- src/helpers/CommonRadioPrefs.h | 4 +- .../test_companion_node_prefs.cpp | 57 +++++++++++++++++++ 4 files changed, 63 insertions(+), 6 deletions(-) diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 3cceed566..1cc5b5018 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -102,8 +102,8 @@ private: void setIntThresh(uint8_t t) override { _parent->interference_threshold = t; markDirty(); } uint8_t getRxGain() const override { return _parent->rx_boosted_gain; } void setRxGain(uint8_t g) override { _parent->rx_boosted_gain = g; markDirty(); } - uint8_t getTxPower() const override { return _parent->tx_power_dbm; } - void setTxPower(uint8_t dbm) override { _parent->tx_power_dbm = dbm; markDirty(); } + int8_t getTxPower() const override { return _parent->tx_power_dbm; } + void setTxPower(int8_t dbm) override { _parent->tx_power_dbm = dbm; markDirty(); } float getRxDelay() const override { return _parent->rx_delay_base; } void setRxDelay(float d) override { _parent->rx_delay_base = d; markDirty(); } uint16_t getAgcResetInt() const override { return _parent->agc_reset_interval * 4; } diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 2759b0c4f..675df7a4b 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -115,8 +115,8 @@ private: void setIntThresh(uint8_t t) override { _parent->interference_threshold = t; markDirty(); } uint8_t getRxGain() const override { return _parent->rx_boosted_gain; } void setRxGain(uint8_t g) override { _parent->rx_boosted_gain = g; markDirty(); } - uint8_t getTxPower() const override { return _parent->tx_power_dbm; } - void setTxPower(uint8_t dbm) override { _parent->tx_power_dbm = dbm; markDirty(); } + int8_t getTxPower() const override { return _parent->tx_power_dbm; } + void setTxPower(int8_t dbm) override { _parent->tx_power_dbm = dbm; markDirty(); } float getRxDelay() const override { return _parent->rx_delay_base; } void setRxDelay(float d) override { _parent->rx_delay_base = d; markDirty(); } uint16_t getAgcResetInt() const override { return _parent->agc_reset_interval * 4; } diff --git a/src/helpers/CommonRadioPrefs.h b/src/helpers/CommonRadioPrefs.h index 0e83ca9df..d0083b8c7 100644 --- a/src/helpers/CommonRadioPrefs.h +++ b/src/helpers/CommonRadioPrefs.h @@ -35,8 +35,8 @@ public: virtual uint8_t getRxGain() const = 0; virtual void setRxGain(uint8_t g) = 0; - virtual uint8_t getTxPower() const = 0; - virtual void setTxPower(uint8_t dbm) = 0; + virtual int8_t getTxPower() const = 0; + virtual void setTxPower(int8_t dbm) = 0; virtual float getRxDelay() const = 0; virtual void setRxDelay(float d) = 0; diff --git a/test/test_companion_node_prefs/test_companion_node_prefs.cpp b/test/test_companion_node_prefs/test_companion_node_prefs.cpp index 433c97106..ab5e0f908 100644 --- a/test/test_companion_node_prefs/test_companion_node_prefs.cpp +++ b/test/test_companion_node_prefs/test_companion_node_prefs.cpp @@ -6,6 +6,32 @@ #include "../../examples/companion_radio/NodePrefs.h" +bool CommonRadioPrefs::getByKey(const char* key, char* value, size_t max_len) { + if (strcmp(key, "fem_rxgain") == 0) { + snprintf(value, max_len, "%d", (uint32_t)getFEMRxGain()); + return true; + } + if (strcmp(key, "fem_txgain") == 0) { + snprintf(value, max_len, "%d", (uint32_t)getFEMTxGain()); + return true; + } + return false; +} + +bool CommonRadioPrefs::setByKey(const char* key, const char* value) { + if (strcmp(key, "fem_rxgain") == 0) { + setFEMRxGain(atoi(value)); + markDirty(); + return true; + } + if (strcmp(key, "fem_txgain") == 0) { + setFEMTxGain(atoi(value)); + markDirty(); + return true; + } + return false; +} + class ReplayStream : public Stream { const char* _text; int _pos = 0; @@ -77,6 +103,37 @@ TEST(CompanionNodePrefs, RxGainSettingsRoundTripIndependently) { } #endif +TEST(CompanionNodePrefs, NegativeTxPowerPreservedAsSigned) { + NodePrefs prefs; + CommonRadioPrefs* radio = prefs.getRadioPrefs(); + + radio->setTxPower(-9); + EXPECT_EQ(-9, prefs.tx_power_dbm); + EXPECT_EQ(-9, radio->getTxPower()); + EXPECT_EQ(-9, static_cast(radio->getTxPower())); + + char reply[32]; + snprintf(reply, sizeof(reply), "> %d", static_cast(radio->getTxPower())); + EXPECT_STREQ("> -9", reply); +} + +TEST(CompanionNodePrefs, NegativeTxPowerRoundTripsSerial) { + NodePrefs saved; + saved.getRadioPrefs()->setTxPower(-9); + + CaptureStream output; + ASSERT_TRUE(saved.saveSerial(output)); + EXPECT_NE(std::string::npos, output.text().find("tx:-9")); + + ReplayStream input("{radio:{tx:-9}}"); + NodePrefs loaded; + loaded.tx_power_dbm = 22; + + ASSERT_TRUE(loaded.loadSerial(input)); + EXPECT_EQ(-9, loaded.tx_power_dbm); + EXPECT_EQ(-9, loaded.getRadioPrefs()->getTxPower()); +} + int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); From b7389353e2a1c8c89e38d6a6285cdb9e67908a3b Mon Sep 17 00:00:00 2001 From: Confituurke <9595028+Confituurke@users.noreply.github.com> Date: Sat, 17 Oct 2026 13:03:06 +0200 Subject: [PATCH 04/26] Show quarter-hour timezone offsets with two decimals 'get tz.offset' printed one decimal, so a UTC+5:45 offset (5.75) read back as 5.8. Print two decimals when the offset is on a quarter hour (x.25 / x.75); everything else keeps one decimal. --- 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 99a9aa30d..2ea539dbc 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2192,7 +2192,7 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* } if (strcmp(command, "get tz.offset") == 0) { - sprintf(reply, "> %.1f", _prefs.tz_offset); + sprintf(reply, ((int)(_prefs.tz_offset * 4) % 2) ? "> %.2f" : "> %.1f", _prefs.tz_offset); return true; } if (memcmp(command, "set tz.offset ", 14) == 0) { From 933c0f6a983a7cef8af95b9ab8c323ed2f1cdc45 Mon Sep 17 00:00:00 2001 From: Radek <“6479266+rbrus@users.noreply.github.com”> Date: Sun, 18 Oct 2026 09:21:53 +0200 Subject: [PATCH 05/26] added dynamic wifi to: lilygo t-beam supreme sx1262 ble companion --- variants/lilygo_tbeam_supreme_SX1262/platformio.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini index af4a8eafa..18a828025 100644 --- a/variants/lilygo_tbeam_supreme_SX1262/platformio.ini +++ b/variants/lilygo_tbeam_supreme_SX1262/platformio.ini @@ -131,11 +131,13 @@ build_flags = -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 -D OFFLINE_QUEUE_SIZE=256 + -D ENABLE_WIFI_INTERFACE ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=8 ; -D MESH_DEBUG=1 build_src_filter = ${T_Beam_S3_Supreme_SX1262.build_src_filter} + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> From 060fd2193d21b2b36e0eeb9460fe75e68c5fd623 Mon Sep 17 00:00:00 2001 From: rado <6479266+rbrus@users.noreply.github.com> Date: Tue, 20 Oct 2026 05:56:02 +0200 Subject: [PATCH 06/26] Skip empty anon contact slots in peer/contact lookups (prevent null-key ECDH sink) --- src/helpers/BaseChatMesh.cpp | 10 +- src/helpers/ContactInfo.h | 6 + .../test_empty_contact_slots.cpp | 230 ++++++++++++++++++ 3 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 test/test_empty_contact_slots/test_empty_contact_slots.cpp diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 28592415c..2c9ad7453 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -128,6 +128,7 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, ContactInfo* from = NULL; for (int i = 0; i < num_contacts; i++) { + if (contacts[i].isEmptySlot()) continue; if (id.matches(contacts[i].id)) { // is from one of our contacts from = &contacts[i]; if (timestamp <= from->last_advert_timestamp) { // check for replay attacks!! @@ -203,6 +204,11 @@ void BaseChatMesh::onAdvertRecv(mesh::Packet* packet, const mesh::Identity& id, int BaseChatMesh::searchPeersByHash(const uint8_t* hash) { int n = 0; for (int i = 0; i < num_contacts && n < MAX_SEARCH_RESULTS; i++) { + // Skip empty/unconfigured anon slots. An empty slot has an all-zero pub_key + // (an order-4 Edwards25519 point whose clamped X25519 key exchange yields an + // all-zero shared secret for any private key) and would otherwise match + // src_hash == 0x00 as a null-key peer sink while consuming search slots. + if (contacts[i].isEmptySlot()) continue; if (contacts[i].id.isHashMatch(hash)) { matching_peer_indexes[n++] = i; // store the INDEXES of matching contacts (for subsequent 'peer' methods) } @@ -866,6 +872,7 @@ ContactInfo* BaseChatMesh::searchContactsByPrefix(const char* name_prefix) { int len = strlen(name_prefix); for (int i = 0; i < num_contacts; i++) { auto c = &contacts[i]; + if (c->isEmptySlot() || c->name[0] == 0) continue; if (memcmp(c->name, name_prefix, len) == 0) return c; } return NULL; // not found @@ -874,6 +881,7 @@ ContactInfo* BaseChatMesh::searchContactsByPrefix(const char* name_prefix) { ContactInfo* BaseChatMesh::lookupContactByPubKey(const uint8_t* pub_key, int prefix_len) { for (int i = 0; i < num_contacts; i++) { auto c = &contacts[i]; + if (c->isEmptySlot()) continue; if (memcmp(c->id.pub_key, pub_key, prefix_len) == 0) return c; } return NULL; // not found @@ -891,7 +899,7 @@ bool BaseChatMesh::addContact(const ContactInfo& contact) { bool BaseChatMesh::removeContact(ContactInfo& contact) { int idx = 0; - while (idx < num_contacts && !contacts[idx].id.matches(contact.id)) { + while (idx < num_contacts && (contacts[idx].isEmptySlot() || !contacts[idx].id.matches(contact.id))) { idx++; } if (idx >= num_contacts) return false; // not found diff --git a/src/helpers/ContactInfo.h b/src/helpers/ContactInfo.h index 5a156c829..a401117e5 100644 --- a/src/helpers/ContactInfo.h +++ b/src/helpers/ContactInfo.h @@ -31,6 +31,12 @@ struct ContactInfo { bool isTelemLocAllowed() const { return flags & 0x04; } bool isTelemEnvAllowed() const { return flags & 0x08; } bool isRemoteCLIAllowed() const { return flags & 0x10; } + bool isEmptySlot() const { + for (int i = 0; i < PUB_KEY_SIZE; i++) { + if (id.pub_key[i] != 0) return false; + } + return true; + } private: mutable uint8_t shared_secret[PUB_KEY_SIZE]; diff --git a/test/test_empty_contact_slots/test_empty_contact_slots.cpp b/test/test_empty_contact_slots/test_empty_contact_slots.cpp new file mode 100644 index 000000000..cb4d5912f --- /dev/null +++ b/test/test_empty_contact_slots/test_empty_contact_slots.cpp @@ -0,0 +1,230 @@ +#include +#include +#include + +#include "Dispatcher.cpp" +#include "Mesh.cpp" +#include "helpers/AdvertDataHelpers.cpp" +#include "helpers/BaseChatMesh.cpp" + +void StrHelper::strncpy(char* dest, const char* src, size_t buf_sz) { + if (buf_sz == 0) return; + std::strncpy(dest, src, buf_sz - 1); + dest[buf_sz - 1] = 0; +} + +namespace mesh { + +Identity::Identity() { + memset(pub_key, 0, sizeof(pub_key)); +} + +bool Identity::verify(const uint8_t*, const uint8_t*, int) const { + return true; +} + +LocalIdentity::LocalIdentity() : Identity() { + memset(prv_key, 0, sizeof(prv_key)); +} + +void LocalIdentity::sign(uint8_t* sig, const uint8_t*, int) const { + memset(sig, 0, SIGNATURE_SIZE); +} + +void LocalIdentity::calcSharedSecret(uint8_t* secret, const uint8_t* other_pub_key) const { + ed25519_key_exchange(secret, other_pub_key, prv_key); +} + +} // namespace mesh + +namespace { + +class DummyRadio : public mesh::Radio { +public: + int recvRaw(uint8_t*, int) override { return 0; } + uint32_t getEstAirtimeFor(int) override { return 10; } + float packetScore(float, int) override { return 1.0f; } + bool startSendRaw(const uint8_t*, int) override { return true; } + bool isSendComplete() override { return true; } + void onSendFinished() override {} + bool isInRecvMode() const override { return true; } +}; + +class DummyMillisecondClock : public mesh::MillisecondClock { +public: + unsigned long getMillis() override { return 1000; } +}; + +class DummyRNG : public mesh::RNG { +public: + void random(uint8_t* dest, size_t len) override { memset(dest, 0x42, len); } +}; + +class DummyRTCClock : public mesh::RTCClock { + uint32_t _now = 1700000000; +public: + uint32_t getCurrentTime() override { return _now; } + void setCurrentTime(uint32_t time) override { _now = time; } +}; + +class DummyPacketManager : public mesh::PacketManager { +public: + mesh::Packet* allocNew() override { return nullptr; } + void free(mesh::Packet*) override {} + void queueOutbound(mesh::Packet*, uint8_t, uint32_t) override {} + mesh::Packet* getNextOutbound(uint32_t) override { return nullptr; } + int getOutboundCount(uint32_t) const override { return 0; } + int getOutboundTotal() const override { return 0; } + int getFreeCount() const override { return 16; } + mesh::Packet* getOutboundByIdx(int) override { return nullptr; } + mesh::Packet* removeOutboundByIdx(int) override { return nullptr; } + void queueInbound(mesh::Packet*, uint32_t) override {} + mesh::Packet* getNextInbound(uint32_t) override { return nullptr; } +}; + +class DummyMeshTables : public mesh::MeshTables { +public: + bool wasSeen(const mesh::Packet*) override { return false; } + void markSeen(const mesh::Packet*) override {} + void clear(const mesh::Packet*) override {} +}; + +class TestChatMesh : public BaseChatMesh { +public: + int messages_received = 0; + + TestChatMesh(mesh::Radio& radio, mesh::MillisecondClock& ms, mesh::RNG& rng, + mesh::RTCClock& rtc, mesh::PacketManager& mgr, mesh::MeshTables& tables) + : BaseChatMesh(radio, ms, rng, rtc, mgr, tables) {} + + void onDiscoveredContact(ContactInfo&, bool, uint8_t, const uint8_t*) override {} + ContactInfo* processAck(const uint8_t*) override { return nullptr; } + void onContactPathUpdated(const ContactInfo&) override {} + void onMessageRecv(const ContactInfo&, mesh::Packet*, uint32_t, const char*) override { + messages_received++; + } + void onCommandDataRecv(const ContactInfo&, mesh::Packet*, uint32_t, const char*) override {} + void onCLICommandRecv(const ContactInfo&, mesh::Packet*, uint32_t, const char*, char*) override {} + void onSignedMessageRecv(const ContactInfo&, mesh::Packet*, uint32_t, const uint8_t*, const char*) override {} + void onChannelMessageRecv(const mesh::GroupChannel&, mesh::Packet*, uint32_t, const char*) override {} + uint8_t onContactRequest(const ContactInfo&, uint32_t, const uint8_t*, uint8_t, uint8_t*) override { return 0; } + void onContactResponse(const ContactInfo&, const uint8_t*, uint8_t) override {} + uint32_t calcFloodTimeoutMillisFor(uint32_t) const override { return 1000; } + uint32_t calcDirectTimeoutMillisFor(uint32_t, uint8_t) const override { return 1000; } + void onSendTimeout() override {} + + using BaseChatMesh::resetContacts; + using BaseChatMesh::searchPeersByHash; + using BaseChatMesh::getPeerSharedSecret; + using mesh::Mesh::onRecvPacket; +}; + +struct MeshFixture { + DummyRadio radio; + DummyMillisecondClock ms; + DummyRNG rng; + DummyRTCClock rtc; + DummyPacketManager mgr; + DummyMeshTables tables; + TestChatMesh chat{radio, ms, rng, rtc, mgr, tables}; +}; + +} // namespace + +TEST(EmptyContactSlots, ZeroPubKeyProducesAllZeroSharedSecretForAnyPrivateKey) { + ContactInfo empty_slot; + memset(&empty_slot, 0, sizeof(empty_slot)); + EXPECT_TRUE(empty_slot.isEmptySlot()); + + uint8_t raw_prv[PRV_KEY_SIZE]; + for (int i = 0; i < PRV_KEY_SIZE; i++) { + raw_prv[i] = static_cast(0xA5 ^ (i * 13 + 7)); + } + uint8_t secret[PUB_KEY_SIZE]; + ed25519_key_exchange(secret, empty_slot.id.pub_key, raw_prv); + + uint8_t expected_zeroes[PUB_KEY_SIZE] = { 0 }; + EXPECT_EQ(0, memcmp(secret, expected_zeroes, PUB_KEY_SIZE)); +} + +TEST(EmptyContactSlots, NullKeyPeerPacketIsRejectedWhenAnonSlotsAreEmpty) { + MeshFixture fx; + fx.chat.self_id.pub_key[0] = 0x42; + + // Craft a PAYLOAD_TYPE_TXT_MSG addressed to self_id (dest_hash=0x42) with src_hash=0x00, + // encrypted and MAC'd under the all-zero 32-byte key that an empty ContactInfo slot derives. + uint8_t zero_secret[PUB_KEY_SIZE] = { 0 }; + uint8_t plaintext[16] = { 0 }; + uint32_t ts = 1700000001; + memcpy(plaintext, &ts, 4); + plaintext[4] = (TXT_TYPE_PLAIN << 2); + strcpy(reinterpret_cast(&plaintext[5]), "forged"); + + mesh::Packet pkt; + pkt.header = ROUTE_TYPE_FLOOD | (PAYLOAD_TYPE_TXT_MSG << PH_TYPE_SHIFT); + pkt.path_len = 0; + pkt.payload[0] = 0x42; // dest_hash (matches fx.chat.self_id) + pkt.payload[1] = 0x00; // src_hash (matches all-zero pub_key if empty slots are not skipped) + int enc_len = mesh::Utils::encryptThenMAC(zero_secret, &pkt.payload[2], plaintext, 12); + pkt.payload_len = static_cast(2 + enc_len); + + fx.chat.onRecvPacket(&pkt); + EXPECT_EQ(0, fx.chat.messages_received); +} + +TEST(EmptyContactSlots, SearchPeersByHashSkipsEmptyAnonSlotsAndFindsRealZeroPrefixPeer) { + MeshFixture fx; + const uint8_t zero_hash = 0x00; + + // Freshly reset contacts has MAX_ANON_CONTACTS (8) zeroed transient slots. + EXPECT_EQ(0, fx.chat.searchPeersByHash(&zero_hash)); + + // Add a legitimate permanent contact whose public key starts with 0x00. + ContactInfo peer; + memset(&peer, 0, sizeof(peer)); + peer.type = ADV_TYPE_CHAT; + peer.lastmod = 1700000100; + strcpy(peer.name, "Alice"); + peer.id.pub_key[0] = 0x00; + for (int i = 1; i < PUB_KEY_SIZE; i++) { + peer.id.pub_key[i] = static_cast(i); + } + ASSERT_TRUE(fx.chat.addContact(peer)); + + // Only the populated contact at index MAX_ANON_CONTACTS should match. + EXPECT_EQ(1, fx.chat.searchPeersByHash(&zero_hash)); + + // Lookup by pubkey and name prefix must also skip the empty transient slots. + uint8_t zero_prefix[6] = { 0 }; + EXPECT_EQ(nullptr, fx.chat.lookupContactByPubKey(zero_prefix, sizeof(zero_prefix))); + + ContactInfo* found = fx.chat.lookupContactByPubKey(peer.id.pub_key, 6); + ASSERT_NE(nullptr, found); + EXPECT_STREQ("Alice", found->name); + + ContactInfo* by_name = fx.chat.searchContactsByPrefix(""); + ASSERT_NE(nullptr, by_name); + EXPECT_STREQ("Alice", by_name->name); +} + +TEST(EmptyContactSlots, PopulatedTransientAnonSlotStillMatchesEvenWhenLastmodIsZero) { + MeshFixture fx; + const uint8_t zero_hash = 0x00; + + ContactInfo anon; + memset(&anon, 0, sizeof(anon)); + anon.type = ADV_TYPE_NONE; + anon.lastmod = 0; // e.g. RTC not yet initialised on nRF52 + anon.id.pub_key[0] = 0x00; + anon.id.pub_key[1] = 0x7F; + ASSERT_TRUE(fx.chat.addContact(anon)); + + EXPECT_FALSE(anon.isEmptySlot()); + EXPECT_EQ(1, fx.chat.searchPeersByHash(&zero_hash)); + EXPECT_NE(nullptr, fx.chat.lookupContactByPubKey(anon.id.pub_key, PUB_KEY_SIZE)); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} From 87c76a8b06a9113fe16a40ca9961bf9aaf99da9d Mon Sep 17 00:00:00 2001 From: liamcottle Date: Wed, 21 Oct 2026 15:41:11 +1300 Subject: [PATCH 07/26] implement new auto detect display for sh1106 and ssd1306 --- src/helpers/ui/AutoDetectDisplay.cpp | 45 ++++++++++++++++++++++++++++ src/helpers/ui/AutoDetectDisplay.h | 32 ++++++++++++++++++++ src/helpers/ui/SH1106Display.cpp | 18 +++++------ variants/rak4631/platformio.ini | 23 +++++++------- variants/rak4631/target.h | 2 +- 5 files changed, 100 insertions(+), 20 deletions(-) create mode 100644 src/helpers/ui/AutoDetectDisplay.cpp create mode 100644 src/helpers/ui/AutoDetectDisplay.h diff --git a/src/helpers/ui/AutoDetectDisplay.cpp b/src/helpers/ui/AutoDetectDisplay.cpp new file mode 100644 index 000000000..e3537ab25 --- /dev/null +++ b/src/helpers/ui/AutoDetectDisplay.cpp @@ -0,0 +1,45 @@ +#include "AutoDetectDisplay.h" +#include + +int AutoDetectDisplay::getDisplayStatus(TwoWire& wire, uint8_t addr) { + int status = -1; + for(int i = 0; i < 4; i++){ + + // request display status + wire.beginTransmission(addr); + wire.write((uint8_t)0x00); + + // ensure response received + if (wire.endTransmission() != 0) return -1; // expect success + if (wire.requestFrom((int)addr, 1) == 0) return -1; // expect at least 1 byte + + // ensure two consecutive reads are the same + int previous = status; + status = wire.read() & 0x0F; + if(status == previous){ + break; + } + + } + return status; +} + +bool AutoDetectDisplay::begin() { + + // request status from display address + int status = getDisplayStatus(Wire, DISPLAY_ADDRESS); + MESH_DEBUG_PRINTLN("AutoDetectDisplay: status=0x%02X", status); + + // detect SH1106 + if(status == 0x00 || status == 0x08){ + MESH_DEBUG_PRINTLN("AutoDetectDisplay: SH1106 detected"); + _impl = &_sh1106; + return _sh1106.begin(); + } + + // fallback to SSD1306 + MESH_DEBUG_PRINTLN("AutoDetectDisplay: falling back to SSD1306"); + _impl = &_ssd1306; + return _ssd1306.begin(); + +} diff --git a/src/helpers/ui/AutoDetectDisplay.h b/src/helpers/ui/AutoDetectDisplay.h new file mode 100644 index 000000000..7208d4f59 --- /dev/null +++ b/src/helpers/ui/AutoDetectDisplay.h @@ -0,0 +1,32 @@ +#pragma once + +#include "DisplayDriver.h" +#include "SSD1306Display.h" +#include "SH1106Display.h" + +// Auto detects SH1106 display at runtime, otherwise falls back to using SSD1306 +class AutoDetectDisplay : public DisplayDriver { + DisplayDriver* _impl; + SSD1306Display _ssd1306; + SH1106Display _sh1106; + + int getDisplayStatus(TwoWire& wire, uint8_t addr); +public: + AutoDetectDisplay() : DisplayDriver(128, 64), _impl(&_ssd1306) { } + bool begin(); + + bool isOn() override { return _impl->isOn(); } + void turnOn() override { _impl->turnOn(); } + void turnOff() override { _impl->turnOff(); } + void clear() override { _impl->clear(); } + void startFrame(ColorVal bkg = UIColor::window_bkg) override { _impl->startFrame(bkg); } + void setTextSize(int sz) override { _impl->setTextSize(sz); } + void setColor(ColorVal c) override { _impl->setColor(c); } + void setCursor(int x, int y) override { _impl->setCursor(x, y); } + void print(const char* str) override { _impl->print(str); } + void fillRect(int x, int y, int w, int h) override { _impl->fillRect(x, y, w, h); } + void drawRect(int x, int y, int w, int h) override { _impl->drawRect(x, y, w, h); } + void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override { _impl->drawXbm(x, y, bits, w, h); } + uint16_t getTextWidth(const char* str) override { return _impl->getTextWidth(str); } + void endFrame() override { _impl->endFrame(); } +}; diff --git a/src/helpers/ui/SH1106Display.cpp b/src/helpers/ui/SH1106Display.cpp index 8b91d857c..e290b3556 100644 --- a/src/helpers/ui/SH1106Display.cpp +++ b/src/helpers/ui/SH1106Display.cpp @@ -10,15 +10,15 @@ bool SH1106Display::i2c_probe(TwoWire &wire, uint8_t addr) } // Color scheme -ColorVal UIColor::window_bkg = SH110X_BLACK; -ColorVal UIColor::title_bkg = SH110X_BLACK; -ColorVal UIColor::title_txt = SH110X_WHITE; -ColorVal UIColor::primary_txt = SH110X_WHITE; -ColorVal UIColor::secondary_txt = SH110X_WHITE; -ColorVal UIColor::warning_txt = SH110X_WHITE; -ColorVal UIColor::popup_bkg = SH110X_BLACK; -ColorVal UIColor::popup_txt = SH110X_WHITE; -ColorVal UIColor::corp_blue = SH110X_WHITE; +__attribute__((weak)) ColorVal UIColor::window_bkg = SH110X_BLACK; +__attribute__((weak)) ColorVal UIColor::title_bkg = SH110X_BLACK; +__attribute__((weak)) ColorVal UIColor::title_txt = SH110X_WHITE; +__attribute__((weak)) ColorVal UIColor::primary_txt = SH110X_WHITE; +__attribute__((weak)) ColorVal UIColor::secondary_txt = SH110X_WHITE; +__attribute__((weak)) ColorVal UIColor::warning_txt = SH110X_WHITE; +__attribute__((weak)) ColorVal UIColor::popup_bkg = SH110X_BLACK; +__attribute__((weak)) ColorVal UIColor::popup_txt = SH110X_WHITE; +__attribute__((weak)) ColorVal UIColor::corp_blue = SH110X_WHITE; bool SH1106Display::begin() { diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index d2781d9fa..4c13b0d74 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -28,11 +28,14 @@ build_src_filter = ${nrf52_base.build_src_filter} +<../variants/rak4631> + + + + + + + lib_deps = ${nrf52_base.lib_deps} ${sensor_base.lib_deps} adafruit/Adafruit SSD1306 @ ^2.5.13 + adafruit/Adafruit SH110X @ ^2.1.13 sparkfun/SparkFun u-blox GNSS Arduino Library@^2.2.27 boschsensortec/BSEC Software Library @ ^1.8.1492 @@ -40,7 +43,7 @@ lib_deps = extends = rak4631 build_flags = ${rak4631.build_flags} - -D DISPLAY_CLASS=SSD1306Display + -D DISPLAY_CLASS=AutoDetectDisplay -D ADVERT_NAME='"RAK4631 Repeater"' -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 @@ -56,7 +59,7 @@ build_src_filter = ${rak4631.build_src_filter} extends = rak4631 build_flags = ${rak4631.build_flags} - -D DISPLAY_CLASS=SSD1306Display + -D DISPLAY_CLASS=AutoDetectDisplay -D ADVERT_NAME='"RAK4631 Repeater"' -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 @@ -75,7 +78,7 @@ lib_deps = extends = rak4631 build_flags = ${rak4631.build_flags} - -D DISPLAY_CLASS=SSD1306Display + -D DISPLAY_CLASS=AutoDetectDisplay -D ADVERT_NAME='"RS232 Bridge"' -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 @@ -98,7 +101,7 @@ build_src_filter = ${rak4631.build_src_filter} extends = rak4631 build_flags = ${rak4631.build_flags} - -D DISPLAY_CLASS=SSD1306Display + -D DISPLAY_CLASS=AutoDetectDisplay -D ADVERT_NAME='"RS232 Bridge"' -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 @@ -121,7 +124,7 @@ build_src_filter = ${rak4631.build_src_filter} extends = rak4631 build_flags = ${rak4631.build_flags} - -D DISPLAY_CLASS=SSD1306Display + -D DISPLAY_CLASS=AutoDetectDisplay -D ADVERT_NAME='"Test Room"' -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 @@ -137,7 +140,7 @@ build_src_filter = ${rak4631.build_src_filter} extends = rak4631 build_flags = ${rak4631.build_flags} - -D DISPLAY_CLASS=SSD1306Display + -D DISPLAY_CLASS=AutoDetectDisplay -D ADVERT_NAME='"RAK4631 Room Server"' -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 @@ -161,7 +164,7 @@ build_flags = -I examples/companion_radio/ui-new -D PIN_USER_BTN=9 -D PIN_USER_BTN_ANA=31 - -D DISPLAY_CLASS=SSD1306Display + -D DISPLAY_CLASS=AutoDetectDisplay -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D ENABLE_USB_INTERFACE @@ -186,7 +189,7 @@ build_flags = -I examples/companion_radio/ui-new -D PIN_USER_BTN=9 -D PIN_USER_BTN_ANA=31 - -D DISPLAY_CLASS=SSD1306Display + -D DISPLAY_CLASS=AutoDetectDisplay -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D ETHERNET_ENABLED=1 @@ -215,7 +218,7 @@ build_flags = -I examples/companion_radio/ui-new -D PIN_USER_BTN=9 -D PIN_USER_BTN_ANA=31 - -D DISPLAY_CLASS=SSD1306Display + -D DISPLAY_CLASS=AutoDetectDisplay -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 @@ -251,7 +254,7 @@ lib_deps = extends = rak4631 build_flags = ${rak4631.build_flags} - -D DISPLAY_CLASS=SSD1306Display + -D DISPLAY_CLASS=AutoDetectDisplay -D ADVERT_NAME='"RAK4631 Sensor"' -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 diff --git a/variants/rak4631/target.h b/variants/rak4631/target.h index 1529f9042..c04cd4e48 100644 --- a/variants/rak4631/target.h +++ b/variants/rak4631/target.h @@ -9,7 +9,7 @@ #include #ifdef DISPLAY_CLASS - #include + #include extern DISPLAY_CLASS display; #include extern MomentaryButton user_btn; From a8e8d01a3c8dd951c85f4c891550c9173a3a342f Mon Sep 17 00:00:00 2001 From: Confituurke <9595028+Confituurke@users.noreply.github.com> Date: Wed, 21 Oct 2026 11:24:34 +0200 Subject: [PATCH 08/26] Use two decimals only for exact quarter-hour offsets --- examples/companion_radio/MyMesh.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 2ea539dbc..cc6340bb1 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2192,7 +2192,8 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* } if (strcmp(command, "get tz.offset") == 0) { - sprintf(reply, ((int)(_prefs.tz_offset * 4) % 2) ? "> %.2f" : "> %.1f", _prefs.tz_offset); + float q = _prefs.tz_offset * 4; + sprintf(reply, (q == (int)q && ((int)q & 1)) ? "> %.2f" : "> %.1f", _prefs.tz_offset); return true; } if (memcmp(command, "set tz.offset ", 14) == 0) { From a31e2c92df6c9e870ce3b0bbb5d3defa37a64490 Mon Sep 17 00:00:00 2001 From: Florent Date: Wed, 21 Oct 2026 14:08:56 -0400 Subject: [PATCH 09/26] refactoring eink devices behaviour --- examples/companion_radio/ui-new/UITask.cpp | 44 ++++++++++++++++------ examples/companion_radio/ui-new/UITask.h | 3 +- src/helpers/ui/DisplayDriver.h | 1 + src/helpers/ui/GxEPDDisplay.cpp | 6 ++- src/helpers/ui/GxEPDDisplay.h | 1 + variants/lilygo_techo/platformio.ini | 8 ++-- variants/lilygo_techo/target.cpp | 1 + variants/lilygo_techo/target.h | 1 + variants/thinknode_m1/platformio.ini | 7 ++++ variants/thinknode_m1/target.cpp | 1 + variants/thinknode_m1/target.h | 1 + variants/thinknode_m5/platformio.ini | 7 +++- variants/thinknode_m5/target.cpp | 1 + variants/thinknode_m5/target.h | 1 + variants/thinknode_m8/platformio.ini | 11 +++--- variants/thinknode_m8/target.cpp | 1 + variants/thinknode_m8/target.h | 1 + 17 files changed, 71 insertions(+), 25 deletions(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index d48647b3d..de9e8f58a 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -197,11 +197,10 @@ class HomeScreen : public UIScreen { sensors_nb ++; } sensors_scroll = sensors_nb > UI_RECENT_LIST_SIZE; -#if AUTO_OFF_MILLIS > 0 - next_sensors_refresh = millis() + 5000; // refresh sensor values every 5 sec -#else - next_sensors_refresh = millis() + 60000; // refresh sensor values every 1 min -#endif + if (display.isEink()) // preserve eink displays + next_sensors_refresh = millis() + 60000; // refresh sensor values every 1 min + else + next_sensors_refresh = millis() + 5000; // refresh sensor values every 5 sec } } @@ -703,11 +702,7 @@ public: display.translateUTF8ToBlocks(filtered_msg, p->msg, sizeof(filtered_msg)); display.printWordWrap(filtered_msg, display.width()); -#if AUTO_OFF_MILLIS==0 // probably e-ink - return 10000; // 10 s -#else - return 1000; // next render after 1000 ms -#endif + return display.isEink() ? 10000 : 1000; // 10 s for eink } bool handleInput(char c) override { @@ -739,6 +734,9 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no #if defined(PIN_USER_BTN_ANA) analog_btn.begin(); #endif +#if defined(BACKLIGHT_BTN) + backlight_btn.begin(); +#endif _node_prefs = node_prefs; @@ -1002,13 +1000,30 @@ void UITask::loop() { #endif #if defined(BACKLIGHT_BTN) if (millis() > next_backlight_btn_check) { - bool touch_state = digitalRead(PIN_BUTTON2); + if (backlight_btn.isPressed()) { + if (!_display->isOn()) { + _display->turnOn(); + _next_refresh = 0; + } + _auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer + backlight_btn_pressed_at = millis(); // still pressed + } + bool touch_state = (millis() - backlight_btn_pressed_at > 5000); + // Backlight button events + int bev = backlight_btn.check(); + if (bev == BUTTON_EVENT_DOUBLE_CLICK) { + _display->forceFullRefresh(); + } else if (bev == BUTTON_EVENT_TRIPLE_CLICK) { + _display->blankScreen(); + _display->turnOff(); + backlight_btn_pressed_at = 0; + } #if defined(DISP_BACKLIGHT) digitalWrite(DISP_BACKLIGHT, !touch_state); #elif defined(EXP_PIN_BACKLIGHT) expander.digitalWrite(EXP_PIN_BACKLIGHT, !touch_state); #endif - next_backlight_btn_check = millis() + 300; + next_backlight_btn_check = millis() + 100; } #endif @@ -1056,6 +1071,11 @@ void UITask::loop() { } #endif if (millis() > _auto_off) { + #ifdef EINK_SCREENOFF_BLANK + if (curr != msg_preview) { // don't blank if messages are waiting + display.blankScreen(); + } + #endif _display->turnOff(); } #endif diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 361b04d01..4d44bfe1a 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -61,7 +61,8 @@ class UITask : public AbstractUITask { unsigned long _alert_expiry; int _msgcount; unsigned long ui_started_at, next_batt_chck; - int next_backlight_btn_check = 0; + unsigned long next_backlight_btn_check = 0; + unsigned long backlight_btn_pressed_at = 0; #ifdef PIN_STATUS_LED int led_state = 0; int next_led_change = 0; diff --git a/src/helpers/ui/DisplayDriver.h b/src/helpers/ui/DisplayDriver.h index 3e9e2dde8..cbef3f98f 100644 --- a/src/helpers/ui/DisplayDriver.h +++ b/src/helpers/ui/DisplayDriver.h @@ -27,6 +27,7 @@ public: virtual void turnOn() = 0; virtual void turnOff() = 0; virtual void clear() = 0; + virtual void blankScreen() {} // to cleanly erase screen before turning off virtual void startFrame(ColorVal bkg = UIColor::window_bkg) = 0; virtual void setTextSize(int sz) = 0; virtual void setColor(ColorVal c) = 0; diff --git a/src/helpers/ui/GxEPDDisplay.cpp b/src/helpers/ui/GxEPDDisplay.cpp index 332157eb8..21b00afc5 100644 --- a/src/helpers/ui/GxEPDDisplay.cpp +++ b/src/helpers/ui/GxEPDDisplay.cpp @@ -82,13 +82,17 @@ void GxEPDDisplay::turnOff() { expander.digitalWrite(EXP_PIN_BACKLIGHT, LOW); #endif _isOn = false; + last_display_crc_value=0; + display.hibernate(); +} + +void GxEPDDisplay::blankScreen() { // do full refresh before powering off to clear screen // no full refresh needed at wakeup display.clearScreen(0xFF); // Clears microcontroller side RAM display.writeScreenBuffer(0xFF); // Forces 0xFF (White) into the display controller's history registers resetPartialRefreshCounter(); last_display_crc_value=0; - display.hibernate(); } void GxEPDDisplay::clear() { diff --git a/src/helpers/ui/GxEPDDisplay.h b/src/helpers/ui/GxEPDDisplay.h index 8a37b72cc..99b25d150 100644 --- a/src/helpers/ui/GxEPDDisplay.h +++ b/src/helpers/ui/GxEPDDisplay.h @@ -61,6 +61,7 @@ public: void turnOn() override; void turnOff() override; void clear() override; + void blankScreen() override; void startFrame(ColorVal bkg = UIColor::window_bkg) override; void setTextSize(int sz) override; void setColor(ColorVal c) override; diff --git a/variants/lilygo_techo/platformio.ini b/variants/lilygo_techo/platformio.ini index 81da2e48c..140406223 100644 --- a/variants/lilygo_techo/platformio.ini +++ b/variants/lilygo_techo/platformio.ini @@ -32,8 +32,12 @@ build_flags = ${nrf52_base.build_flags} -D PIN_GPS_RESET_ACTIVE=LOW -D TELEM_BME280_ADDRESS=0x77 -D DISPLAY_CLASS=GxEPDDisplay + -D EINK_SCREENOFF_BLANK + -D UI_RECENT_LIST_SIZE=9 + -D UI_MSG_PREVIEW_SIZE=165 + -D EINK_MAX_PARTIAL_REFRESH=60 + -D AUTO_OFF_MILLIS=1800000 -D BACKLIGHT_BTN=PIN_BUTTON2 - -D AUTO_OFF_MILLIS=0 build_src_filter = ${nrf52_base.build_src_filter} + + @@ -91,7 +95,6 @@ build_flags = -D BLE_PIN_CODE=123456 ; -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=256 - -D UI_RECENT_LIST_SIZE=9 -D UI_SENSORS_PAGE=1 -D UI_GPS_PAGE=1 ; -D MESH_PACKET_LOGGING=1 @@ -116,7 +119,6 @@ build_flags = -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 - -D UI_RECENT_LIST_SIZE=9 -D UI_SENSORS_PAGE=1 -D AUTO_SHUTDOWN_MILLIVOLTS=3300 -D QSPIFLASH=1 diff --git a/variants/lilygo_techo/target.cpp b/variants/lilygo_techo/target.cpp index 278fc76da..12b40d7ee 100644 --- a/variants/lilygo_techo/target.cpp +++ b/variants/lilygo_techo/target.cpp @@ -23,6 +23,7 @@ EnvironmentSensorManager sensors = EnvironmentSensorManager(); #ifdef DISPLAY_CLASS DISPLAY_CLASS display; MomentaryButton user_btn(PIN_USER_BTN, 1000, true); + MomentaryButton backlight_btn(BACKLIGHT_BTN, 1000, true); #endif bool radio_init() { diff --git a/variants/lilygo_techo/target.h b/variants/lilygo_techo/target.h index 2b8a1783f..a1668f031 100644 --- a/variants/lilygo_techo/target.h +++ b/variants/lilygo_techo/target.h @@ -22,6 +22,7 @@ extern EnvironmentSensorManager sensors; #ifdef DISPLAY_CLASS extern DISPLAY_CLASS display; extern MomentaryButton user_btn; + extern MomentaryButton backlight_btn; #endif bool radio_init(); diff --git a/variants/thinknode_m1/platformio.ini b/variants/thinknode_m1/platformio.ini index 89c48403b..2e1aa48f4 100644 --- a/variants/thinknode_m1/platformio.ini +++ b/variants/thinknode_m1/platformio.ini @@ -24,6 +24,13 @@ build_flags = ${nrf52_base.build_flags} -D SX126X_RX_BOOSTED_GAIN=1 -D LORA_TX_POWER=22 -D P_LORA_TX_LED=13 + -D UI_RECENT_LIST_SIZE=9 + -D UI_MSG_PREVIEW_SIZE=165 + -D EINK_MAX_PARTIAL_REFRESH=60 + -D BACKLIGHT_BTN=PIN_BUTTON2 + -D AUTO_OFF_MILLIS=1800000 + -D UI_NO_HIBERNATE + -D DISABLE_DIAGNOSTIC_OUTPUT build_src_filter = ${nrf52_base.build_src_filter} + + diff --git a/variants/thinknode_m1/target.cpp b/variants/thinknode_m1/target.cpp index 69306fc0e..c430cee00 100644 --- a/variants/thinknode_m1/target.cpp +++ b/variants/thinknode_m1/target.cpp @@ -17,6 +17,7 @@ ThinkNodeM1SensorManager sensors = ThinkNodeM1SensorManager(nmea); #ifdef DISPLAY_CLASS DISPLAY_CLASS display; MomentaryButton user_btn(PIN_USER_BTN, 1000, true); + MomentaryButton backlight_btn(BACKLIGHT_BTN, 1000, true); #endif bool radio_init() { diff --git a/variants/thinknode_m1/target.h b/variants/thinknode_m1/target.h index ec1297bef..d2d6aed1d 100644 --- a/variants/thinknode_m1/target.h +++ b/variants/thinknode_m1/target.h @@ -40,6 +40,7 @@ extern ThinkNodeM1SensorManager sensors; #ifdef DISPLAY_CLASS extern DISPLAY_CLASS display; extern MomentaryButton user_btn; + extern MomentaryButton backlight_btn; #endif bool radio_init(); diff --git a/variants/thinknode_m5/platformio.ini b/variants/thinknode_m5/platformio.ini index a3f348b33..ed187dd25 100644 --- a/variants/thinknode_m5/platformio.ini +++ b/variants/thinknode_m5/platformio.ini @@ -26,8 +26,12 @@ build_flags = ${esp32_base.build_flags} -D EINK_SCALE_Y=1.5625f -D EINK_X_OFFSET=0 -D EINK_Y_OFFSET=10 + -D UI_RECENT_LIST_SIZE=9 + -D UI_MSG_PREVIEW_SIZE=165 + -D EINK_MAX_PARTIAL_REFRESH=60 -D BACKLIGHT_BTN=PIN_BUTTON2 - -D AUTO_OFF_MILLIS=0 + -D AUTO_OFF_MILLIS=1800000 + -D UI_NO_HIBERNATE -D DISABLE_DIAGNOSTIC_OUTPUT -D SX126X_DIO2_AS_RF_SWITCH=true -D SX126X_DIO3_TCXO_VOLTAGE=3.3 @@ -150,7 +154,6 @@ build_flags = -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 -D OFFLINE_QUEUE_SIZE=256 - -D UI_RECENT_LIST_SIZE=9 ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 ; -D GPS_NMEA_DEBUG diff --git a/variants/thinknode_m5/target.cpp b/variants/thinknode_m5/target.cpp index be534f67d..ec524df74 100644 --- a/variants/thinknode_m5/target.cpp +++ b/variants/thinknode_m5/target.cpp @@ -26,6 +26,7 @@ EnvironmentSensorManager sensors = EnvironmentSensorManager(); #ifdef DISPLAY_CLASS DISPLAY_CLASS display; MomentaryButton user_btn(PIN_USER_BTN, 1000, true); + MomentaryButton backlight_btn(BACKLIGHT_BTN, 1000, true); #endif bool radio_init() { diff --git a/variants/thinknode_m5/target.h b/variants/thinknode_m5/target.h index a613e3909..7cce1edbf 100644 --- a/variants/thinknode_m5/target.h +++ b/variants/thinknode_m5/target.h @@ -24,6 +24,7 @@ extern PCA9557 expander; #ifdef DISPLAY_CLASS extern DISPLAY_CLASS display; extern MomentaryButton user_btn; + extern MomentaryButton backlight_btn; #endif bool radio_init(); diff --git a/variants/thinknode_m8/platformio.ini b/variants/thinknode_m8/platformio.ini index db19e4466..2c07038f2 100644 --- a/variants/thinknode_m8/platformio.ini +++ b/variants/thinknode_m8/platformio.ini @@ -18,12 +18,16 @@ build_flags = ${nrf52_base.build_flags} -D P_LORA_SCLK=19 -D P_LORA_MISO=22 -D P_LORA_MOSI=20 - -D UI_SHOW_CLOCK=1 + -D DISPLAY_ROTATION=4 -D UI_HAS_ROTARY_INPUT=1 -D UI_HAS_NAV_INPUT=1 -D UI_RECENT_LIST_SIZE=9 -D UI_MSG_PREVIEW_SIZE=165 -D EINK_MAX_PARTIAL_REFRESH=60 + -D AUTO_OFF_MILLIS=1800000 + -D BACKLIGHT_BTN=PIN_BUTTON2 + -D UI_SHOW_CLOCK=1 + -D EINK_SCREENOFF_BLANK -D EINK_DISPLAY_MODEL=GxEPD2_154_D67 ; -D EINK_DISPLAY_MODEL=GxEPD2_154_GDEY0154D67 ; -D EINK_DISPLAY_MODEL=GxEPD2_150_BN @@ -92,10 +96,7 @@ build_flags = -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 # -D BLE_DEBUG_LOGGING=1 - -D DISPLAY_ROTATION=4 -D DISPLAY_CLASS=GxEPDDisplay - -D BACKLIGHT_BTN=PIN_BUTTON2 - -D AUTO_OFF_MILLIS=0 -D OFFLINE_QUEUE_SIZE=256 -D PIN_BUZZER=33 -D AUTO_SHUTDOWN_MILLIVOLTS=3300 @@ -133,8 +134,6 @@ build_flags = -D DISPLAY_ROTATION=4 -D QSPIFLASH=1 -D DISPLAY_CLASS=GxEPDDisplay - -D BACKLIGHT_BTN=PIN_BUTTON2 - -D AUTO_OFF_MILLIS=0 -D OFFLINE_QUEUE_SIZE=256 -D PIN_BUZZER=6 -D AUTO_SHUTDOWN_MILLIVOLTS=3300 diff --git a/variants/thinknode_m8/target.cpp b/variants/thinknode_m8/target.cpp index ac4ac100e..69f3cc526 100644 --- a/variants/thinknode_m8/target.cpp +++ b/variants/thinknode_m8/target.cpp @@ -23,6 +23,7 @@ EnvironmentSensorManager sensors = EnvironmentSensorManager(); DISPLAY_CLASS display; MomentaryButton user_btn(PIN_USER_BTN, 1000, true); RotaryInputGPIO rotary_input(PIN_BUTTON1_A, PIN_BUTTON1_B); + MomentaryButton backlight_btn(BACKLIGHT_BTN, 1000, true); #endif bool radio_init() { diff --git a/variants/thinknode_m8/target.h b/variants/thinknode_m8/target.h index c3402a01a..399285065 100644 --- a/variants/thinknode_m8/target.h +++ b/variants/thinknode_m8/target.h @@ -23,6 +23,7 @@ extern EnvironmentSensorManager sensors; #ifdef DISPLAY_CLASS extern DISPLAY_CLASS display; extern MomentaryButton user_btn; + extern MomentaryButton backlight_btn; extern RotaryInputGPIO rotary_input; #endif From 408d674ace41688bd53a3d1888cd397adaacba9c Mon Sep 17 00:00:00 2001 From: Florent Date: Wed, 21 Oct 2026 17:13:27 -0400 Subject: [PATCH 10/26] DisplayDriver: fixing alignment issue in vtable for nrf52 and epd display --- examples/companion_radio/ui-new/UITask.cpp | 6 +++++- src/helpers/ui/DisplayDriver.h | 3 ++- src/helpers/ui/GxEPDDisplay.cpp | 7 +++++-- src/helpers/ui/GxEPDDisplay.h | 3 ++- variants/lilygo_techo/platformio.ini | 2 +- variants/thinknode_m1/platformio.ini | 2 +- variants/thinknode_m5/platformio.ini | 2 +- variants/thinknode_m8/platformio.ini | 2 +- 8 files changed, 18 insertions(+), 9 deletions(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index de9e8f58a..6e987f875 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -955,7 +955,9 @@ void UITask::loop() { if (ev == BUTTON_EVENT_CLICK) { c = checkDisplayOn(KEY_ENTER); } else if (ev == BUTTON_EVENT_LONG_PRESS) { + display.blankScreen(); display.turnOff(); + display.powerOff(); } else if (ev == BUTTON_EVENT_DOUBLE_CLICK) { c = handleDoubleClick(KEY_SELECT); } else if (ev == BUTTON_EVENT_TRIPLE_CLICK) { @@ -1016,6 +1018,7 @@ void UITask::loop() { } else if (bev == BUTTON_EVENT_TRIPLE_CLICK) { _display->blankScreen(); _display->turnOff(); + _display->powerOff(); backlight_btn_pressed_at = 0; } #if defined(DISP_BACKLIGHT) @@ -1073,7 +1076,8 @@ void UITask::loop() { if (millis() > _auto_off) { #ifdef EINK_SCREENOFF_BLANK if (curr != msg_preview) { // don't blank if messages are waiting - display.blankScreen(); + _display->blankScreen(); + _display->powerOff(); } #endif _display->turnOff(); diff --git a/src/helpers/ui/DisplayDriver.h b/src/helpers/ui/DisplayDriver.h index cbef3f98f..58147aa0e 100644 --- a/src/helpers/ui/DisplayDriver.h +++ b/src/helpers/ui/DisplayDriver.h @@ -26,8 +26,9 @@ public: virtual void forceFullRefresh() {} // next refresh will be full for eink virtual void turnOn() = 0; virtual void turnOff() = 0; - virtual void clear() = 0; virtual void blankScreen() {} // to cleanly erase screen before turning off + virtual void powerOff() {} + virtual void clear() = 0; virtual void startFrame(ColorVal bkg = UIColor::window_bkg) = 0; virtual void setTextSize(int sz) = 0; virtual void setColor(ColorVal c) = 0; diff --git a/src/helpers/ui/GxEPDDisplay.cpp b/src/helpers/ui/GxEPDDisplay.cpp index 21b00afc5..ce09423e1 100644 --- a/src/helpers/ui/GxEPDDisplay.cpp +++ b/src/helpers/ui/GxEPDDisplay.cpp @@ -75,7 +75,11 @@ void GxEPDDisplay::turnOn() { } } -void GxEPDDisplay::turnOff() { +void GxEPDDisplay::powerOff() { + display.hibernate(); +} + +void GxEPDDisplay::turnOff() { // Only stops refresh #if defined(DISP_BACKLIGHT) && !defined(BACKLIGHT_BTN) digitalWrite(DISP_BACKLIGHT, LOW); #elif defined(EXP_PIN_BACKLIGHT) && !defined(BACKLIGHT_BTN) @@ -83,7 +87,6 @@ void GxEPDDisplay::turnOff() { #endif _isOn = false; last_display_crc_value=0; - display.hibernate(); } void GxEPDDisplay::blankScreen() { diff --git a/src/helpers/ui/GxEPDDisplay.h b/src/helpers/ui/GxEPDDisplay.h index 99b25d150..462744026 100644 --- a/src/helpers/ui/GxEPDDisplay.h +++ b/src/helpers/ui/GxEPDDisplay.h @@ -60,8 +60,9 @@ public: void resetPartialRefreshCounter() { _cycles_before_full_refresh = max_partial_refresh; }; void turnOn() override; void turnOff() override; - void clear() override; void blankScreen() override; + void powerOff() override; + void clear() override; void startFrame(ColorVal bkg = UIColor::window_bkg) override; void setTextSize(int sz) override; void setColor(ColorVal c) override; diff --git a/variants/lilygo_techo/platformio.ini b/variants/lilygo_techo/platformio.ini index 140406223..eb4a69e28 100644 --- a/variants/lilygo_techo/platformio.ini +++ b/variants/lilygo_techo/platformio.ini @@ -36,7 +36,7 @@ build_flags = ${nrf52_base.build_flags} -D UI_RECENT_LIST_SIZE=9 -D UI_MSG_PREVIEW_SIZE=165 -D EINK_MAX_PARTIAL_REFRESH=60 - -D AUTO_OFF_MILLIS=1800000 + -D AUTO_OFF_MILLIS=900000 -D BACKLIGHT_BTN=PIN_BUTTON2 build_src_filter = ${nrf52_base.build_src_filter} + diff --git a/variants/thinknode_m1/platformio.ini b/variants/thinknode_m1/platformio.ini index 2e1aa48f4..c775ae3fa 100644 --- a/variants/thinknode_m1/platformio.ini +++ b/variants/thinknode_m1/platformio.ini @@ -28,7 +28,7 @@ build_flags = ${nrf52_base.build_flags} -D UI_MSG_PREVIEW_SIZE=165 -D EINK_MAX_PARTIAL_REFRESH=60 -D BACKLIGHT_BTN=PIN_BUTTON2 - -D AUTO_OFF_MILLIS=1800000 + -D AUTO_OFF_MILLIS=900000 -D UI_NO_HIBERNATE -D DISABLE_DIAGNOSTIC_OUTPUT build_src_filter = ${nrf52_base.build_src_filter} diff --git a/variants/thinknode_m5/platformio.ini b/variants/thinknode_m5/platformio.ini index ed187dd25..1980575ec 100644 --- a/variants/thinknode_m5/platformio.ini +++ b/variants/thinknode_m5/platformio.ini @@ -30,7 +30,7 @@ build_flags = ${esp32_base.build_flags} -D UI_MSG_PREVIEW_SIZE=165 -D EINK_MAX_PARTIAL_REFRESH=60 -D BACKLIGHT_BTN=PIN_BUTTON2 - -D AUTO_OFF_MILLIS=1800000 + -D AUTO_OFF_MILLIS=900000 -D UI_NO_HIBERNATE -D DISABLE_DIAGNOSTIC_OUTPUT -D SX126X_DIO2_AS_RF_SWITCH=true diff --git a/variants/thinknode_m8/platformio.ini b/variants/thinknode_m8/platformio.ini index 2c07038f2..47d3a3fcd 100644 --- a/variants/thinknode_m8/platformio.ini +++ b/variants/thinknode_m8/platformio.ini @@ -24,7 +24,7 @@ build_flags = ${nrf52_base.build_flags} -D UI_RECENT_LIST_SIZE=9 -D UI_MSG_PREVIEW_SIZE=165 -D EINK_MAX_PARTIAL_REFRESH=60 - -D AUTO_OFF_MILLIS=1800000 + -D AUTO_OFF_MILLIS=900000 -D BACKLIGHT_BTN=PIN_BUTTON2 -D UI_SHOW_CLOCK=1 -D EINK_SCREENOFF_BLANK From 56b5f4e9c68ae668662f93044b0c2a62d2ccf10b Mon Sep 17 00:00:00 2001 From: Marcel Verdult Date: Thu, 22 Oct 2026 09:33:07 +0000 Subject: [PATCH 11/26] fix(chat): refuse a negative text length for a group message CMD_SEND_CHANNEL_TXT_MSG reads txt_type, channel_idx and a 4 byte timestamp, so the frame needs 7 bytes before the text starts, and the branch had no length guard. For a shorter frame "len - i" is computed in size_t, wraps, and reaches sendGroupMessage() as a negative int. The clamp there tests "text_len + prefix_len > MAX_TEXT_LEN", which a negative value passes, and the following memcpy() converts the length back to a size_t: a 3 byte frame asks for 4294967292 bytes into a 197 byte stack buffer. Two guards, because there are two mistakes. The branch now states its minimum like most other branches of handleCmdFrame do, so a frame below 7 bytes is answered through the existing path instead of being parsed. And sendGroupMessage() refuses a negative length the way sendGroupData() in the same file already does, which protects every other caller. A frame of exactly 7 bytes keeps working and sends an empty message, as before. Evidence. On a T-Echo running dev 428a95c6, built with FIRMWARE_BUILD_DATE "f1-dev" and confirmed by "ver => v1.18.0 (Build: f1-dev)": CH_TXT len=7 -> RESP_CODE_OK GET_STATS core -> uptime=78s CH_TXT len=3 -> NO REPLY DEVICE_QUERY -> NO REPLY (three times over 8 s, node never came back) --- examples/companion_radio/MyMesh.cpp | 4 +++- src/helpers/BaseChatMesh.cpp | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 99a9aa30d..d74050be7 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1175,7 +1175,9 @@ void MyMesh::handleCmdFrame(size_t len) { ? ERR_CODE_NOT_FOUND : ERR_CODE_UNSUPPORTED_CMD); // unknown recipient, or unsupported TXT_TYPE_* } - } else if (cmd_frame[0] == CMD_SEND_CHANNEL_TXT_MSG) { // send GroupChannel text msg + } else if (cmd_frame[0] == CMD_SEND_CHANNEL_TXT_MSG && len >= 7) { // send GroupChannel text msg + // 7 = code, txt_type, channel_idx and the 4 byte timestamp; the text may be empty. + // Without this the text length below is computed as len - 7 and turns negative. int i = 1; uint8_t txt_type = cmd_frame[i++]; // should be TXT_TYPE_PLAIN uint8_t channel_idx = cmd_frame[i++]; diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 28592415c..2637bc327 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -516,6 +516,11 @@ int BaseChatMesh::sendCommandData(const ContactInfo& recipient, uint32_t timest } bool BaseChatMesh::sendGroupMessage(uint32_t timestamp, mesh::GroupChannel& channel, const char* sender_name, const char* text, int text_len) { + if (text_len < 0) { // memcpy() below would read this as a size_t, so clamping cannot catch it + MESH_DEBUG_PRINTLN("sendGroupMessage: invalid negative text_len=%d", text_len); + return false; + } + uint8_t temp[5+MAX_TEXT_LEN+32]; memcpy(temp, ×tamp, 4); // mostly an extra blob to help make packet_hash unique temp[4] = 0; // TXT_TYPE_PLAIN From 83bd824f6c9b1b8e36f802a341d5b2201b647f7b Mon Sep 17 00:00:00 2001 From: Marcel Verdult Date: Thu, 22 Oct 2026 09:42:58 +0000 Subject: [PATCH 12/26] fix(radio): refuse "nan" for the airtime factor "set af nan" is accepted and stores a NaN in airtime_factor, the duty cycle budget the roles hand to their airtime accounting. strtof() consumes the token, so the parse guard does not fire, and no ordered comparison is true for a NaN, so "af < 0 || af > 9" cannot reject it. "set dutycycle" writes the same field and has the same hole. Both commands now check the text before it is parsed: after optional white space and sign, a number has to start with a digit or a '.'. That refuses "nan", "inf" and their variants, and keeps every spelling that was accepted before (signs, leading white space, ".5"). The ranges and both reply strings are unchanged. The check is on the text rather than on the parsed float on purpose. nRF52 builds use -Ofast, and under -ffast-math the compiler may assume a float is never NaN or infinite. A test on the value, even one that reads its bits, can then be folded away: clang does exactly that with a bit test at -Ofast, while arm-none-eabi-gcc keeps it. A character test does not depend on that. --- src/helpers/CommonRadioPrefs.cpp | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/helpers/CommonRadioPrefs.cpp b/src/helpers/CommonRadioPrefs.cpp index 41b179932..44e0eba31 100644 --- a/src/helpers/CommonRadioPrefs.cpp +++ b/src/helpers/CommonRadioPrefs.cpp @@ -2,6 +2,22 @@ #include "TxtDataHelpers.h" #include "Utils.h" #include "target.h" +#include + +namespace { + +// strtof() and atof() also read "nan" and "inf". A NaN cannot be refused by a +// range check: no ordered comparison is true for it, and under -ffast-math, +// which nRF52 builds use, the compiler may assume it never occurs and fold any +// test on the parsed value away. So the text is checked before it is parsed: +// after optional white space and sign, a number starts with a digit or a '.'. +bool looksLikeANumber(const char* s) { + while (isspace((unsigned char)*s)) s++; + if (*s == '+' || *s == '-') s++; + return isdigit((unsigned char)*s) || *s == '.'; +} + +} // namespace bool CommonRadioPrefs::getByKey(const char* key, char* value, size_t max_len) { if (strcmp(key, "fem_rxgain") == 0) { @@ -69,7 +85,7 @@ bool CommonRadioPrefs::handleCommand(const char* command, uint32_t sender_timest if (memcmp(command, "set af ", 7) == 0) { char* end; float af = strtof(&command[7], &end); - if (end == &command[7] || af < 0 || af > 9) { + if (end == &command[7] || !looksLikeANumber(&command[7]) || af < 0 || af > 9) { strcpy(reply, "ERROR: af must be 0-9"); } else { setAirtimeFactor(af); @@ -87,7 +103,7 @@ bool CommonRadioPrefs::handleCommand(const char* command, uint32_t sender_timest } if (memcmp(command, "set dutycycle ", 14) == 0) { float dc = atof(&command[14]); - if (dc < 1 || dc > 100) { + if (!looksLikeANumber(&command[14]) || dc < 1 || dc > 100) { strcpy(reply, "ERROR: dutycycle must be 1-100"); } else { setAirtimeFactor((100.0f / dc) - 1.0f); From 47259633f2e1d419377b248c88ce8dd2e740fa63 Mon Sep 17 00:00:00 2001 From: Blake Latchford Date: Thu, 22 Oct 2026 17:57:33 -0400 Subject: [PATCH 13/26] Fix broken KISS modem build on 6 ESP32-S3 platforms Broken out from PR #3496 which has been open for a month. --- examples/kiss_modem/main.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/kiss_modem/main.cpp b/examples/kiss_modem/main.cpp index 44b2a3df0..bb77c23d7 100644 --- a/examples/kiss_modem/main.cpp +++ b/examples/kiss_modem/main.cpp @@ -115,7 +115,10 @@ void setup() { delay(100); #if defined(ESP32) && defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT Serial.setTxTimeoutMs(USB_TX_TIMEOUT_MS); +#if defined(ARDUINO_USB_MODE) && ARDUINO_USB_MODE + // HWCDC only; TinyUSB's USBCDC TX buffer is fixed at build time Serial.setTxBufferSize(USB_TX_BUFFER_SIZE); +#endif #endif modem = new KissModem(Serial, identity, rng, radio_driver, board, sensors); #endif From da98f5318da86699b0711914a9abe27880cec805 Mon Sep 17 00:00:00 2001 From: Florent Date: Sat, 24 Oct 2026 10:16:56 -0400 Subject: [PATCH 14/26] UiTask: better cleaning of eink screens when hibernating --- examples/companion_radio/ui-new/UITask.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 6e987f875..aced739dc 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -908,9 +908,11 @@ void UITask::shutdown(bool restart){ if (restart) { _board->reboot(); } else { - display.forceFullRefresh(); - display.clear(); - display.endFrame(); + if (_display->isEink()) { + _display->blankScreen(); + _display->turnOff(); + _display->powerOff(); + } // Power off board including radio, display, GPS and components _board->powerOff(); } From d7c0eadbe25c336a0b2d3d80249dfe07521ad6d5 Mon Sep 17 00:00:00 2001 From: Florent Date: Sat, 24 Oct 2026 11:15:28 -0400 Subject: [PATCH 15/26] thinknode_m1: fix button2 definition --- variants/thinknode_m1/variant.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/thinknode_m1/variant.h b/variants/thinknode_m1/variant.h index f7f52a6a5..e096f3caa 100644 --- a/variants/thinknode_m1/variant.h +++ b/variants/thinknode_m1/variant.h @@ -81,7 +81,7 @@ #define BUTTON_PIN PIN_BUTTON1 #define PIN_USER_BTN BUTTON_PIN -#define PIN_BUTTON2 (11) +#define PIN_BUTTON2 (39) #define BUTTON_PIN2 PIN_BUTTON2 #define EXTERNAL_FLASH_DEVICES MX25R1635F From b2106ea7ee3a569158369ad772318569504d543c Mon Sep 17 00:00:00 2001 From: Florent Date: Sat, 24 Oct 2026 13:26:06 -0400 Subject: [PATCH 16/26] ThinknodeM1: update eink driver --- variants/thinknode_m1/platformio.ini | 5 +++++ variants/thinknode_m1/variant.h | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/variants/thinknode_m1/platformio.ini b/variants/thinknode_m1/platformio.ini index c775ae3fa..7c620e2ed 100644 --- a/variants/thinknode_m1/platformio.ini +++ b/variants/thinknode_m1/platformio.ini @@ -27,6 +27,11 @@ build_flags = ${nrf52_base.build_flags} -D UI_RECENT_LIST_SIZE=9 -D UI_MSG_PREVIEW_SIZE=165 -D EINK_MAX_PARTIAL_REFRESH=60 + -D EINK_DISPLAY_MODEL=GxEPD2_154_D67 + -D EINK_SCALE_X=1.5625f + -D EINK_SCALE_Y=1.5625f + -D EINK_X_OFFSET=0 + -D EINK_Y_OFFSET=10 -D BACKLIGHT_BTN=PIN_BUTTON2 -D AUTO_OFF_MILLIS=900000 -D UI_NO_HIBERNATE diff --git a/variants/thinknode_m1/variant.h b/variants/thinknode_m1/variant.h index e096f3caa..aecf197e6 100644 --- a/variants/thinknode_m1/variant.h +++ b/variants/thinknode_m1/variant.h @@ -131,6 +131,11 @@ extern const int SCK; #define DISP_BUSY (3) #define DISP_BACKLIGHT (43) +#define PIN_DISPLAY_CS DISP_CS +#define PIN_DISPLAY_DC DISP_DC +#define PIN_DISPLAY_RST DISP_RST +#define PIN_DISPLAY_BUSY DISP_BUSY + //////////////////////////////////////////////////////////////////////////////// // GPS From 2cad4eecb4d48ebe4e663bf26a92c8265993a18e Mon Sep 17 00:00:00 2001 From: Florent Date: Sun, 25 Oct 2026 17:49:16 -0400 Subject: [PATCH 17/26] ThinknodeM5: enable wifi in usb companion --- variants/thinknode_m5/platformio.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/variants/thinknode_m5/platformio.ini b/variants/thinknode_m5/platformio.ini index 1980575ec..a9761597d 100644 --- a/variants/thinknode_m5/platformio.ini +++ b/variants/thinknode_m5/platformio.ini @@ -177,12 +177,14 @@ build_flags = -D MAX_GROUP_CHANNELS=40 -D OFFLINE_QUEUE_SIZE=256 -D ENABLE_USB_INTERFACE + -D ENABLE_WIFI_INTERFACE build_src_filter = ${ThinkNode_M5.build_src_filter} + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> + + + lib_deps = ${ThinkNode_M5.lib_deps} densaugeo/base64 @ ~1.4.0 From 26e438904e19e374c798ce2dfd4b453e0f40741c Mon Sep 17 00:00:00 2001 From: Blake Latchford Date: Sun, 25 Oct 2026 21:45:10 -0400 Subject: [PATCH 18/26] Saturate duplicate packet stats instead of wrapping Tested on a Heltec V4 repeater with the counter seeded at 65530. Fixes #3255 Co-Authored-By: Claude Opus 5.5 --- examples/simple_repeater/MyMesh.cpp | 4 ++-- examples/simple_room_server/MyMesh.cpp | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 61b6c149e..b50d19d01 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -229,8 +229,8 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t stats.n_recv_direct = getNumRecvDirect(); stats.err_events = _err_flags; stats.last_snr = (int16_t)(radio_driver.getLastSNR() * 4); - stats.n_direct_dups = ((SimpleMeshTables *)getTables())->getNumDirectDups(); - stats.n_flood_dups = ((SimpleMeshTables *)getTables())->getNumFloodDups(); + stats.n_direct_dups = std::min(((SimpleMeshTables *)getTables())->getNumDirectDups(), 0xFFFF); + stats.n_flood_dups = std::min(((SimpleMeshTables *)getTables())->getNumFloodDups(), 0xFFFF); stats.total_rx_air_time_secs = getReceiveAirTime() / 1000; stats.n_recv_errors = radio_driver.getPacketsRecvErrors(); memcpy(&reply_data[4], &stats, sizeof(stats)); diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index da4c776c0..935fd7f92 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -1,4 +1,5 @@ #include "MyMesh.h" +#include #define REPLY_DELAY_MILLIS 1500 #define PUSH_NOTIFY_DELAY_MILLIS 2000 @@ -170,8 +171,8 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t stats.n_recv_direct = getNumRecvDirect(); stats.err_events = _err_flags; stats.last_snr = (int16_t)(radio_driver.getLastSNR() * 4); - stats.n_direct_dups = ((SimpleMeshTables *)getTables())->getNumDirectDups(); - stats.n_flood_dups = ((SimpleMeshTables *)getTables())->getNumFloodDups(); + stats.n_direct_dups = std::min(((SimpleMeshTables *)getTables())->getNumDirectDups(), 0xFFFF); + stats.n_flood_dups = std::min(((SimpleMeshTables *)getTables())->getNumFloodDups(), 0xFFFF); stats.n_posted = _num_posted; stats.n_post_push = _num_post_pushes; From bd882a2aa207e997e125c90afec16c437da399c1 Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 26 Oct 2026 20:29:39 +1100 Subject: [PATCH 19/26] * scope 0 rule fixes --- src/helpers/RegionMap.cpp | 2 +- src/helpers/RoutingPolicy.h | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/helpers/RegionMap.cpp b/src/helpers/RegionMap.cpp index d8d773803..16a8e1ff9 100644 --- a/src/helpers/RegionMap.cpp +++ b/src/helpers/RegionMap.cpp @@ -188,7 +188,7 @@ int RegionMap::getTransportKeysFor(const RegionEntry& src, TransportKey dest[], } RegionEntry* RegionMap::findMatch(mesh::Packet* packet, uint8_t mask) { - if (packet->transport_codes[0] == 0) return &wildcard; + if (packet->transport_codes[0] == 0) return (wildcard.flags & mask) == 0 ? &wildcard : NULL; for (int i = 0; i < num_regions; i++) { auto region = ®ions[i]; if ((region->flags & mask) == 0) { // does region allow this? (per 'mask' param) diff --git a/src/helpers/RoutingPolicy.h b/src/helpers/RoutingPolicy.h index 5251a590a..225e5cd48 100644 --- a/src/helpers/RoutingPolicy.h +++ b/src/helpers/RoutingPolicy.h @@ -17,6 +17,7 @@ inline bool isFloodHopLimitExceeded(const Packet* packet, uint8_t flood_max, uint8_t hops = packet->getPathHashCount(); if (hops >= flood_max) return true; if (packet->getRouteType() == ROUTE_TYPE_FLOOD && hops >= flood_max_unscoped) return true; + if (packet->getRouteType() == ROUTE_TYPE_TRANSPORT_FLOOD && packet->transport_codes[0] == 0 && hops >= flood_max_unscoped) return true; // scope 0, treat as unscoped if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT && hops >= flood_max_advert) return true; return false; } From 55b961e36f8e020a08f0f5f8b65ad37485be070b Mon Sep 17 00:00:00 2001 From: Scott Powell Date: Mon, 26 Oct 2026 20:39:46 +1100 Subject: [PATCH 20/26] * unit test fix --- test/test_routing_policy/test_routing_policy.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/test_routing_policy/test_routing_policy.cpp b/test/test_routing_policy/test_routing_policy.cpp index bba9ec790..3f7faf305 100644 --- a/test/test_routing_policy/test_routing_policy.cpp +++ b/test/test_routing_policy/test_routing_policy.cpp @@ -8,6 +8,9 @@ static Packet makeFlood(uint8_t route_type, uint8_t payload_type, uint8_t hops) p.header = route_type | (payload_type << PH_TYPE_SHIFT); p.setPathHashSizeAndCount(1, hops); p.payload_len = 1; + if (route_type == ROUTE_TYPE_TRANSPORT_FLOOD) { + p.transport_codes[0] = 1; // has to be non-zero + } return p; } From e197e557c32166cbfdbf1d142452d0c8048d3f37 Mon Sep 17 00:00:00 2001 From: Marcel Verdult Date: Fri, 23 Oct 2026 01:30:57 +0200 Subject: [PATCH 21/26] fix(esp32): define the RTC backup of ESP32RTCClock once ESP32Board.h defined _rtc_backup_time and _rtc_backup_magic as static in the header, so every translation unit that includes it has its own copy, and the inline members of ESP32RTCClock read and write the copy of the unit they are compiled in. Today one unit per image uses them; a second one could restore a copy that setCurrentTime() never wrote. The header now declares them, and ESP32Board.cpp defines them once. --- src/helpers/ESP32Board.cpp | 4 ++++ src/helpers/ESP32Board.h | 7 +++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/helpers/ESP32Board.cpp b/src/helpers/ESP32Board.cpp index a55abb264..d190fca19 100644 --- a/src/helpers/ESP32Board.cpp +++ b/src/helpers/ESP32Board.cpp @@ -3,6 +3,10 @@ #include "ESP32Board.h" #include +// The RTC backup of ESP32RTCClock, declared in ESP32Board.h. +RTC_NOINIT_ATTR uint32_t _rtc_backup_time; +RTC_NOINIT_ATTR uint32_t _rtc_backup_magic; + #if defined(ADMIN_PASSWORD) && !defined(DISABLE_WIFI_OTA) // Repeater or Room Server only #include #include diff --git a/src/helpers/ESP32Board.h b/src/helpers/ESP32Board.h index 3f95e6923..cd274cd93 100644 --- a/src/helpers/ESP32Board.h +++ b/src/helpers/ESP32Board.h @@ -200,8 +200,11 @@ public: } }; -static RTC_NOINIT_ATTR uint32_t _rtc_backup_time; -static RTC_NOINIT_ATTR uint32_t _rtc_backup_magic; +// The time ESP32RTCClock restores after a reset, in RTC memory that a reset does +// not clear. Defined once, in ESP32Board.cpp: the inline members below run in +// every translation unit that uses them and must all see the same backup. +extern uint32_t _rtc_backup_time; +extern uint32_t _rtc_backup_magic; #define RTC_BACKUP_MAGIC 0xAA55CC33 #define RTC_TIME_MIN 1772323200 // 1 Mar 2026 From b0bacf8a0451b1b5e5b32015dbf2f204fab173ca Mon Sep 17 00:00:00 2001 From: WayenWeng Date: Tue, 27 Oct 2026 10:44:36 +0800 Subject: [PATCH 22/26] fix(radio): put LR2021 in standby before re-config in setParams --- src/helpers/radiolib/CustomLR2021Wrapper.h | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/helpers/radiolib/CustomLR2021Wrapper.h b/src/helpers/radiolib/CustomLR2021Wrapper.h index 8299e2bf5..c60bfda3d 100644 --- a/src/helpers/radiolib/CustomLR2021Wrapper.h +++ b/src/helpers/radiolib/CustomLR2021Wrapper.h @@ -16,15 +16,23 @@ public: CustomLR2021Wrapper(CustomLR2021& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { } void setParams(float freq, float bw, uint8_t sf, uint8_t cr) override { - ((CustomLR2021 *)_radio)->setFrequency(freq); - ((CustomLR2021 *)_radio)->setSpreadingFactor(sf); - ((CustomLR2021 *)_radio)->setBandwidth(bw); - ((CustomLR2021 *)_radio)->setCodingRate(cr); + auto r = (CustomLR2021 *)_radio; + // LR2021 only accepts RF/modulation config commands while in STANDBY mode. + // Re-configuring while in continuous RX gets silently rejected (-706), + // which broke hot frequency changes over BLE. + int16_t status = r->standby(); + if (status != RADIOLIB_ERR_NONE) { MESH_DEBUG_PRINTLN("CustomLR2021Wrapper: standby() -> %d", status); } + if ((status = r->setFrequency(freq)) != RADIOLIB_ERR_NONE) { MESH_DEBUG_PRINTLN("CustomLR2021Wrapper: setFrequency() -> %d", status); } + if ((status = r->setSpreadingFactor(sf)) != RADIOLIB_ERR_NONE) { MESH_DEBUG_PRINTLN("CustomLR2021Wrapper: setSpreadingFactor() -> %d", status); } + if ((status = r->setBandwidth(bw)) != RADIOLIB_ERR_NONE) { MESH_DEBUG_PRINTLN("CustomLR2021Wrapper: setBandwidth() -> %d", status); } + if ((status = r->setCodingRate(cr)) != RADIOLIB_ERR_NONE) { MESH_DEBUG_PRINTLN("CustomLR2021Wrapper: setCodingRate() -> %d", status); } updatePreamble(sf); - applySideDetectorConfig(); + // internal idle() sets STATE_IDLE, which triggers a startReceive() on the next loop + status = applySideDetectorConfig(); + if (status != RADIOLIB_ERR_NONE) { MESH_DEBUG_PRINTLN("CustomLR2021Wrapper: applySideDetectorConfig() -> %d", status); } PacketMillis pm = calcMaxPacketMillis(sf, bw, cr, preambleLengthForSF(sf)); - ((CustomLR2021 *)_radio)->setPreambleMillis(pm.preambleMillis); - ((CustomLR2021 *)_radio)->setMaxPayloadMillis(pm.payloadMillis); + r->setPreambleMillis(pm.preambleMillis); // pure in-memory setters, no standby needed + r->setMaxPayloadMillis(pm.payloadMillis); } From 3e4f71d9a22ce990128f1dc49139adccdc746de2 Mon Sep 17 00:00:00 2001 From: rado <6479266+rbrus@users.noreply.github.com> Date: Tue, 27 Oct 2026 07:21:24 +0100 Subject: [PATCH 23/26] Drop unit tests per review feedback --- .../test_companion_node_prefs.cpp | 57 ------------------- 1 file changed, 57 deletions(-) diff --git a/test/test_companion_node_prefs/test_companion_node_prefs.cpp b/test/test_companion_node_prefs/test_companion_node_prefs.cpp index ab5e0f908..433c97106 100644 --- a/test/test_companion_node_prefs/test_companion_node_prefs.cpp +++ b/test/test_companion_node_prefs/test_companion_node_prefs.cpp @@ -6,32 +6,6 @@ #include "../../examples/companion_radio/NodePrefs.h" -bool CommonRadioPrefs::getByKey(const char* key, char* value, size_t max_len) { - if (strcmp(key, "fem_rxgain") == 0) { - snprintf(value, max_len, "%d", (uint32_t)getFEMRxGain()); - return true; - } - if (strcmp(key, "fem_txgain") == 0) { - snprintf(value, max_len, "%d", (uint32_t)getFEMTxGain()); - return true; - } - return false; -} - -bool CommonRadioPrefs::setByKey(const char* key, const char* value) { - if (strcmp(key, "fem_rxgain") == 0) { - setFEMRxGain(atoi(value)); - markDirty(); - return true; - } - if (strcmp(key, "fem_txgain") == 0) { - setFEMTxGain(atoi(value)); - markDirty(); - return true; - } - return false; -} - class ReplayStream : public Stream { const char* _text; int _pos = 0; @@ -103,37 +77,6 @@ TEST(CompanionNodePrefs, RxGainSettingsRoundTripIndependently) { } #endif -TEST(CompanionNodePrefs, NegativeTxPowerPreservedAsSigned) { - NodePrefs prefs; - CommonRadioPrefs* radio = prefs.getRadioPrefs(); - - radio->setTxPower(-9); - EXPECT_EQ(-9, prefs.tx_power_dbm); - EXPECT_EQ(-9, radio->getTxPower()); - EXPECT_EQ(-9, static_cast(radio->getTxPower())); - - char reply[32]; - snprintf(reply, sizeof(reply), "> %d", static_cast(radio->getTxPower())); - EXPECT_STREQ("> -9", reply); -} - -TEST(CompanionNodePrefs, NegativeTxPowerRoundTripsSerial) { - NodePrefs saved; - saved.getRadioPrefs()->setTxPower(-9); - - CaptureStream output; - ASSERT_TRUE(saved.saveSerial(output)); - EXPECT_NE(std::string::npos, output.text().find("tx:-9")); - - ReplayStream input("{radio:{tx:-9}}"); - NodePrefs loaded; - loaded.tx_power_dbm = 22; - - ASSERT_TRUE(loaded.loadSerial(input)); - EXPECT_EQ(-9, loaded.tx_power_dbm); - EXPECT_EQ(-9, loaded.getRadioPrefs()->getTxPower()); -} - int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); From 63bc8ed2bfba30dcb23d5e65f3d3f1766d9fabd1 Mon Sep 17 00:00:00 2001 From: Rastislav Vysoky Date: Tue, 27 Oct 2026 07:42:32 +0100 Subject: [PATCH 24/26] bump RadioLib to 7.8.1 --- platformio.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/platformio.ini b/platformio.ini index f296f8382..92c94204c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -19,8 +19,7 @@ monitor_speed = 115200 lib_deps = SPI Wire - ;jgromes/RadioLib @ ^7.7.1 - https://github.com/jgromes/RadioLib.git#6d8934836678d8894e3d556550475b37dce3e2b6 + jgromes/RadioLib @ 7.8.1 rweather/Crypto @ ^0.4.0 adafruit/RTClib @ ^2.1.3 melopero/Melopero RV3028 @ ^1.1.0 From 492814bbef5cb0918c3fa6c51b49b0c847692542 Mon Sep 17 00:00:00 2001 From: akalinin-softvelum <142931037+akalinin-softvelum@users.noreply.github.com> Date: Tue, 27 Oct 2026 18:20:36 +0000 Subject: [PATCH 25/26] T-Echo: use GxEPD2_154_D67 e-ink driver T-Echo used the fallback GxEPD2_150_BN; on my unit every refresh hit the 10 s busy timeout. Switch to GxEPD2_154_D67 like Meshtastic. --- variants/lilygo_techo/platformio.ini | 5 +++++ variants/lilygo_techo/variant.h | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/variants/lilygo_techo/platformio.ini b/variants/lilygo_techo/platformio.ini index eb4a69e28..ba39549e1 100644 --- a/variants/lilygo_techo/platformio.ini +++ b/variants/lilygo_techo/platformio.ini @@ -32,6 +32,11 @@ build_flags = ${nrf52_base.build_flags} -D PIN_GPS_RESET_ACTIVE=LOW -D TELEM_BME280_ADDRESS=0x77 -D DISPLAY_CLASS=GxEPDDisplay + -D EINK_DISPLAY_MODEL=GxEPD2_154_D67 + -D EINK_SCALE_X=1.5625f + -D EINK_SCALE_Y=1.5625f + -D EINK_X_OFFSET=0 + -D EINK_Y_OFFSET=10 -D EINK_SCREENOFF_BLANK -D UI_RECENT_LIST_SIZE=9 -D UI_MSG_PREVIEW_SIZE=165 diff --git a/variants/lilygo_techo/variant.h b/variants/lilygo_techo/variant.h index 1de62d91a..d11aac58c 100644 --- a/variants/lilygo_techo/variant.h +++ b/variants/lilygo_techo/variant.h @@ -131,6 +131,12 @@ extern const int SCK; #define DISP_BUSY (3) #define DISP_BACKLIGHT (43) +// used by GxEPDDisplay when EINK_DISPLAY_MODEL is set +#define PIN_DISPLAY_CS DISP_CS +#define PIN_DISPLAY_DC DISP_DC +#define PIN_DISPLAY_RST DISP_RST +#define PIN_DISPLAY_BUSY DISP_BUSY + //////////////////////////////////////////////////////////////////////////////// // GPS From cedb8050a507549a079744f88a215bfd62d20268 Mon Sep 17 00:00:00 2001 From: Florent Date: Tue, 27 Oct 2026 15:41:36 -0400 Subject: [PATCH 26/26] Remove unused buttonStateChanged from some variants --- variants/meshtracker_x1/MeshTrackerX1Board.cpp | 1 - variants/meshtracker_x1/MeshTrackerX1Board.h | 14 -------------- variants/promicro/PromicroBoard.cpp | 1 - variants/promicro/PromicroBoard.h | 12 ------------ variants/t1000-e/T1000eBoard.cpp | 1 - variants/t1000-e/T1000eBoard.h | 14 -------------- variants/thinknode_m3/ThinkNodeM3Board.cpp | 1 - variants/thinknode_m3/ThinkNodeM3Board.h | 12 ------------ variants/thinknode_m4/ThinkNodeM4Board.cpp | 1 - variants/thinknode_m4/ThinkNodeM4Board.h | 12 ------------ 10 files changed, 69 deletions(-) diff --git a/variants/meshtracker_x1/MeshTrackerX1Board.cpp b/variants/meshtracker_x1/MeshTrackerX1Board.cpp index 56e537a23..94c20a63c 100644 --- a/variants/meshtracker_x1/MeshTrackerX1Board.cpp +++ b/variants/meshtracker_x1/MeshTrackerX1Board.cpp @@ -5,7 +5,6 @@ void MeshTrackerX1Board::begin() { NRF52BoardDCDC::begin(); - btn_prev_state = LOW; // button is active HIGH #ifdef BUTTON_PIN pinMode(BATTERY_PIN, INPUT); diff --git a/variants/meshtracker_x1/MeshTrackerX1Board.h b/variants/meshtracker_x1/MeshTrackerX1Board.h index 51f022e0b..d332244f5 100644 --- a/variants/meshtracker_x1/MeshTrackerX1Board.h +++ b/variants/meshtracker_x1/MeshTrackerX1Board.h @@ -5,9 +5,6 @@ #include class MeshTrackerX1Board : public NRF52BoardDCDC { -protected: - uint8_t btn_prev_state; - public: MeshTrackerX1Board() : NRF52Board("X1_OTA") {} void begin(); @@ -35,17 +32,6 @@ public: return "Seeed SenseCAP MeshTracker X1"; } - int buttonStateChanged() { - #ifdef BUTTON_PIN - uint8_t v = digitalRead(BUTTON_PIN); - if (v != btn_prev_state) { - btn_prev_state = v; - return (v == USER_BTN_PRESSED) ? 1 : -1; - } - #endif - return 0; - } - void powerOff() override { #ifdef HAS_GPS digitalWrite(GPS_VRTC_EN, LOW); diff --git a/variants/promicro/PromicroBoard.cpp b/variants/promicro/PromicroBoard.cpp index 7011521b8..aba808786 100644 --- a/variants/promicro/PromicroBoard.cpp +++ b/variants/promicro/PromicroBoard.cpp @@ -5,7 +5,6 @@ void PromicroBoard::begin() { NRF52Board::begin(); - btn_prev_state = HIGH; pinMode(PIN_VBAT_READ, INPUT); diff --git a/variants/promicro/PromicroBoard.h b/variants/promicro/PromicroBoard.h index ee16c9654..605b03652 100644 --- a/variants/promicro/PromicroBoard.h +++ b/variants/promicro/PromicroBoard.h @@ -9,7 +9,6 @@ class PromicroBoard : public NRF52BoardDCDC { protected: - uint8_t btn_prev_state; float adc_mult = ADC_MULTIPLIER; public: @@ -48,15 +47,4 @@ public: const char* getManufacturerName() const override { return "ProMicro DIY"; } - - int buttonStateChanged() { - #ifdef BUTTON_PIN - uint8_t v = digitalRead(BUTTON_PIN); - if (v != btn_prev_state) { - btn_prev_state = v; - return (v == LOW) ? 1 : -1; - } - #endif - return 0; - } }; diff --git a/variants/t1000-e/T1000eBoard.cpp b/variants/t1000-e/T1000eBoard.cpp index 0d390f798..1d3ce423c 100644 --- a/variants/t1000-e/T1000eBoard.cpp +++ b/variants/t1000-e/T1000eBoard.cpp @@ -5,7 +5,6 @@ void T1000eBoard::begin() { NRF52BoardDCDC::begin(); - btn_prev_state = HIGH; #ifdef BUTTON_PIN pinMode(BATTERY_PIN, INPUT); diff --git a/variants/t1000-e/T1000eBoard.h b/variants/t1000-e/T1000eBoard.h index ba8bf0c1d..653bea617 100644 --- a/variants/t1000-e/T1000eBoard.h +++ b/variants/t1000-e/T1000eBoard.h @@ -5,9 +5,6 @@ #include class T1000eBoard : public NRF52BoardDCDC { -protected: - uint8_t btn_prev_state; - public: T1000eBoard() : NRF52Board("T1000E_OTA") {} void begin(); @@ -38,17 +35,6 @@ public: return "Seeed Tracker T1000-E"; } - int buttonStateChanged() { - #ifdef BUTTON_PIN - uint8_t v = digitalRead(BUTTON_PIN); - if (v != btn_prev_state) { - btn_prev_state = v; - return (v == USER_BTN_PRESSED) ? 1 : -1; - } - #endif - return 0; - } - void powerOff() override { #ifdef HAS_GPS digitalWrite(GPS_VRTC_EN, LOW); diff --git a/variants/thinknode_m3/ThinkNodeM3Board.cpp b/variants/thinknode_m3/ThinkNodeM3Board.cpp index ac513ade5..518d15bda 100644 --- a/variants/thinknode_m3/ThinkNodeM3Board.cpp +++ b/variants/thinknode_m3/ThinkNodeM3Board.cpp @@ -6,7 +6,6 @@ void ThinkNodeM3Board::begin() { NRF52Board::begin(); - btn_prev_state = HIGH; Wire.begin(); diff --git a/variants/thinknode_m3/ThinkNodeM3Board.h b/variants/thinknode_m3/ThinkNodeM3Board.h index 9e8f49894..bbd69d264 100644 --- a/variants/thinknode_m3/ThinkNodeM3Board.h +++ b/variants/thinknode_m3/ThinkNodeM3Board.h @@ -11,7 +11,6 @@ protected: #if NRF52_POWER_MANAGEMENT void initiateShutdown(uint8_t reason) override; #endif - uint8_t btn_prev_state; public: ThinkNodeM3Board() : NRF52Board("THINKNODE_M3_OTA") {} @@ -31,17 +30,6 @@ public: return "Elecrow ThinkNode M3"; } - int buttonStateChanged() { - #ifdef BUTTON_PIN - uint8_t v = digitalRead(BUTTON_PIN); - if (v != btn_prev_state) { - btn_prev_state = v; - return (v == LOW) ? 1 : -1; - } - #endif - return 0; - } - void powerOff() override { // turn off all leds, sd_power_system_off will not do this for us digitalWrite(PIN_LED_BLUE, !LED_STATE_ON); diff --git a/variants/thinknode_m4/ThinkNodeM4Board.cpp b/variants/thinknode_m4/ThinkNodeM4Board.cpp index 4a144a3a4..281983ae3 100644 --- a/variants/thinknode_m4/ThinkNodeM4Board.cpp +++ b/variants/thinknode_m4/ThinkNodeM4Board.cpp @@ -6,7 +6,6 @@ void ThinkNodeM4Board::begin() { NRF52Board::begin(); - btn_prev_state = HIGH; Wire.begin(); battery_serial->begin(4800); diff --git a/variants/thinknode_m4/ThinkNodeM4Board.h b/variants/thinknode_m4/ThinkNodeM4Board.h index c2cae54b6..63de1ff27 100644 --- a/variants/thinknode_m4/ThinkNodeM4Board.h +++ b/variants/thinknode_m4/ThinkNodeM4Board.h @@ -9,7 +9,6 @@ protected: #if NRF52_POWER_MANAGEMENT void initiateShutdown(uint8_t reason) override; #endif - uint8_t btn_prev_state; HardwareSerial * battery_serial; uint16_t bat_level_mv = 0; uint8_t bat_level_percent =0; @@ -32,17 +31,6 @@ public: return "Elecrow ThinkNode M4"; } - int buttonStateChanged() { - #ifdef BUTTON_PIN - uint8_t v = digitalRead(BUTTON_PIN); - if (v != btn_prev_state) { - btn_prev_state = v; - return (v == USER_BTN_PRESSED) ? 1 : -1; - } - #endif - return 0; - } - void shutdownPeripherals() override { // shutdown common peripherals NRF52Board::shutdownPeripherals();