diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index 099b9ccc4..be018094f 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -399,6 +399,9 @@ jobs: - name: Run Unit Tests run: pio test -e native -e native_kiss_modem -e native_ota_channel -vv + - name: Verify sensor reply bounds and duplicate counter saturation + run: python3 -B test/test_upstream_cli_bounds.py -v + - name: Verify real GPS NMEA parser and provider with address and undefined sanitizers run: pio test -e native_gps -vv diff --git a/docs/companion_cli.md b/docs/companion_cli.md new file mode 100644 index 000000000..80abc8a07 --- /dev/null +++ b/docs/companion_cli.md @@ -0,0 +1,62 @@ +# Companion CLI + +Companions accept local CLI commands through the Companion protocol. Remote +commands require the configured admin permission. Available commands depend on +the board and firmware profile; use `help` for the commands in your build. + +## Common commands + +| Purpose | Commands | +| --- | --- | +| Firmware version | `ver` | +| Node name | `get name`, `set name ` | +| Radio profile | `get radio`, `set radio ,,,` | +| Frequency | `get freq`, `set freq ` | +| Transmit power | `get tx`, `set tx ` | +| Radio apply status | `get radio.apply` | +| Timezone, hours | `get tz.offset`, `set tz.offset ` | +| Airtime limit | `get dutycycle`, `set dutycycle ` | +| Receive gain | `get radio.rxgain`, `set radio.rxgain ` | +| Receive power saving | `get radio.rxps`, `get radio.rxps.config` | +| FEM receive gain | `get radio.fem.rxgain`, `set radio.fem.rxgain ` | +| FEM transmit gain | `get radio.fem.txgain`, `set radio.fem.txgain ` | +| BLE PIN | `set pin <0-999999>` | +| Restart | `reboot` | +| Power off, where supported | `poweroff` or `shutdown` | + +Supported frequencies, bandwidths and power limits depend on the radio and +board. RF changes may wait for active traffic; `get radio.apply` reports the +apply state. Transmit power is the radio-chip setting, which can differ from +antenna output on amplified boards. + +See [CLI commands](cli_commands.md) for setting ranges and access restrictions, +and [radio profiles](radio_profiles.md) for radio2, temporary profiles and +schedules. + +## Wi-Fi + +Wi-Fi builds expose `get wifi.status`, `get wifi.ip`, and +`get/set wifi.enabled`. Configure credentials with `set wifi.ssid ` and +`set wifi.pwd `. Credential access follows the normal local/admin +restrictions. Reboot after saving Wi-Fi settings to apply them. + +## Display and notifications + +This fork uses the saved `display.*` controls described in +[display controls](cli_commands.md#set-display-modes-and-timeouts): + +```text +get display.mode +get display.timeout +get display.usb.mode +get display.usb.timeout +get display.inbox +``` + +Timeouts default to 900 seconds on e-paper and 15 seconds on other screens. +Battery and USB settings are independent. Display settings live in a separate +checked preferences file and preserve the existing node-preferences layout. +The upstream `ui.*` settings are not aliases in this fork. + +For sound, vibration and custom notification rules, see +[notifications](notifications.md). diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 332666662..8a15d4df9 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -617,11 +617,7 @@ class HomeScreen : public UIScreen { sensors_scroll_offset = 0; return; } -#if AUTO_OFF_MILLIS > 0 - const uint32_t refresh_interval = 5000; -#else - const uint32_t refresh_interval = 60000; -#endif + const uint32_t refresh_interval = display.isEink() ? 60000 : 5000; // Empty data requests an initial/forced sample. Elapsed time also handles // returning to this page after more than half a millis() cycle. if (sensors_lpp.getSize() == 0 @@ -2293,11 +2289,7 @@ public: #endif renderChannelFilter(display); -#if AUTO_OFF_MILLIS==0 // probably e-ink - return UI_BUTTON_READER_HINT == 1 ? 3000 : 10000; -#else - return 1000; // next render after 1000 ms -#endif + return display.isEink() ? (UI_BUTTON_READER_HINT == 1 ? 3000 : 10000) : 1000; } bool handleInput(char c) override { @@ -2735,9 +2727,13 @@ void UITask::shutdown(bool restart){ if (restart) { _board->reboot(); } else { - display.forceFullRefresh(); - display.clear(); - display.endFrame(); + if (_display != NULL && _display->isEink()) { + if (_display->isOn()) _display->turnOff(); + } else { + display.forceFullRefresh(); + display.clear(); + display.endFrame(); + } // Power off board including radio, display, GPS and components _board->powerOff(); } @@ -2875,11 +2871,11 @@ void UITask::loop() { #endif #if defined(UI_HAS_ROTARY_INPUT) RotaryInputEvent rotaryEv = rotary_input.poll(); - if (c == 0 && _display != NULL && _display->isOn()) { + if (c == 0 && _display != NULL) { if (rotaryEv == RotaryInputEvent::Next) { - c = KEY_NEXT; + c = checkDisplayOn(KEY_NEXT); } else if (rotaryEv == RotaryInputEvent::Prev) { - c = KEY_PREV; + c = checkDisplayOn(KEY_PREV); } } #endif diff --git a/examples/companion_radio/ui-orig/UITask.cpp b/examples/companion_radio/ui-orig/UITask.cpp index 656a59884..062f9ee82 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -761,6 +761,7 @@ void UITask::loop() { userLedHandler(); #ifdef PIN_BUZZER + if (buzzer.isQuiet() != (bool)_node_prefs->buzzer_quiet) buzzer.quiet(_node_prefs->buzzer_quiet); if (buzzer.isPlaying()) buzzer.loop(); #endif #if defined(PIN_BUZZER) && MESH_GPS_VOICE diff --git a/examples/companion_radio/ui-tiny/UITask.cpp b/examples/companion_radio/ui-tiny/UITask.cpp index cfbb60444..aa5c268ac 100644 --- a/examples/companion_radio/ui-tiny/UITask.cpp +++ b/examples/companion_radio/ui-tiny/UITask.cpp @@ -998,6 +998,7 @@ void UITask::loop() { userLedHandler(); #ifdef PIN_BUZZER + if (buzzer.isQuiet() != (bool)_node_prefs->buzzer_quiet) buzzer.quiet(_node_prefs->buzzer_quiet); if (buzzer.isPlaying()) buzzer.loop(); #endif diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 99ad0af26..8cd61e507 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -846,8 +846,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 1a402b1fc..d40037c74 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -1383,8 +1383,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; diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 61d01feee..a39a85078 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -244,6 +244,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!! @@ -327,11 +328,14 @@ int BaseChatMesh::searchPeersByHash(const uint8_t* hash) { // A matching transient slot may have the same identity as an accepted contact. // Try persistent contacts first so the transient slot cannot swallow their DMs. for (int i = MAX_ANON_CONTACTS; i < num_contacts && n < MAX_SEARCH_RESULTS; i++) { + if (contacts[i].isEmptySlot()) continue; if (contacts[i].id.isHashMatch(hash)) { matching_peer_indexes[n++] = i; } } for (int i = 0; i < MAX_ANON_CONTACTS && n < MAX_SEARCH_RESULTS; i++) { + // Empty anonymous slots must not consume matches or derive a null key. + 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) } @@ -748,6 +752,11 @@ int BaseChatMesh::sendCommandData(const ContactInfo& recipient, } bool BaseChatMesh::sendGroupMessage(uint32_t timestamp, mesh::GroupChannel& channel, const char* sender_name, const char* text, int text_len) { + if (text_len < 0) { + 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 @@ -1086,6 +1095,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 @@ -1094,6 +1104,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 @@ -1102,6 +1113,7 @@ ContactInfo* BaseChatMesh::lookupContactByPubKey(const uint8_t* pub_key, int pre ContactInfo* BaseChatMesh::lookupTransientContactByPubKey(const uint8_t* pub_key, int prefix_len) { for (int i = 0; i < MAX_ANON_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 @@ -1110,6 +1122,7 @@ ContactInfo* BaseChatMesh::lookupTransientContactByPubKey(const uint8_t* pub_key ContactInfo* BaseChatMesh::lookupPersistentContactByPubKey(const uint8_t* pub_key, int prefix_len) { for (int i = MAX_ANON_CONTACTS; 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 diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 1fad91104..88cb00fe7 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -4166,18 +4166,26 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re if (start >= end) { strcpy(reply, "no custom var"); } else { + const int reply_max = 160; // callers pass a 160-byte reply buffer + const int next_reserve = 16; // room for "... next:NNN" sprintf(dp, "%d vars\n", end); dp = strchr(dp, 0); int i; - for (i = start; i < end && (dp-reply < 134); i++) { - sprintf(dp, "%s=%s\n", - _sensors->getSettingName(i), - _sensors->getSettingValue(i)); + for (i = start; i < end; i++) { + const char* name = _sensors->getSettingName(i); + const char* value = _sensors->getSettingValue(i); + if (name == NULL) name = "?"; + if (value == NULL) value = ""; + int room = reply_max - next_reserve - (int)(dp - reply); + int need = snprintf(NULL, 0, "%s=%s\n", name, value); + if (need >= room && i > start) break; // continue on the next page + snprintf(dp, room, "%s=%s\n", name, value); // a single oversized entry is truncated dp = strchr(dp, 0); + if (need >= room) { i++; break; } } if (i < end) { - sprintf(dp, "... next:%d", i); - } else { + snprintf(dp, reply_max - (int)(dp - reply), "... next:%d", i); + } else if (dp > reply && *(dp-1) == '\n') { *(dp-1) = 0; // remove last CR } } diff --git a/src/helpers/ContactInfo.h b/src/helpers/ContactInfo.h index a7aa4dfd7..97120e2d0 100644 --- a/src/helpers/ContactInfo.h +++ b/src/helpers/ContactInfo.h @@ -95,6 +95,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: #if !MESH_CONTACT_CACHE diff --git a/test/fixtures/cli_reply_text.json b/test/fixtures/cli_reply_text.json index a2790420e..25b06e4d1 100644 --- a/test/fixtures/cli_reply_text.json +++ b/test/fixtures/cli_reply_text.json @@ -1406,8 +1406,8 @@ } ], "noncompact_tokens_sha256": { - "src/helpers/CommonCLI.cpp": "fd13053f606a8d008c10a690cbc7729c2701f44f84e433bb3cd2c776b9e61b0c", - "examples/simple_repeater/MyMesh.cpp": "e92d418e64bd3e2ddff19d196aafac130cef02dd6463a687735dfe11cd273725", + "src/helpers/CommonCLI.cpp": "ba486e64f3c948c44f08a6cb2ea4ef82bf1f3844fe0287e94f653654d3675dea", + "examples/simple_repeater/MyMesh.cpp": "9e688372f2b35a2c0f20f0eae00255877200ac9d84c9e5a30870296fbb928ca8", "src/helpers/RadioProfileCLI.cpp": "d641d16e872eba3e73b9b5a50f9718c10b0b7da72782f793706ac36b3724a624", "src/helpers/WirelessControl.h": "f19bb602999118cc423ec74c7de7100ed91cb2c553295038077e52e321bb4e73", "src/helpers/PowerSavingDiagnostics.h": "18b78f5780046ea8c772c4ce6c337741ae0c027039d9399c94c5f4205d114000", diff --git a/test/fixtures/home_sensor_refresh.cpp b/test/fixtures/home_sensor_refresh.cpp index 74b4bdbd2..a9501ee7a 100644 --- a/test/fixtures/home_sensor_refresh.cpp +++ b/test/fixtures/home_sensor_refresh.cpp @@ -81,6 +81,10 @@ struct Task { void toggleGPS() { ++toggles; } }; +struct Display { + bool isEink() const { return TEST_EINK; } +} display; + enum class HomePage { OTHER, SENSORS }; struct Home { @@ -97,7 +101,9 @@ struct Home { } }; -static constexpr uint32_t interval_ms = AUTO_OFF_MILLIS > 0 ? 5000u : 60000u; +static constexpr uint32_t interval_ms = TEST_NEW_UI + ? (TEST_EINK ? 60000u : 5000u) + : (AUTO_OFF_MILLIS > 0 ? 5000u : 60000u); static void begin_case(const char* name, uint32_t initial_ms) { active_case = name; 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..329beeed0 --- /dev/null +++ b/test/test_empty_contact_slots/test_empty_contact_slots.cpp @@ -0,0 +1,234 @@ +#include +#include +#include + +#include "helpers/AdvertDataHelpers.cpp" +#include "helpers/BaseChatMesh.cpp" + +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: + unsigned allocations = 0; + mesh::Packet* allocNew() override { ++allocations; return nullptr; } + void free(mesh::Packet*) override {} + bool queueOutbound(mesh::Packet*, uint8_t, uint32_t) override { return false; } + 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 markSent(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 {} + bool processAck(const uint8_t*, ContactInfo*& peer) override { + peer = nullptr; + return false; + } + 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 {} + bool onSignedMessageRecv(const ContactInfo&, mesh::Packet*, uint32_t, const uint8_t*, const char*) override { return true; } + 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 BaseChatMesh::onPeerDataRecv; + 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; + 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; + 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))); + EXPECT_EQ(nullptr, fx.chat.lookupTransientContactByPubKey(zero_prefix, sizeof(zero_prefix))); + EXPECT_EQ(nullptr, fx.chat.lookupPersistentContactByPubKey(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; + 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)); +} + +TEST(EmptyContactSlots, AcceptedContactKeepsPriorityOverMatchingTransientPeer) { + MeshFixture fx; + ContactInfo anon; + anon.id.pub_key[1] = 0x7F; + ASSERT_TRUE(fx.chat.addContact(anon)); + ContactInfo accepted = anon; + accepted.type = ADV_TYPE_CHAT; + accepted.out_path_len = OUT_PATH_UNKNOWN; + strcpy(accepted.name, "Alice"); + ASSERT_TRUE(fx.chat.addContact(accepted)); + const uint8_t zero_hash = 0; + ASSERT_EQ(2, fx.chat.searchPeersByHash(&zero_hash)); + + mesh::Packet packet; + packet.header = ROUTE_TYPE_FLOOD | (PAYLOAD_TYPE_TXT_MSG << PH_TYPE_SHIFT); + packet.path_len = 0; + uint8_t secret[PUB_KEY_SIZE] = {}; + uint8_t text[16] = {}; + text[4] = TXT_TYPE_PLAIN << 2; + strcpy(reinterpret_cast(text + 5), "hello"); + fx.chat.onPeerDataRecv(&packet, PAYLOAD_TYPE_TXT_MSG, 0, secret, text, 11); + EXPECT_EQ(1, fx.chat.messages_received); +} + +TEST(EmptyContactSlots, NegativeGroupTextLengthFailsBeforeReadingInputOrAllocating) { + MeshFixture fx; + mesh::GroupChannel channel; + EXPECT_FALSE(fx.chat.sendGroupMessage(1, channel, nullptr, nullptr, -1)); + EXPECT_EQ(0u, fx.mgr.allocations); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/test/test_home_sensor_refresh.py b/test/test_home_sensor_refresh.py index 7d838bf0a..7b4baaa76 100644 --- a/test/test_home_sensor_refresh.py +++ b/test/test_home_sensor_refresh.py @@ -67,13 +67,14 @@ class HomeSensorRefreshTest(unittest.TestCase): for ui in ('ui-new', 'ui-tiny'): source = work / (ui + '.cpp') source.write_text(harness(ui, mutation), encoding='ascii') - for auto_off in (15000, 0): - with self.subTest(ui=ui, auto_off=auto_off): - binary = work / (ui + '-' + str(auto_off) + + for auto_off, eink in ((15000, False), (15000, True), (0, False), (0, True)): + with self.subTest(ui=ui, auto_off=auto_off, eink=eink): + binary = work / (ui + '-' + str(auto_off) + '-' + str(int(eink)) + ('.exe' if os.name == 'nt' else '')) compiled = subprocess.run([ compiler, '-std=c++17', '-O1', '-g', '-Wall', '-Wextra', '-Werror', *sanitize, '-DAUTO_OFF_MILLIS=' + str(auto_off), + '-DTEST_EINK=' + str(int(eink)), '-DTEST_NEW_UI=' + str(int(ui == 'ui-new')), str(source), '-o', str(binary), ], text=True, capture_output=True, timeout=60) diff --git a/test/test_routing_policy/test_routing_policy.cpp b/test/test_routing_policy/test_routing_policy.cpp index 08903c08d..91d24dfcf 100644 --- a/test/test_routing_policy/test_routing_policy.cpp +++ b/test/test_routing_policy/test_routing_policy.cpp @@ -12,6 +12,9 @@ static Packet makeFlood(uint8_t route_type, uint8_t payload_type, uint8_t hops) p.transport_codes[1] = 0; 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; } diff --git a/test/test_upstream_cli_bounds.py b/test/test_upstream_cli_bounds.py new file mode 100644 index 000000000..cd5f9d1c9 --- /dev/null +++ b/test/test_upstream_cli_bounds.py @@ -0,0 +1,285 @@ +"""Exercise upstream sensor-list bounds and duplicate-stat wire saturation.""" +from pathlib import Path +import re +import shutil +import subprocess +import sys +import tempfile +import unittest + +from test_replay_reset_integration import extract_braced + + +ROOT = Path(__file__).resolve().parents[1] + +SENSOR_HARNESS = r''' +#include +#include +#include +#include +#include +#include +#include +#include + +@ATOI@ + +struct Setting { const char* name; const char* value; }; +struct Sensors { + std::vector settings; + int reads = 0; + int getNumSettings() const { return static_cast(settings.size()); } + const char* getSettingName(int i) { + assert(i >= 0 && i < getNumSettings()); + ++reads; + return settings[i].name; + } + const char* getSettingValue(int i) { + assert(i >= 0 && i < getNumSettings()); + ++reads; + return settings[i].value; + } +}; +struct Fixture { + Sensors* _sensors; + void handle(char* command, char* reply) { + @SENSOR_LIST@ + else { assert(false && "fixture accepts only sensor list"); } + } +}; + +static std::string page(Fixture& fixture, int start = -1) { + // A long trailing guard also makes the old unbounded write fail our own + // assertion, without depending on a particular platform's stack layout. + std::array guarded; + guarded.fill(0xA5); + char* reply = reinterpret_cast(guarded.data() + 32); + std::memset(reply, 0, 160); + std::string text = "sensor list"; + if (start >= 0) text += " " + std::to_string(start); + std::vector command(text.begin(), text.end()); + command.push_back(0); + fixture.handle(command.data(), reply); + for (size_t i = 0; i < 32; ++i) assert(guarded[i] == 0xA5); + for (size_t i = 32 + 160; i < guarded.size(); ++i) assert(guarded[i] == 0xA5); + assert(std::memchr(reply, 0, 160) != nullptr); + return reply; +} + +static int nextPage(const std::string& text) { + const size_t marker = text.find("... next:"); + return marker == std::string::npos ? -1 : std::atoi(text.c_str() + marker + 9); +} + +int main(int argc, char** argv) { + assert(argc == 2); + const std::string scenario = argv[1]; + if (scenario == "nulls") { + Sensors sensors{{{nullptr, nullptr}, {"enabled", nullptr}, {nullptr, "ok"}}}; + Fixture fixture{&sensors}; + assert(page(fixture) == "3 vars\n?=\nenabled=\n?=ok"); + sensors.settings = {{"", ""}, {"name", "1"}}; + assert(page(fixture) == "2 vars\n=\nname=1"); + sensors.settings.clear(); sensors.reads = 0; + assert(page(fixture) == "no custom var" && sensors.reads == 0); + sensors.settings = {{"one", "1"}}; + assert(page(fixture, 1) == "no custom var"); + assert(page(fixture, 999) == "no custom var" && sensors.reads == 0); + } else if (scenario == "oversized") { + const std::string name(1024, 'n'), value(3072, 'v'); + Sensors sensors{{{name.c_str(), value.c_str()}}}; + Fixture fixture{&sensors}; + const std::string output = page(fixture); + assert(output.size() < 160 && output.find("1 vars\n") == 0); + assert(nextPage(output) == -1); + } else if (scenario == "pagination") { + std::vector names, values; + names.reserve(30); values.reserve(30); + Sensors sensors; + for (int i = 0; i < 30; ++i) { + names.push_back("sensor" + std::to_string(i)); + values.push_back(std::string(20, 'a' + i % 26)); + sensors.settings.push_back({names.back().c_str(), values.back().c_str()}); + } + Fixture fixture{&sensors}; + int start = 0, pages = 0; + std::string all; + do { + const std::string output = page(fixture, start); + assert(output.find("30 vars\n") == 0); + all += output + "\n"; + const int next = nextPage(output); + if (next < 0) break; + assert(next > start && next < 30 && ++pages < 30); + start = next; + } while (true); + assert(pages > 1); + for (size_t i = 0; i < names.size(); ++i) { + const std::string row = names[i] + "=" + values[i] + "\n"; + const size_t found = all.find(row); + assert(found != std::string::npos); + assert(all.find(row, found + row.size()) == std::string::npos); + } + // An oversized entry encountered after a fitting entry is deferred, then + // consumed once on the next page, so it cannot trap the paging cursor. + const std::string huge(2048, 'z'); + sensors.settings = {{"first", "ok"}, {"large", huge.c_str()}, {"last", "ok"}}; + const std::string first = page(fixture, 0); + assert(first.find("first=ok\n") != std::string::npos && nextPage(first) == 1); + assert(nextPage(page(fixture, 1)) == 2); + assert(page(fixture, 2) == "3 vars\nlast=ok"); + } else if (scenario == "boundaries") { + // Move each entry across every remaining-space and truncation boundary. + for (size_t length = 0; length <= 260; ++length) { + const std::string value(length, 'x'); + Sensors sensors{{{"edge", value.c_str()}, {"tail", "done"}}}; + Fixture fixture{&sensors}; + const std::string first = page(fixture); + const int next = nextPage(first); + if (next >= 0) { + assert(next == 1); + assert(page(fixture, next) == "2 vars\ntail=done"); + } else { + assert(first.find("tail=done") != std::string::npos); + } + } + } else { assert(false && "unknown scenario"); } + return 0; +} +''' + +STATS_HARNESS = r''' +#include +#include +#include +#include + +@STRUCT@; +using WireStats = @STATS_TYPE@; +static_assert(sizeof(WireStats::n_direct_dups) == 2, "direct duplicate wire width"); +static_assert(sizeof(WireStats::n_flood_dups) == 2, "flood duplicate wire width"); +struct SimpleMeshTables { + uint32_t direct, flood; + uint32_t getNumDirectDups() const { return direct; } + uint32_t getNumFloodDups() const { return flood; } +}; +struct Fixture { + SimpleMeshTables tables; + void* getTables() { return &tables; } + WireStats read() { + WireStats stats{}; + @ASSIGNMENTS@ + return stats; + } +}; +int main() { + const uint32_t values[] = {0, 1, 65530, 65534, 65535, 65536, 65537, 131065, UINT32_MAX}; + for (const uint32_t direct : values) for (const uint32_t flood : values) { + Fixture fixture{{direct, flood}}; + const WireStats stats = fixture.read(); + assert(stats.n_direct_dups == (direct > 65535 ? 65535 : direct)); + assert(stats.n_flood_dups == (flood > 65535 ? 65535 : flood)); + assert(fixture.tables.direct == direct && fixture.tables.flood == flood); + unsigned char wire[sizeof(stats)]; + std::memcpy(wire, &stats, sizeof(stats)); + WireStats decoded{}; + std::memcpy(&decoded, wire, sizeof(decoded)); + assert(decoded.n_direct_dups == stats.n_direct_dups); + assert(decoded.n_flood_dups == stats.n_flood_dups); + } + return 0; +} +''' + + +class UpstreamCliBoundsTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.compiler = shutil.which("g++") or shutil.which("clang++") + if not cls.compiler: + raise unittest.SkipTest("a host C++17 compiler is required") + cls.temporary = tempfile.TemporaryDirectory(prefix="meshcore-upstream-bounds-") + cls.addClassCleanup(cls.temporary.cleanup) + cls.work = Path(cls.temporary.name) + source = (ROOT / "src/helpers/CommonCLI.cpp").read_text(encoding="utf-8") + cls.sensor_code = SENSOR_HARNESS.replace( + "@ATOI@", extract_braced(source, "static uint32_t _atoi(")) + cls.sensor_code = cls.sensor_code.replace( + "@SENSOR_LIST@", extract_braced(source, 'if (memcmp(command, "sensor list", 11) == 0)')) + cls.sensor = cls.compile(cls.sensor_code, "sensors") + + @classmethod + def compile(cls, code, name): + source = cls.work / (name + ".cpp") + binary = cls.work / (name + (".exe" if sys.platform == "win32" else "")) + source.write_text(code, encoding="utf-8") + sanitizers = (["-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-no-pie"] + if sys.platform.startswith("linux") else []) + built = subprocess.run([cls.compiler, "-std=c++17", "-O1", "-g", "-Wall", "-Wextra", + "-Werror", *sanitizers, str(source), "-o", str(binary)], + capture_output=True, text=True, timeout=60) + if built.returncode: + raise AssertionError(built.stdout + built.stderr) + return binary + + def run_binary(self, binary, *arguments, expect_failure=False): + result = subprocess.run([str(binary), *arguments], capture_output=True, text=True, timeout=30) + if expect_failure: + self.assertNotEqual(result.returncode, 0, "unsafe negative control unexpectedly passed") + else: + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_sensor_list_handles_null_empty_and_exhausted_entries(self): + self.run_binary(self.sensor, "nulls") + + def test_sensor_list_truncates_one_oversized_entry_without_overflow(self): + self.run_binary(self.sensor, "oversized") + + def test_sensor_list_paginates_without_losing_entries_or_stalling(self): + self.run_binary(self.sensor, "pagination") + + def test_sensor_list_remaining_space_boundaries(self): + self.run_binary(self.sensor, "boundaries") + + def test_previous_unbounded_sensor_write_is_detected(self): + unsafe = self.sensor_code.replace('snprintf(dp, room, "%s=%s\\n", name, value);', + 'sprintf(dp, "%s=%s\\n", name, value);') + self.assertNotEqual(unsafe, self.sensor_code) + self.run_binary(self.compile(unsafe, "unsafe-sensors"), "oversized", expect_failure=True) + + def stats_code(self, role, unsafe=False): + source = (ROOT / "examples" / role / "MyMesh.cpp").read_text(encoding="utf-8") + if role == "simple_repeater": + struct_source = (ROOT / "examples" / role / "MyMesh.h").read_text(encoding="utf-8") + stats_type = "RepeaterStats" + else: + struct_source = source + stats_type = "ServerStats" + request = extract_braced(source, "int MyMesh::handleRequest(") + status = extract_braced(request, "if (payload[0] == REQ_TYPE_GET_STATUS)") + assignments = [] + for field in ("direct", "flood"): + match = re.search(r"stats\.n_" + field + r"_dups\s*=[^;]+;", status) + self.assertIsNotNone(match, role + " " + field) + text = match.group() + if unsafe: + text = re.sub(r"std::min\((.*), 0xFFFF\)", r"\1", text) + self.assertNotEqual(text, match.group()) + assignments.append(text) + return (STATS_HARNESS.replace("@STRUCT@", extract_braced(struct_source, "struct " + stats_type)) + .replace("@STATS_TYPE@", stats_type).replace("@ASSIGNMENTS@", "\n".join(assignments))) + + def test_duplicate_stats_saturate_both_roles_at_the_wire_boundary(self): + for role in ("simple_repeater", "simple_room_server"): + with self.subTest(role=role): + self.run_binary(self.compile(self.stats_code(role), role + "-stats")) + + def test_previous_wrapping_duplicate_stats_are_detected(self): + for role in ("simple_repeater", "simple_room_server"): + with self.subTest(role=role): + self.run_binary(self.compile(self.stats_code(role, unsafe=True), role + "-unsafe-stats"), + expect_failure=True) + + +if __name__ == "__main__": + unittest.main() diff --git a/variants/lilygo_techo/platformio.ini b/variants/lilygo_techo/platformio.ini index 2b678fcbd..49b2788b5 100644 --- a/variants/lilygo_techo/platformio.ini +++ b/variants/lilygo_techo/platformio.ini @@ -32,6 +32,9 @@ build_flags = ${rak4631_hw.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 UI_RECENT_LIST_SIZE=9 + -D UI_MSG_PREVIEW_SIZE=165 -D BACKLIGHT_BTN=PIN_BUTTON2 -D AUTO_OFF_MILLIS=0 build_src_filter = ${rak4631_hw.build_src_filter} 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 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 d22848a99..6b1b7685d 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(); @@ -50,17 +47,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 shutdownPeripherals() override { NRF52Board::shutdownPeripherals(); 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 195e662c4..73fb742a2 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: @@ -51,15 +50,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 e47786d9c..f93fe142c 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(); @@ -46,17 +43,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 shutdownPeripherals() override { NRF52Board::shutdownPeripherals(); diff --git a/variants/thinknode_m1/platformio.ini b/variants/thinknode_m1/platformio.ini index c5c2bbc80..965391bda 100644 --- a/variants/thinknode_m1/platformio.ini +++ b/variants/thinknode_m1/platformio.ini @@ -25,6 +25,11 @@ build_flags = ${rak4631_hw.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_DISPLAY_MODEL=GxEPD2_154_D67 + -D BACKLIGHT_BTN=PIN_BUTTON2 + -D DISABLE_DIAGNOSTIC_OUTPUT build_src_filter = ${rak4631_hw.build_src_filter} + + diff --git a/variants/thinknode_m1/variant.h b/variants/thinknode_m1/variant.h index 3399253c9..ceee88ff6 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 @@ -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 diff --git a/variants/thinknode_m3/ThinkNodeM3Board.cpp b/variants/thinknode_m3/ThinkNodeM3Board.cpp index 3f5eb8e7f..1ddcd3ef2 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 87922be82..3641f0517 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") {} @@ -38,17 +37,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 shutdownPeripherals() override { NRF52Board::shutdownPeripherals(); 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 72a9d97b4..56cac1732 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; @@ -37,17 +36,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(); diff --git a/variants/thinknode_m5/platformio.ini b/variants/thinknode_m5/platformio.ini index a73a790f6..aafd16020 100644 --- a/variants/thinknode_m5/platformio.ini +++ b/variants/thinknode_m5/platformio.ini @@ -26,6 +26,7 @@ build_flags = ${esp32_base.build_flags} -D BACKLIGHT_BTN=PIN_BUTTON2 -D AUTO_OFF_MILLIS=0 -D UI_RECENT_LIST_SIZE=9 + -D UI_MSG_PREVIEW_SIZE=165 -D DISABLE_DIAGNOSTIC_OUTPUT -D SX126X_DIO2_AS_RF_SWITCH=true -D SX126X_DIO3_TCXO_VOLTAGE=3.3 @@ -174,12 +175,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 diff --git a/variants/thinknode_m8/platformio.ini b/variants/thinknode_m8/platformio.ini index f50c7d714..f63da86fc 100644 --- a/variants/thinknode_m8/platformio.ini +++ b/variants/thinknode_m8/platformio.ini @@ -36,7 +36,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 =