diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 5d54ad869..348916ca5 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1211,7 +1211,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++]; @@ -2228,7 +2230,8 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* } if (strcmp(command, "get tz.offset") == 0) { - sprintf(reply, "> %.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) { 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/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index d48647b3d..aced739dc 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; @@ -910,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(); } @@ -957,7 +957,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) { @@ -1002,13 +1004,31 @@ 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(); + _display->powerOff(); + 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 +1076,12 @@ 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(); + _display->powerOff(); + } + #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/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 diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index e8d725f25..7945ef74a 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -258,8 +258,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 ffa24617b..ba13dc62b 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 @@ -193,8 +194,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/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 diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index c78d1b64b..b03ddc5a7 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -167,6 +167,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!! @@ -243,6 +244,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) } @@ -584,6 +590,11 @@ int BaseChatMesh::sendCommandData(ContactInfo& recipient, uint32_t timestamp, u } 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 @@ -916,6 +927,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 @@ -924,6 +936,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 @@ -941,7 +954,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/CommonCLI.h b/src/helpers/CommonCLI.h index 3d218aead..33c8c95ae 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -116,8 +116,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.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); 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/src/helpers/ContactInfo.h b/src/helpers/ContactInfo.h index f93fea77a..5f431bdaf 100644 --- a/src/helpers/ContactInfo.h +++ b/src/helpers/ContactInfo.h @@ -34,6 +34,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/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 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; } 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); } 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/DisplayDriver.h b/src/helpers/ui/DisplayDriver.h index 3e9e2dde8..58147aa0e 100644 --- a/src/helpers/ui/DisplayDriver.h +++ b/src/helpers/ui/DisplayDriver.h @@ -26,6 +26,8 @@ public: virtual void forceFullRefresh() {} // next refresh will be full for eink virtual void turnOn() = 0; virtual void turnOff() = 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; diff --git a/src/helpers/ui/GxEPDDisplay.cpp b/src/helpers/ui/GxEPDDisplay.cpp index 332157eb8..ce09423e1 100644 --- a/src/helpers/ui/GxEPDDisplay.cpp +++ b/src/helpers/ui/GxEPDDisplay.cpp @@ -75,20 +75,27 @@ 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) expander.digitalWrite(EXP_PIN_BACKLIGHT, LOW); #endif _isOn = false; + last_display_crc_value=0; +} + +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..462744026 100644 --- a/src/helpers/ui/GxEPDDisplay.h +++ b/src/helpers/ui/GxEPDDisplay.h @@ -60,6 +60,8 @@ public: void resetPartialRefreshCounter() { _cycles_before_full_refresh = max_partial_refresh; }; void turnOn() override; void turnOff() override; + void blankScreen() override; + void powerOff() override; void clear() override; void startFrame(ColorVal bkg = UIColor::window_bkg) override; void setTextSize(int sz) override; 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/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(); +} 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; } 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 = 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> diff --git a/variants/lilygo_techo/platformio.ini b/variants/lilygo_techo/platformio.ini index 81da2e48c..ba39549e1 100644 --- a/variants/lilygo_techo/platformio.ini +++ b/variants/lilygo_techo/platformio.ini @@ -32,8 +32,17 @@ 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 + -D EINK_MAX_PARTIAL_REFRESH=60 + -D AUTO_OFF_MILLIS=900000 -D BACKLIGHT_BTN=PIN_BUTTON2 - -D AUTO_OFF_MILLIS=0 build_src_filter = ${nrf52_base.build_src_filter} + + @@ -91,7 +100,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 +124,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/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 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/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; 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_m1/platformio.ini b/variants/thinknode_m1/platformio.ini index 89c48403b..7c620e2ed 100644 --- a/variants/thinknode_m1/platformio.ini +++ b/variants/thinknode_m1/platformio.ini @@ -24,6 +24,18 @@ 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 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 + -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_m1/variant.h b/variants/thinknode_m1/variant.h index f7f52a6a5..aecf197e6 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 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(); diff --git a/variants/thinknode_m5/platformio.ini b/variants/thinknode_m5/platformio.ini index a3f348b33..a9761597d 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=900000 + -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 @@ -174,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 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..5282f280f 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=900000 + -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 @@ -40,6 +44,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 = @@ -92,10 +97,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 @@ -110,7 +112,6 @@ build_src_filter = ${ThinkNode_M8.build_src_filter} + + + - + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = @@ -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 @@ -144,7 +143,6 @@ build_src_filter = ${ThinkNode_M8.build_src_filter} + + + - + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = 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