From ecfaf3a095b352021eb7731667284863f18d2ce4 Mon Sep 17 00:00:00 2001 From: todd-herbert Date: Wed, 25 Jun 2025 23:04:18 +1200 Subject: [PATCH 01/25] Canned Messages via InkHUD menu (#7096) * Allow observers to respond to AdminMessage requests Ground work for CannedMessage getters and setters * Enable CannedMessage config in apps for InkHUD devices * Migrate the InkHUD::Events AdminModule observer Use the new AdminModule_ObserverData struct * Bare-bones NicheGraphics util to access canned messages Handles loading and parsing. Handle admin messages for setting and getting. * Send canned messages via on-screen menu * Change ThreadedMessageApplet from Observer to Module API Allows us to intercept locally generated packets ('loopbackOK = true'), to handle outgoing canned messages. * Fix: crash getting empty canned message string via Client API * Move file into Utils subdir * Move an include statement from .cpp to .h * Limit strncpy size of dest, not source Wasn't critical in ths specific case, but definitely a mistake. --- src/graphics/Screen.cpp | 6 +- src/graphics/Screen.h | 7 +- .../InkHUD/Applets/System/Menu/MenuAction.h | 2 + .../InkHUD/Applets/System/Menu/MenuApplet.cpp | 170 +++++++++++++++++- .../InkHUD/Applets/System/Menu/MenuApplet.h | 41 ++++- .../InkHUD/Applets/System/Menu/MenuPage.h | 1 + .../ThreadedMessage/ThreadedMessageApplet.cpp | 40 ++--- .../ThreadedMessage/ThreadedMessageApplet.h | 9 +- src/graphics/niche/InkHUD/Events.cpp | 10 +- src/graphics/niche/InkHUD/Events.h | 8 +- src/graphics/niche/InkHUD/Persistence.h | 2 +- .../niche/Utils/CannedMessageStore.cpp | 163 +++++++++++++++++ src/graphics/niche/Utils/CannedMessageStore.h | 54 ++++++ src/graphics/niche/{ => Utils}/FlashData.h | 0 src/main.cpp | 2 +- src/modules/AdminModule.cpp | 26 ++- src/modules/AdminModule.h | 11 +- 17 files changed, 498 insertions(+), 54 deletions(-) create mode 100644 src/graphics/niche/Utils/CannedMessageStore.cpp create mode 100644 src/graphics/niche/Utils/CannedMessageStore.h rename src/graphics/niche/{ => Utils}/FlashData.h (100%) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index b2087bf4e..0818619a6 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -58,7 +58,6 @@ along with this program. If not, see . #include "mesh/Channels.h" #include "mesh/generated/meshtastic/deviceonly.pb.h" #include "meshUtils.h" -#include "modules/AdminModule.h" #include "modules/ExternalNotificationModule.h" #include "modules/TextMessageModule.h" #include "modules/WaypointModule.h" @@ -1377,12 +1376,13 @@ int Screen::handleInputEvent(const InputEvent *event) return 0; } -int Screen::handleAdminMessage(const meshtastic_AdminMessage *arg) +int Screen::handleAdminMessage(AdminModule_ObserverData *arg) { - switch (arg->which_payload_variant) { + switch (arg->request->which_payload_variant) { // Node removed manually (i.e. via app) case meshtastic_AdminMessage_remove_by_nodenum_tag: setFrames(FOCUS_PRESERVE); + *arg->result = AdminMessageHandleResult::HANDLED; break; // Default no-op, in case the admin message observable gets used by other classes in future diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index c264f0f07..8a836edfc 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -78,6 +78,7 @@ class Screen #include "concurrency/OSThread.h" #include "input/InputBroker.h" #include "mesh/MeshModule.h" +#include "modules/AdminModule.h" #include "power.h" #include #include @@ -193,8 +194,8 @@ class Screen : public concurrency::OSThread CallbackObserver(this, &Screen::handleUIFrameEvent); // Sent by Mesh Modules CallbackObserver inputObserver = CallbackObserver(this, &Screen::handleInputEvent); - CallbackObserver adminMessageObserver = - CallbackObserver(this, &Screen::handleAdminMessage); + CallbackObserver adminMessageObserver = + CallbackObserver(this, &Screen::handleAdminMessage); public: explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY); @@ -544,7 +545,7 @@ class Screen : public concurrency::OSThread int handleTextMessage(const meshtastic_MeshPacket *arg); int handleUIFrameEvent(const UIFrameEvent *arg); int handleInputEvent(const InputEvent *arg); - int handleAdminMessage(const meshtastic_AdminMessage *arg); + int handleAdminMessage(AdminModule_ObserverData *arg); /// Used to force (super slow) eink displays to draw critical frames void forceDisplay(bool forceUiUpdate = false); diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h index f162aa385..f42b9dc2c 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h @@ -19,6 +19,8 @@ namespace NicheGraphics::InkHUD enum MenuAction { NO_ACTION, SEND_PING, + STORE_CANNEDMESSAGE_SELECTION, + SEND_CANNEDMESSAGE, SHUTDOWN, NEXT_TILE, TOGGLE_BACKLIGHT, diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp index 9fdfad8ee..69965972f 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp @@ -5,6 +5,7 @@ #include "RTC.h" #include "MeshService.h" +#include "Router.h" #include "airtime.h" #include "main.h" #include "power.h" @@ -31,6 +32,12 @@ InkHUD::MenuApplet::MenuApplet() : concurrency::OSThread("MenuApplet") if (settings->optionalMenuItems.backlight) { backlight = Drivers::LatchingBacklight::getInstance(); } + + // Initialize the Canned Message store + // This is a shared nicheGraphics component + // - handles loading & parsing the canned messages + // - handles setting / getting of canned messages via apps (Client API Admin Messages) + cm.store = CannedMessageStore::getInstance(); } void InkHUD::MenuApplet::onForeground() @@ -65,6 +72,10 @@ void InkHUD::MenuApplet::onForeground() void InkHUD::MenuApplet::onBackground() { + // Discard any data we generated while selecting a canned message + // Frees heap mem + freeCannedMessageResources(); + // If device has a backlight which isn't controlled by aux button: // Item in options submenu allows keeping backlight on after menu is closed // If this item is deselected we will turn backlight off again, now that menu is closing @@ -153,6 +164,16 @@ void InkHUD::MenuApplet::execute(MenuItem item) inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL); break; + case STORE_CANNEDMESSAGE_SELECTION: + cm.selectedMessageItem = &cm.messageItems.at(cursor - 1); // Minus one: offset for the initial "Send Ping" entry + break; + + case SEND_CANNEDMESSAGE: + cm.selectedRecipientItem = &cm.recipientItems.at(cursor); + sendText(cm.selectedRecipientItem->dest, cm.selectedRecipientItem->channelIndex, cm.selectedMessageItem->rawText.c_str()); + inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL); // Next refresh should be FULL. Lots of button pressing to get here + break; + case ROTATE: inkhud->rotate(); break; @@ -260,9 +281,11 @@ void InkHUD::MenuApplet::showPage(MenuPage page) break; case SEND: - items.push_back(MenuItem("Ping", MenuAction::SEND_PING, MenuPage::EXIT)); - // Todo: canned messages - items.push_back(MenuItem("Exit", MenuPage::EXIT)); + populateSendPage(); + break; + + case CANNEDMESSAGE_RECIPIENT: + populateRecipientPage(); break; case OPTIONS: @@ -497,6 +520,8 @@ void InkHUD::MenuApplet::populateAutoshowPage() } } +// Create MenuItem entries to select our definition of "Recent" +// Controls how long data will remain in any "Recents" flavored applets void InkHUD::MenuApplet::populateRecentsPage() { // How many values are shown for use to choose from @@ -510,6 +535,112 @@ void InkHUD::MenuApplet::populateRecentsPage() } } +// MenuItem entries for the "send" page +// Dynamically creates menu items based on available canned messages +void InkHUD::MenuApplet::populateSendPage() +{ + // Position / NodeInfo packet + items.push_back(MenuItem("Ping", MenuAction::SEND_PING, MenuPage::EXIT)); + + // One menu item for each canned message + uint8_t count = cm.store->size(); + for (uint8_t i = 0; i < count; i++) { + // Gather the information for this item + CannedMessages::MessageItem messageItem; + messageItem.rawText = cm.store->at(i); + messageItem.label = parse(messageItem.rawText); + + // Store the item (until the menu closes) + cm.messageItems.push_back(messageItem); + + // Create a menu item + const char *itemText = cm.messageItems.back().label.c_str(); + items.push_back(MenuItem(itemText, MenuAction::STORE_CANNEDMESSAGE_SELECTION, MenuPage::CANNEDMESSAGE_RECIPIENT)); + } + + items.push_back(MenuItem("Exit", MenuPage::EXIT)); +} + +// Dynamically create MenuItem entries for possible canned message destinations +// All available channels are shown +// Favorite nodes are shown, provided we don't have an *excessive* amount +void InkHUD::MenuApplet::populateRecipientPage() +{ + // Create recipient data (and menu items) for any channels + // -------------------------------------------------------- + + for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) { + // Get the channel, and check if it's enabled + meshtastic_Channel &channel = channels.getByIndex(i); + if (!channel.has_settings || channel.role == meshtastic_Channel_Role_DISABLED) + continue; + + CannedMessages::RecipientItem r; + + // Set index + r.channelIndex = channel.index; + + // Set a label for the menu item + r.label = "Ch " + to_string(i) + ": "; + if (channel.role == meshtastic_Channel_Role_PRIMARY) + r.label += "Primary"; + else + r.label += parse(channel.settings.name); + + // Add to the list of recipients + cm.recipientItems.push_back(r); + + // Add a menu item for this recipient + const char *itemText = cm.recipientItems.back().label.c_str(); + items.push_back(MenuItem(itemText, SEND_CANNEDMESSAGE, MenuPage::EXIT)); + } + + // Create recipient data (and menu items) for favorite nodes + // --------------------------------------------------------- + + uint32_t nodeCount = nodeDB->getNumMeshNodes(); + uint32_t favoriteCount = 0; + + // Count favorites + for (uint32_t i = 0; i < nodeCount; i++) { + if (nodeDB->getMeshNodeByIndex(i)->is_favorite) + favoriteCount++; + } + + // Only add favorites if the number is reasonable + // Don't want some monstrous list that takes 100 clicks to reach exit + if (favoriteCount < 20) { + for (uint32_t i = 0; i < nodeCount; i++) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); + + // Skip node if not a favorite + if (!node->is_favorite) + continue; + + CannedMessages::RecipientItem r; + + r.dest = node->num; + r.channelIndex = nodeDB->getMeshNodeChannel(node->num); // Channel index only relevant if encrypted DM not possible(?) + + // Set a label for the menu item + r.label = "DM: "; + if (node->has_user) + r.label += parse(node->user.long_name); + else + r.label += hexifyNodeNum(node->num); // Unsure if it's possible to favorite a node without NodeInfo? + + // Add to the list of recipients + cm.recipientItems.push_back(r); + + // Add a menu item for this recipient + const char *itemText = cm.recipientItems.back().label.c_str(); + items.push_back(MenuItem(itemText, SEND_CANNEDMESSAGE, MenuPage::EXIT)); + } + } + + items.push_back(MenuItem("Exit", MenuPage::EXIT)); +} + // Renders the panel shown at the top of the root menu. // Displays the clock, and several other pieces of instantaneous system info, // which we'd prefer not to have displayed in a normal applet, as they update too frequently. @@ -619,4 +750,37 @@ uint16_t InkHUD::MenuApplet::getSystemInfoPanelHeight() return height; } +// Send a text message to the mesh +// Used to send our canned messages +void InkHUD::MenuApplet::sendText(NodeNum dest, ChannelIndex channel, const char *message) +{ + meshtastic_MeshPacket *p = router->allocForSending(); + p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP; + p->to = dest; + p->channel = channel; + p->want_ack = true; + p->decoded.payload.size = strlen(message); + memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size); + + // Tack on a bell character if requested + if (moduleConfig.canned_message.send_bell && p->decoded.payload.size < meshtastic_Constants_DATA_PAYLOAD_LEN) { + p->decoded.payload.bytes[p->decoded.payload.size] = 7; // Bell character + p->decoded.payload.bytes[p->decoded.payload.size + 1] = '\0'; // Append Null Terminator + p->decoded.payload.size++; + } + + LOG_INFO("Send message id=%d, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes); + + service->sendToMesh(p, RX_SRC_LOCAL, true); // Send to mesh, cc to phone +} + +// Free up any heap mmemory we'd used while selecting / sending canned messages +void InkHUD::MenuApplet::freeCannedMessageResources() +{ + cm.selectedMessageItem = nullptr; + cm.selectedRecipientItem = nullptr; + cm.messageItems.clear(); + cm.recipientItems.clear(); +} + #endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h index d9297c8ed..4c974672a 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h @@ -6,10 +6,12 @@ #include "graphics/niche/InkHUD/InkHUD.h" #include "graphics/niche/InkHUD/Persistence.h" #include "graphics/niche/InkHUD/SystemApplet.h" +#include "graphics/niche/Utils/CannedMessageStore.h" #include "./MenuItem.h" #include "./MenuPage.h" +#include "Channels.h" #include "concurrency/OSThread.h" namespace NicheGraphics::InkHUD @@ -36,12 +38,18 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread void execute(MenuItem item); // Perform the MenuAction associated with a MenuItem, if any void showPage(MenuPage page); // Load and display a MenuPage + + void populateSendPage(); // Dynamically create MenuItems including canned messages + void populateRecipientPage(); // Dynamically create a page of possible destinations for a canned message void populateAppletPage(); // Dynamically create MenuItems for toggling loaded applets void populateAutoshowPage(); // Dynamically create MenuItems for selecting which applets can autoshow void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds + uint16_t getSystemInfoPanelHeight(); void drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width, - uint16_t *height = nullptr); // Info panel at top of root menu + uint16_t *height = nullptr); // Info panel at top of root menu + void sendText(NodeNum dest, ChannelIndex channel, const char *message); // Send a text message to mesh + void freeCannedMessageResources(); // Clear MenuApplet's canned message processing data MenuPage currentPage = MenuPage::ROOT; uint8_t cursor = 0; // Which menu item is currently highlighted @@ -51,6 +59,37 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread std::vector items; // MenuItems for the current page. Filled by ShowPage + // Data for selecting and sending canned messages via the menu + // Placed into a sub-class for organization only + class CannedMessages + { + public: + // Share NicheGraphics component + // Handles loading, getting, setting + CannedMessageStore *store; + + // One canned message + // Links the menu item to the true message text + struct MessageItem { + std::string label; // Shown in menu. Prefixed, and UTF-8 chars parsed + std::string rawText; // The message which will be sent, if this item is selected + } *selectedMessageItem; + + // One possible destination for a canned message + // Links the menu item to the intended recipient + // May represent either broadcast or DM + struct RecipientItem { + std::string label; // Shown in menu + NodeNum dest = NODENUM_BROADCAST; + uint8_t channelIndex = 0; + } *selectedRecipientItem; + + // These lists are generated when the menu page is populated + // Cleared onBackground (when MenuApplet closes) + std::vector messageItems; + std::vector recipientItems; + } cm; + Applet *borrowedTileOwner = nullptr; // Which applet we have temporarily replaced while displaying menu }; diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h index d2314e83b..389e411c3 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuPage.h @@ -18,6 +18,7 @@ namespace NicheGraphics::InkHUD enum MenuPage : uint8_t { ROOT, // Initial menu page SEND, + CANNEDMESSAGE_RECIPIENT, // Select destination for a canned message OPTIONS, APPLETS, AUTOSHOW, diff --git a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp index d5d7f77f8..fdb5a168d 100644 --- a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp @@ -13,7 +13,8 @@ using namespace NicheGraphics; constexpr uint8_t MAX_MESSAGES_SAVED = 10; constexpr uint32_t MAX_MESSAGE_SIZE = 250; -InkHUD::ThreadedMessageApplet::ThreadedMessageApplet(uint8_t channelIndex) : channelIndex(channelIndex) +InkHUD::ThreadedMessageApplet::ThreadedMessageApplet(uint8_t channelIndex) + : SinglePortModule("ThreadedMessageApplet", meshtastic_PortNum_TEXT_MESSAGE_APP), channelIndex(channelIndex) { // Create the message store // Will shortly attempt to load messages from RAM, if applet is active @@ -69,9 +70,8 @@ void InkHUD::ThreadedMessageApplet::onRender() // Grab data for message MessageStore::Message &m = store->messages.at(i); - bool outgoing = (m.sender == 0); - meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(m.sender); - std::string bodyText = parse(m.text); // Parse any non-ascii chars in the message + bool outgoing = (m.sender == 0) || (m.sender == myNodeInfo.my_node_num); // Own NodeNum if canned message + std::string bodyText = parse(m.text); // Parse any non-ascii chars in the message // Cache bottom Y of message text // - Used when drawing vertical line alongside @@ -171,54 +171,54 @@ void InkHUD::ThreadedMessageApplet::onRender() void InkHUD::ThreadedMessageApplet::onActivate() { loadMessagesFromFlash(); - textMessageObserver.observe(textMessageModule); // Begin handling any new text messages with onReceiveTextMessage + loopbackOk = true; // Allow us to handle messages generated on the node (canned messages) } // Code which runs when the applet stop running -// This might be happen at shutdown, or if user disables the applet at run-time +// This might be at shutdown, or if the user disables the applet at run-time, via the menu void InkHUD::ThreadedMessageApplet::onDeactivate() { - textMessageObserver.unobserve(textMessageModule); // Stop handling any new text messages with onReceiveTextMessage + loopbackOk = false; // Slightly reduce our impact if the applet is disabled } // Handle new text messages // These might be incoming, from the mesh, or outgoing from phone // Each instance of the ThreadMessageApplet will only listen on one specific channel -// Method should return 0, to indicate general success to TextMessageModule -int InkHUD::ThreadedMessageApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p) +ProcessMessage InkHUD::ThreadedMessageApplet::handleReceived(const meshtastic_MeshPacket &mp) { // Abort if applet fully deactivated - // Already handled by onActivate and onDeactivate, but good practice for all applets if (!isActive()) - return 0; + return ProcessMessage::CONTINUE; // Abort if wrong channel - if (p->channel != this->channelIndex) - return 0; + if (mp.channel != this->channelIndex) + return ProcessMessage::CONTINUE; // Abort if message was a DM - if (p->to != NODENUM_BROADCAST) - return 0; + if (mp.to != NODENUM_BROADCAST) + return ProcessMessage::CONTINUE; // Extract info into our slimmed-down "StoredMessage" type MessageStore::Message newMessage; newMessage.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time - newMessage.sender = p->from; - newMessage.channelIndex = p->channel; - newMessage.text = std::string(&p->decoded.payload.bytes[0], &p->decoded.payload.bytes[p->decoded.payload.size]); + newMessage.sender = mp.from; + newMessage.channelIndex = mp.channel; + newMessage.text = std::string((const char *)mp.decoded.payload.bytes, mp.decoded.payload.size); // Store newest message at front // These records are used when rendering, and also stored in flash at shutdown store->messages.push_front(newMessage); // If this was an incoming message, suggest that our applet becomes foreground, if permitted - if (getFrom(p) != nodeDB->getNodeNum()) + if (getFrom(&mp) != nodeDB->getNodeNum()) requestAutoshow(); // Redraw the applet, perhaps. requestUpdate(); // Want to update display, if applet is foreground - return 0; + // Tell Module API to continue informing other firmware components about this message + // We're not the only component which is interested in new text messages + return ProcessMessage::CONTINUE; } // Don't show notifications for text messages broadcast to our channel, when the applet is displayed diff --git a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h index 3e11a25f2..c986539b3 100644 --- a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h +++ b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h @@ -30,7 +30,7 @@ namespace NicheGraphics::InkHUD class Applet; -class ThreadedMessageApplet : public Applet +class ThreadedMessageApplet : public Applet, public SinglePortModule { public: explicit ThreadedMessageApplet(uint8_t channelIndex); @@ -41,16 +41,11 @@ class ThreadedMessageApplet : public Applet void onActivate() override; void onDeactivate() override; void onShutdown() override; - int onReceiveTextMessage(const meshtastic_MeshPacket *p); + ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; bool approveNotification(Notification &n) override; // Which notifications to suppress protected: - // Used to register our text message callback - CallbackObserver textMessageObserver = - CallbackObserver(this, - &ThreadedMessageApplet::onReceiveTextMessage); - void saveMessagesToFlash(); void loadMessagesFromFlash(); diff --git a/src/graphics/niche/InkHUD/Events.cpp b/src/graphics/niche/InkHUD/Events.cpp index f07645989..2abe30793 100644 --- a/src/graphics/niche/InkHUD/Events.cpp +++ b/src/graphics/niche/InkHUD/Events.cpp @@ -4,14 +4,13 @@ #include "RTC.h" #include "buzz.h" -#include "modules/AdminModule.h" #include "modules/ExternalNotificationModule.h" #include "modules/TextMessageModule.h" #include "sleep.h" #include "./Applet.h" #include "./SystemApplet.h" -#include "graphics/niche/FlashData.h" +#include "graphics/niche/Utils/FlashData.h" using namespace NicheGraphics; @@ -30,7 +29,7 @@ void InkHUD::Events::begin() rebootObserver.observe(¬ifyReboot); textMessageObserver.observe(textMessageModule); #if !MESHTASTIC_EXCLUDE_ADMIN - adminMessageObserver.observe(adminModule); + adminMessageObserver.observe((Observable *)adminModule); #endif #ifdef ARCH_ESP32 lightSleepObserver.observe(¬ifyLightSleep); @@ -193,14 +192,15 @@ int InkHUD::Events::onReceiveTextMessage(const meshtastic_MeshPacket *packet) return 0; // Tell caller to continue notifying other observers. (No reason to abort this event) } -int InkHUD::Events::onAdminMessage(const meshtastic_AdminMessage *message) +int InkHUD::Events::onAdminMessage(AdminModule_ObserverData *data) { - switch (message->which_payload_variant) { + switch (data->request->which_payload_variant) { // Factory reset // Two possible messages. One preserves BLE bonds, other wipes. Both should clear InkHUD data. case meshtastic_AdminMessage_factory_reset_device_tag: case meshtastic_AdminMessage_factory_reset_config_tag: eraseOnReboot = true; + *data->result = AdminMessageHandleResult::HANDLED; break; default: diff --git a/src/graphics/niche/InkHUD/Events.h b/src/graphics/niche/InkHUD/Events.h index 2a2dad5dc..df68f368c 100644 --- a/src/graphics/niche/InkHUD/Events.h +++ b/src/graphics/niche/InkHUD/Events.h @@ -13,7 +13,7 @@ however this class handles general events which concern InkHUD as a whole, e.g. #include "configuration.h" -#include "Observer.h" +#include "modules/AdminModule.h" #include "./InkHUD.h" #include "./Persistence.h" @@ -33,7 +33,7 @@ class Events int beforeDeepSleep(void *unused); // Prepare for shutdown int beforeReboot(void *unused); // Prepare for reboot int onReceiveTextMessage(const meshtastic_MeshPacket *packet); // Store most recent text message - int onAdminMessage(const meshtastic_AdminMessage *message); // Handle incoming admin messages + int onAdminMessage(AdminModule_ObserverData *data); // Handle incoming admin messages #ifdef ARCH_ESP32 int beforeLightSleep(void *unused); // Prepare for light sleep #endif @@ -54,8 +54,8 @@ class Events CallbackObserver(this, &Events::onReceiveTextMessage); // Get notified of incoming admin messages, and handle any which are relevant to InkHUD - CallbackObserver adminMessageObserver = - CallbackObserver(this, &Events::onAdminMessage); + CallbackObserver adminMessageObserver = + CallbackObserver(this, &Events::onAdminMessage); #ifdef ARCH_ESP32 // Get notified when the system is entering light sleep diff --git a/src/graphics/niche/InkHUD/Persistence.h b/src/graphics/niche/InkHUD/Persistence.h index 40f1dd521..b85274c87 100644 --- a/src/graphics/niche/InkHUD/Persistence.h +++ b/src/graphics/niche/InkHUD/Persistence.h @@ -15,8 +15,8 @@ The save / load mechanism is a shared NicheGraphics feature. #include "configuration.h" #include "./InkHUD.h" -#include "graphics/niche/FlashData.h" #include "graphics/niche/InkHUD/MessageStore.h" +#include "graphics/niche/Utils/FlashData.h" namespace NicheGraphics::InkHUD { diff --git a/src/graphics/niche/Utils/CannedMessageStore.cpp b/src/graphics/niche/Utils/CannedMessageStore.cpp new file mode 100644 index 000000000..50998930d --- /dev/null +++ b/src/graphics/niche/Utils/CannedMessageStore.cpp @@ -0,0 +1,163 @@ +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "./CannedMessageStore.h" + +#include "FSCommon.h" +#include "NodeDB.h" +#include "SPILock.h" +#include "generated/meshtastic/cannedmessages.pb.h" + +using namespace NicheGraphics; + +// Location of the file which stores the canned messages on flash +static const char *cannedMessagesConfigFile = "/prefs/cannedConf.proto"; + +CannedMessageStore::CannedMessageStore() +{ +#if !MESHTASTIC_EXCLUDE_ADMIN + adminMessageObserver.observe(adminModule); +#endif + + // Load & parse messages from flash + load(); +} + +// Get access to (or create) the singleton instance of this class +CannedMessageStore *CannedMessageStore::getInstance() +{ + // Instantiate the class the first time this method is called + static CannedMessageStore *const singletonInstance = new CannedMessageStore; + + return singletonInstance; +} + +// Access canned messages by index +// Consumer should check CannedMessageStore::size to avoid accessing out of bounds +const std::string &CannedMessageStore::at(uint8_t i) +{ + assert(i < messages.size()); + return messages.at(i); +} + +// Number of canned message strings available +uint8_t CannedMessageStore::size() +{ + return messages.size(); +} + +// Load canned message data from flash, and parse into the individual strings +void CannedMessageStore::load() +{ + // In case we're reloading + messages.clear(); + + // Attempt to load the bulk canned message data from flash + meshtastic_CannedMessageModuleConfig cannedMessageModuleConfig; + LoadFileResult result = nodeDB->loadProto("/prefs/cannedConf.proto", meshtastic_CannedMessageModuleConfig_size, + sizeof(meshtastic_CannedMessageModuleConfig), + &meshtastic_CannedMessageModuleConfig_msg, &cannedMessageModuleConfig); + + // Abort if nothing to load + if (result != LoadFileResult::LOAD_SUCCESS || strlen(cannedMessageModuleConfig.messages) == 0) + return; + + // Split into individual canned messages + // These are concatenated when stored in flash, using '|' as a delimiter + std::string s; + for (char c : cannedMessageModuleConfig.messages) { // Character by character + + // If found end of a string + if (c == '|' || c == '\0') { + // Copy into the vector (if non-empty) + if (!s.empty()) + messages.push_back(s); + + // Reset the string builder + s.clear(); + + // End of data, all strings processed + if (c == 0) + break; + } + + // Otherwise, append char (continue building string) + else + s.push_back(c); + } +} + +// Handle incoming admin messages +// We get these as an observer of AdminModule +// It's our responsibility to handle setting and getting of canned messages via the client API +// Ordinarily, this would be handled by the CannedMessageModule, but it is bound to Screen.cpp, so not suitable for NicheGraphics +int CannedMessageStore::onAdminMessage(AdminModule_ObserverData *data) +{ + switch (data->request->which_payload_variant) { + + // Client API changing the canned messages + case meshtastic_AdminMessage_set_canned_message_module_messages_tag: + handleSet(data->request); + *data->result = AdminMessageHandleResult::HANDLED; + break; + + // Client API wants to know the current canned messages + case meshtastic_AdminMessage_get_canned_message_module_messages_request_tag: + handleGet(data->response); + *data->result = AdminMessageHandleResult::HANDLED_WITH_RESPONSE; + break; + + default: + break; + } + + return 0; // Tell caller to continue notifying other observers. (No reason to abort this event) +} + +// Client API changing the canned messages +void CannedMessageStore::handleSet(const meshtastic_AdminMessage *request) +{ + // Copy into the correct struct (for writing to flash as protobuf) + meshtastic_CannedMessageModuleConfig cannedMessageModuleConfig; + strncpy(cannedMessageModuleConfig.messages, request->set_canned_message_module_messages, + sizeof(cannedMessageModuleConfig.messages)); + + // Ensure the directory exists +#ifdef FSCom + spiLock->lock(); + FSCom.mkdir("/prefs"); + spiLock->unlock(); +#endif + + // Write to flash + nodeDB->saveProto(cannedMessagesConfigFile, meshtastic_CannedMessageModuleConfig_size, + &meshtastic_CannedMessageModuleConfig_msg, &cannedMessageModuleConfig); + + // Reload from flash, to update the canned messages in RAM + // (This is a lazy way to handle it) + load(); +} + +// Client API wants to know the current canned messages +// We're reconstructing the monolithic canned message string from our copy of the messages in RAM +// Lazy, but more convenient that reloading the monolithic string from flash just for this +void CannedMessageStore::handleGet(meshtastic_AdminMessage *response) +{ + // Merge the canned messages back into the delimited format expected + std::string merged; + if (!messages.empty()) { // Don't run if no messages: error on pop_back with size=0 + merged.reserve(201); + for (std::string &s : messages) { + merged += s; + merged += '|'; + } + merged.pop_back(); // Drop the final delimiter (loop added one too many) + } + + // Place the data into the response + // This response is scoped to AdminModule::handleReceivedProtobuf + // We were passed reference to it via the observable + response->which_payload_variant = meshtastic_AdminMessage_get_canned_message_module_messages_response_tag; + strncpy(response->get_canned_message_module_messages_response, merged.c_str(), strlen(merged.c_str()) + 1); +} + +#endif \ No newline at end of file diff --git a/src/graphics/niche/Utils/CannedMessageStore.h b/src/graphics/niche/Utils/CannedMessageStore.h new file mode 100644 index 000000000..c00e1cf5c --- /dev/null +++ b/src/graphics/niche/Utils/CannedMessageStore.h @@ -0,0 +1,54 @@ +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +/* + +Re-usable NicheGraphics tool + +Makes canned message data accessible to any NicheGraphics UI. + - handles loading & parsing from flash + - handles the admin messages for setting & getting canned messages via client API (phone apps, etc) + +The original CannedMessageModule class is bound to Screen.cpp, +making it incompatible with the NicheGraphics framework, which suppresses Screen.cpp + +This implementation aims to be self-contained. +The necessary interaction with the AdminModule is done as an observer. + +*/ + +#pragma once + +#include "configuration.h" + +#include "modules/AdminModule.h" + +namespace NicheGraphics +{ + +class CannedMessageStore +{ + public: + static CannedMessageStore *getInstance(); // Create or get the singleton instance + const std::string &at(uint8_t i); // Get canned message at index + uint8_t size(); // Get total number of canned messages + + int onAdminMessage(AdminModule_ObserverData *data); // Handle incoming admin messages + + private: + CannedMessageStore(); // Constructor made private: force use of CannedMessageStore::instance() + + void load(); // Load from flash, and parse + + void handleSet(const meshtastic_AdminMessage *request); // Client API changing the canned messages + void handleGet(meshtastic_AdminMessage *response); // Client API wants to know current canned messages + + std::vector messages; + + // Get notified of incoming admin messages, to get / set canned messages + CallbackObserver adminMessageObserver = + CallbackObserver(this, &CannedMessageStore::onAdminMessage); +}; + +}; // namespace NicheGraphics + +#endif \ No newline at end of file diff --git a/src/graphics/niche/FlashData.h b/src/graphics/niche/Utils/FlashData.h similarity index 100% rename from src/graphics/niche/FlashData.h rename to src/graphics/niche/Utils/FlashData.h diff --git a/src/main.cpp b/src/main.cpp index 17214b13f..f3147520f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1422,7 +1422,7 @@ extern meshtastic_DeviceMetadata getDeviceMetadata() deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_AUDIO_CONFIG; #endif // Option to explicitly include canned messages for edge cases, e.g. niche graphics -#if (!HAS_SCREEN || NO_EXT_GPIO) || MESHTASTIC_EXCLUDE_CANNEDMESSAGES +#if ((!HAS_SCREEN || NO_EXT_GPIO) || MESHTASTIC_EXCLUDE_CANNEDMESSAGES) && !defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS) deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_CANNEDMSG_CONFIG; #endif #if NO_EXT_GPIO diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index d489231ad..aad7f5f06 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -470,22 +470,38 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta setPassKey(&res); myReply = allocDataProtobuf(res); } else if (mp.decoded.want_response) { - LOG_DEBUG("Did not responded to a request that wanted a respond. req.variant=%d", r->which_payload_variant); + LOG_DEBUG("Module API did not respond to admin message. req.variant=%d", r->which_payload_variant); } else if (handleResult != AdminMessageHandleResult::HANDLED) { // Probably a message sent by us or sent to our local node. FIXME, we should avoid scanning these messages - LOG_DEBUG("Ignore irrelevant admin %d", r->which_payload_variant); + LOG_DEBUG("Module API did not handle admin message %d", r->which_payload_variant); } break; } + // Allow any observers (e.g. the UI) to handle/respond + AdminMessageHandleResult observerResult = AdminMessageHandleResult::NOT_HANDLED; + meshtastic_AdminMessage observerResponse = meshtastic_AdminMessage_init_default; + AdminModule_ObserverData observerData = { + .request = r, + .response = &observerResponse, + .result = &observerResult, + }; + + notifyObservers(&observerData); + + if (observerResult == AdminMessageHandleResult::HANDLED_WITH_RESPONSE) { + setPassKey(&observerResponse); + myReply = allocDataProtobuf(observerResponse); + LOG_DEBUG("Observer responded to admin message"); + } else if (observerResult == AdminMessageHandleResult::HANDLED) { + LOG_DEBUG("Observer handled admin message"); + } + // If asked for a response and it is not yet set, generate an 'ACK' response if (mp.decoded.want_response && !myReply) { myReply = allocErrorResponse(meshtastic_Routing_Error_NONE, &mp); } - // Allow any observers (e.g. the UI) to respond to this event - notifyObservers(r); - return handled; } diff --git a/src/modules/AdminModule.h b/src/modules/AdminModule.h index 5638e57e7..867751f49 100644 --- a/src/modules/AdminModule.h +++ b/src/modules/AdminModule.h @@ -6,10 +6,19 @@ #include "mesh/wifi/WiFiAPClient.h" #endif +/** + * Datatype passed to Observers by AdminModule, to allow external handling of admin messages + */ +struct AdminModule_ObserverData { + const meshtastic_AdminMessage *request; + meshtastic_AdminMessage *response; + AdminMessageHandleResult *result; +}; + /** * Admin module for admin messages */ -class AdminModule : public ProtobufModule, public Observable +class AdminModule : public ProtobufModule, public Observable { public: /** Constructor From a7dcf580ad6275d3ff730e1b192ec2a410bc5875 Mon Sep 17 00:00:00 2001 From: Kongduino Date: Thu, 26 Jun 2025 01:54:57 +0800 Subject: [PATCH 02/25] Update RedirectablePrint.cpp (#7114) Bug fix to my hexDump code. Because `log()` adds a carriage return, hexdump lines were split over 3 lines. This fixes it. --- src/RedirectablePrint.cpp | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/RedirectablePrint.cpp b/src/RedirectablePrint.cpp index 07f873864..7c8d77651 100644 --- a/src/RedirectablePrint.cpp +++ b/src/RedirectablePrint.cpp @@ -352,8 +352,8 @@ void RedirectablePrint::hexDump(const char *logLevel, unsigned char *buf, uint16 for (uint16_t i = 0; i < len; i += 16) { if (i % 128 == 0) log(logLevel, " +------------------------------------------------+ +----------------+"); - char s[] = "| | | |\n"; - uint8_t ix = 1, iy = 52; + char s[] = " | | | |\n"; + uint8_t ix = 5, iy = 56; for (uint8_t j = 0; j < 16; j++) { if (i + j < len) { uint8_t c = buf[i + j]; @@ -367,10 +367,8 @@ void RedirectablePrint::hexDump(const char *logLevel, unsigned char *buf, uint16 } } uint8_t index = i / 16; - if (i < 256) - log(logLevel, " "); - log(logLevel, "%02x", index); - log(logLevel, "."); + sprintf(s, "%03x", index); + s[3] = '.'; log(logLevel, s); } log(logLevel, " +------------------------------------------------+ +----------------+"); @@ -393,4 +391,4 @@ std::string RedirectablePrint::mt_sprintf(const std::string fmt_str, ...) break; } return std::string(formatted.get()); -} \ No newline at end of file +} From 3870d81bf6a1b0b1c5a4a9855cfa3196b9cab53b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 25 Jun 2025 20:18:55 +0200 Subject: [PATCH 03/25] [create-pull-request] automated change (#7134) Co-authored-by: caveman99 <25002+caveman99@users.noreply.github.com> --- protobufs | 2 +- src/mesh/generated/meshtastic/telemetry.pb.h | 66 +++++++++++++++++--- 2 files changed, 60 insertions(+), 8 deletions(-) diff --git a/protobufs b/protobufs index 6791138f0..386fa53c1 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 6791138f0ba2b7c471072bd4bba6cbb8bacffe2d +Subproject commit 386fa53c1596c8dfc547521f08df107f4cb3a275 diff --git a/src/mesh/generated/meshtastic/telemetry.pb.h b/src/mesh/generated/meshtastic/telemetry.pb.h index 4fa673df8..90b0d9d10 100644 --- a/src/mesh/generated/meshtastic/telemetry.pb.h +++ b/src/mesh/generated/meshtastic/telemetry.pb.h @@ -91,7 +91,9 @@ typedef enum _meshtastic_TelemetrySensorType { /* MAX17261 lipo battery gauge */ meshtastic_TelemetrySensorType_MAX17261 = 38, /* PCT2075 Temperature Sensor */ - meshtastic_TelemetrySensorType_PCT2075 = 39 + meshtastic_TelemetrySensorType_PCT2075 = 39, + /* ADS1X15 ADC */ + meshtastic_TelemetrySensorType_ADS1X15 = 40 } meshtastic_TelemetrySensorType; /* Struct definitions */ @@ -206,6 +208,36 @@ typedef struct _meshtastic_PowerMetrics { /* Current (Ch3) */ bool has_ch3_current; float ch3_current; + /* Voltage (Ch4) */ + bool has_ch4_voltage; + float ch4_voltage; + /* Current (Ch4) */ + bool has_ch4_current; + float ch4_current; + /* Voltage (Ch5) */ + bool has_ch5_voltage; + float ch5_voltage; + /* Current (Ch5) */ + bool has_ch5_current; + float ch5_current; + /* Voltage (Ch6) */ + bool has_ch6_voltage; + float ch6_voltage; + /* Current (Ch6) */ + bool has_ch6_current; + float ch6_current; + /* Voltage (Ch7) */ + bool has_ch7_voltage; + float ch7_voltage; + /* Current (Ch7) */ + bool has_ch7_current; + float ch7_current; + /* Voltage (Ch8) */ + bool has_ch8_voltage; + float ch8_voltage; + /* Current (Ch8) */ + bool has_ch8_current; + float ch8_current; } meshtastic_PowerMetrics; /* Air quality metrics */ @@ -360,8 +392,8 @@ extern "C" { /* Helper constants for enums */ #define _meshtastic_TelemetrySensorType_MIN meshtastic_TelemetrySensorType_SENSOR_UNSET -#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_PCT2075 -#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_PCT2075+1)) +#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_ADS1X15 +#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_ADS1X15+1)) @@ -376,7 +408,7 @@ extern "C" { /* Initializer values for message structs */ #define meshtastic_DeviceMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0} #define meshtastic_EnvironmentMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} -#define meshtastic_PowerMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} +#define meshtastic_PowerMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} #define meshtastic_AirQualityMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} #define meshtastic_LocalStats_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_HealthMetrics_init_default {false, 0, false, 0, false, 0} @@ -385,7 +417,7 @@ extern "C" { #define meshtastic_Nau7802Config_init_default {0, 0} #define meshtastic_DeviceMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0} #define meshtastic_EnvironmentMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} -#define meshtastic_PowerMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} +#define meshtastic_PowerMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} #define meshtastic_AirQualityMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0} #define meshtastic_LocalStats_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define meshtastic_HealthMetrics_init_zero {false, 0, false, 0, false, 0} @@ -427,6 +459,16 @@ extern "C" { #define meshtastic_PowerMetrics_ch2_current_tag 4 #define meshtastic_PowerMetrics_ch3_voltage_tag 5 #define meshtastic_PowerMetrics_ch3_current_tag 6 +#define meshtastic_PowerMetrics_ch4_voltage_tag 7 +#define meshtastic_PowerMetrics_ch4_current_tag 8 +#define meshtastic_PowerMetrics_ch5_voltage_tag 9 +#define meshtastic_PowerMetrics_ch5_current_tag 10 +#define meshtastic_PowerMetrics_ch6_voltage_tag 11 +#define meshtastic_PowerMetrics_ch6_current_tag 12 +#define meshtastic_PowerMetrics_ch7_voltage_tag 13 +#define meshtastic_PowerMetrics_ch7_current_tag 14 +#define meshtastic_PowerMetrics_ch8_voltage_tag 15 +#define meshtastic_PowerMetrics_ch8_current_tag 16 #define meshtastic_AirQualityMetrics_pm10_standard_tag 1 #define meshtastic_AirQualityMetrics_pm25_standard_tag 2 #define meshtastic_AirQualityMetrics_pm100_standard_tag 3 @@ -518,7 +560,17 @@ X(a, STATIC, OPTIONAL, FLOAT, ch1_current, 2) \ X(a, STATIC, OPTIONAL, FLOAT, ch2_voltage, 3) \ X(a, STATIC, OPTIONAL, FLOAT, ch2_current, 4) \ X(a, STATIC, OPTIONAL, FLOAT, ch3_voltage, 5) \ -X(a, STATIC, OPTIONAL, FLOAT, ch3_current, 6) +X(a, STATIC, OPTIONAL, FLOAT, ch3_current, 6) \ +X(a, STATIC, OPTIONAL, FLOAT, ch4_voltage, 7) \ +X(a, STATIC, OPTIONAL, FLOAT, ch4_current, 8) \ +X(a, STATIC, OPTIONAL, FLOAT, ch5_voltage, 9) \ +X(a, STATIC, OPTIONAL, FLOAT, ch5_current, 10) \ +X(a, STATIC, OPTIONAL, FLOAT, ch6_voltage, 11) \ +X(a, STATIC, OPTIONAL, FLOAT, ch6_current, 12) \ +X(a, STATIC, OPTIONAL, FLOAT, ch7_voltage, 13) \ +X(a, STATIC, OPTIONAL, FLOAT, ch7_current, 14) \ +X(a, STATIC, OPTIONAL, FLOAT, ch8_voltage, 15) \ +X(a, STATIC, OPTIONAL, FLOAT, ch8_current, 16) #define meshtastic_PowerMetrics_CALLBACK NULL #define meshtastic_PowerMetrics_DEFAULT NULL @@ -631,7 +683,7 @@ extern const pb_msgdesc_t meshtastic_Nau7802Config_msg; #define meshtastic_HostMetrics_size 264 #define meshtastic_LocalStats_size 72 #define meshtastic_Nau7802Config_size 16 -#define meshtastic_PowerMetrics_size 30 +#define meshtastic_PowerMetrics_size 81 #define meshtastic_Telemetry_size 272 #ifdef __cplusplus From 7512673b09fb2bdeb3f287e68ad7c4ff28657b7e Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 25 Jun 2025 16:36:33 -0500 Subject: [PATCH 04/25] Do not beacon Device telemetry by default anymore (#7116) * Do not beacon Device telemetry by default anymore * Update * Old default interval for sensor * Added userpref * Addd tracker to default telemetry roles * Let the macro do its job in router mode --- src/mesh/NodeDB.cpp | 10 +++++++++- userPrefs.jsonc | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index d13864bd9..f4f50f8b0 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -850,10 +850,12 @@ void NodeDB::installRoleDefaults(meshtastic_Config_DeviceConfig_Role role) if (role == meshtastic_Config_DeviceConfig_Role_ROUTER) { initConfigIntervals(); initModuleConfigIntervals(); + moduleConfig.telemetry.device_update_interval = default_telemetry_broadcast_interval_secs; config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_CORE_PORTNUMS_ONLY; owner.has_is_unmessagable = true; owner.is_unmessagable = true; } else if (role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) { + moduleConfig.telemetry.device_update_interval = ONE_DAY; owner.has_is_unmessagable = true; owner.is_unmessagable = true; } else if (role == meshtastic_Config_DeviceConfig_Role_REPEATER) { @@ -864,6 +866,7 @@ void NodeDB::installRoleDefaults(meshtastic_Config_DeviceConfig_Role role) } else if (role == meshtastic_Config_DeviceConfig_Role_SENSOR) { owner.has_is_unmessagable = true; owner.is_unmessagable = true; + moduleConfig.telemetry.device_update_interval = default_telemetry_broadcast_interval_secs; moduleConfig.telemetry.environment_measurement_enabled = true; moduleConfig.telemetry.environment_update_interval = 300; } else if (role == meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND) { @@ -881,6 +884,7 @@ void NodeDB::installRoleDefaults(meshtastic_Config_DeviceConfig_Role role) } else if (role == meshtastic_Config_DeviceConfig_Role_TRACKER) { owner.has_is_unmessagable = true; owner.is_unmessagable = true; + moduleConfig.telemetry.device_update_interval = default_telemetry_broadcast_interval_secs; } else if (role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) { owner.has_is_unmessagable = true; owner.is_unmessagable = true; @@ -910,7 +914,11 @@ void NodeDB::installRoleDefaults(meshtastic_Config_DeviceConfig_Role role) void NodeDB::initModuleConfigIntervals() { // Zero out telemetry intervals so that they coalesce to defaults in Default.h - moduleConfig.telemetry.device_update_interval = 0; +#ifdef USERPREFS_CONFIG_DEVICE_TELEM_UPDATE_INTERVAL + moduleConfig.telemetry.device_update_interval = USERPREFS_CONFIG_DEVICE_TELEM_UPDATE_INTERVAL; +#else + moduleConfig.telemetry.device_update_interval = UINT32_MAX; +#endif moduleConfig.telemetry.environment_update_interval = 0; moduleConfig.telemetry.air_quality_interval = 0; moduleConfig.telemetry.power_update_interval = 0; diff --git a/userPrefs.jsonc b/userPrefs.jsonc index 497327478..fc9e6ed72 100644 --- a/userPrefs.jsonc +++ b/userPrefs.jsonc @@ -31,6 +31,7 @@ // "USERPREFS_CONFIG_SMART_POSITION_ENABLED": "false", // "USERPREFS_CONFIG_GPS_UPDATE_INTERVAL": "600", // "USERPREFS_CONFIG_POSITION_BROADCAST_INTERVAL": "1800", + // "USERPREFS_CONFIG_DEVICE_TELEM_UPDATE_INTERVAL": "900", // Device telemetry update interval in seconds // "USERPREFS_LORACONFIG_CHANNEL_NUM": "31", // "USERPREFS_LORACONFIG_MODEM_PRESET": "meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST", // "USERPREFS_USE_ADMIN_KEY_0": "{ 0xcd, 0xc0, 0xb4, 0x3c, 0x53, 0x24, 0xdf, 0x13, 0xca, 0x5a, 0xa6, 0x0c, 0x0d, 0xec, 0x85, 0x5a, 0x4c, 0xf6, 0x1a, 0x96, 0x04, 0x1a, 0x3e, 0xfc, 0xbb, 0x8e, 0x33, 0x71, 0xe5, 0xfc, 0xff, 0x3c }", From c144bd03dcaa7f16472ac61929c06d81a4fe602b Mon Sep 17 00:00:00 2001 From: Austin Date: Wed, 25 Jun 2025 21:17:47 -0400 Subject: [PATCH 05/25] MeshAdv-Mini: Correct autoconf settings (#7117) --- bin/config.d/lora-MeshAdv-Mini-900M22S.yaml | 2 +- src/platform/portduino/PortduinoGlue.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/config.d/lora-MeshAdv-Mini-900M22S.yaml b/bin/config.d/lora-MeshAdv-Mini-900M22S.yaml index 554116b57..b47b5c996 100644 --- a/bin/config.d/lora-MeshAdv-Mini-900M22S.yaml +++ b/bin/config.d/lora-MeshAdv-Mini-900M22S.yaml @@ -6,6 +6,6 @@ Lora: IRQ: 16 Busy: 20 Reset: 24 - TXen: 13 + RXen: 12 DIO2_AS_RF_SWITCH: true DIO3_TCXO_VOLTAGE: true diff --git a/src/platform/portduino/PortduinoGlue.h b/src/platform/portduino/PortduinoGlue.h index 43aea4218..5795f0d8d 100644 --- a/src/platform/portduino/PortduinoGlue.h +++ b/src/platform/portduino/PortduinoGlue.h @@ -11,7 +11,7 @@ inline const std::unordered_map configProducts = {{"MESHTOAD", "lora-usb-meshtoad-e22.yaml"}, {"MESHSTICK", "lora-meshstick-1262.yaml"}, {"MESHADV-PI", "lora-MeshAdv-900M30S.yaml"}, - {"MESHADV-MINI", "lora-MeshAdv-Mini-900M22S.yaml"}, + {"MeshAdv Mini", "lora-MeshAdv-Mini-900M22S.yaml"}, {"POWERPI", "lora-MeshAdv-900M30S.yaml"}}; enum configNames { From f6630cd31d5607c193abed6f9e75fcb08adce24a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 26 Jun 2025 18:31:14 +1000 Subject: [PATCH 06/25] Upgrade trunk (#7084) Co-authored-by: sachaw <11172820+sachaw@users.noreply.github.com> --- .trunk/trunk.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index b40f9458b..dc065d041 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -4,13 +4,13 @@ cli: plugins: sources: - id: trunk - ref: v1.7.0 + ref: v1.7.1 uri: https://github.com/trunk-io/plugins lint: enabled: - - checkov@3.2.442 - - renovate@40.60.3 - - prettier@3.5.3 + - checkov@3.2.446 + - renovate@41.10.0 + - prettier@3.6.1 - trufflehog@3.89.2 - yamllint@1.37.1 - bandit@1.8.5 @@ -20,9 +20,9 @@ lint: - isort@6.0.1 - markdownlint@0.45.0 - oxipng@9.1.5 - - svgo@3.3.2 + - svgo@4.0.0 - actionlint@1.7.7 - - flake8@7.2.0 + - flake8@7.3.0 - hadolint@2.12.1-beta - shfmt@3.6.0 - shellcheck@0.10.0 From 8ae05f6b33934efe152b0da8aa08498b62644f43 Mon Sep 17 00:00:00 2001 From: Manuel <71137295+mverch67@users.noreply.github.com> Date: Thu, 26 Jun 2025 12:44:51 +0200 Subject: [PATCH 07/25] defcon tft display size definitions (#7142) --- variants/picomputer-s3/platformio.ini | 2 ++ variants/seeed-sensecap-indicator/platformio.ini | 2 ++ variants/t-deck/platformio.ini | 4 +++- variants/unphone/platformio.ini | 2 ++ 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/variants/picomputer-s3/platformio.ini b/variants/picomputer-s3/platformio.ini index b861b5496..b7987796f 100644 --- a/variants/picomputer-s3/platformio.ini +++ b/variants/picomputer-s3/platformio.ini @@ -42,6 +42,8 @@ build_flags = -D LV_USE_LOG=0 -D USE_LOG_DEBUG -D LOG_DEBUG_INC=\"DebugConfiguration.h\" + -D LGFX_SCREEN_WIDTH=240 + -D LGFX_SCREEN_HEIGHT=320 -D LGFX_DRIVER=LGFX_PICOMPUTER_S3 -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_PICOMPUTER_S3.h\" -D VIEW_320x240 diff --git a/variants/seeed-sensecap-indicator/platformio.ini b/variants/seeed-sensecap-indicator/platformio.ini index 2187ebd8a..140c6f527 100644 --- a/variants/seeed-sensecap-indicator/platformio.ini +++ b/variants/seeed-sensecap-indicator/platformio.ini @@ -53,6 +53,8 @@ build_flags = -D USE_LOG_DEBUG -D LOG_DEBUG_INC=\"DebugConfiguration.h\" -D CUSTOM_TOUCH_DRIVER + -D LGFX_SCREEN_WIDTH=480 + -D LGFX_SCREEN_HEIGHT=480 -D LGFX_DRIVER=LGFX_INDICATOR -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_INDICATOR.h\" -D VIEW_320x240 diff --git a/variants/t-deck/platformio.ini b/variants/t-deck/platformio.ini index 04e305abb..6ee95b119 100644 --- a/variants/t-deck/platformio.ini +++ b/variants/t-deck/platformio.ini @@ -51,7 +51,9 @@ build_flags = -D RADIOLIB_DEBUG_SPI=0 -D RADIOLIB_DEBUG_PROTOCOL=0 -D RADIOLIB_SPI_PARANOID=0 - -D CALIBRATE_TOUCH=0 +; -D CALIBRATE_TOUCH=0 + -D LGFX_SCREEN_WIDTH=240 + -D LGFX_SCREEN_HEIGHT=320 -D LGFX_DRIVER=LGFX_TDECK -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_T_DECK.h\" ; -D LVGL_DRIVER=LVGL_TDECK diff --git a/variants/unphone/platformio.ini b/variants/unphone/platformio.ini index ef0f62b60..f286c3d4c 100644 --- a/variants/unphone/platformio.ini +++ b/variants/unphone/platformio.ini @@ -54,6 +54,8 @@ build_flags = -D LV_USE_LOG=0 -D USE_LOG_DEBUG -D LOG_DEBUG_INC=\"DebugConfiguration.h\" + -D LGFX_SCREEN_WIDTH=320 + -D LGFX_SCREEN_HEIGHT=480 -D LGFX_DRIVER=LGFX_UNPHONE_V9 -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_UNPHONE.h\" -D VIEW_320x240 From eeb52a1221bd24320559b1252bf17d6ef795f79a Mon Sep 17 00:00:00 2001 From: dylanli Date: Thu, 26 Jun 2025 19:30:45 +0800 Subject: [PATCH 08/25] support seeed_wio_tracker_L1_eink (#7125) * initial commit of eink version * fit for ssd1682 initial test run hud * update to solve mirroring problem * change eink screen ic to ssd1680 * remove HINK_E0213A367 * trunk fmt * fix wrong type * fix some fmt --- src/graphics/niche/InkHUD/DisplayHealth.cpp | 5 + .../seeed_wio_tracker_L1_eink/nicheGraphics.h | 106 ++++++++++ .../seeed_wio_tracker_L1_eink/platformio.ini | 14 ++ .../seeed_wio_tracker_L1_eink/variant.cpp | 103 ++++++++++ variants/seeed_wio_tracker_L1_eink/variant.h | 194 ++++++++++++++++++ 5 files changed, 422 insertions(+) create mode 100644 variants/seeed_wio_tracker_L1_eink/nicheGraphics.h create mode 100644 variants/seeed_wio_tracker_L1_eink/platformio.ini create mode 100644 variants/seeed_wio_tracker_L1_eink/variant.cpp create mode 100644 variants/seeed_wio_tracker_L1_eink/variant.h diff --git a/src/graphics/niche/InkHUD/DisplayHealth.cpp b/src/graphics/niche/InkHUD/DisplayHealth.cpp index e8849b72e..7e1accafd 100644 --- a/src/graphics/niche/InkHUD/DisplayHealth.cpp +++ b/src/graphics/niche/InkHUD/DisplayHealth.cpp @@ -7,7 +7,12 @@ using namespace NicheGraphics; // Timing for "maintenance" // Paying off full-refresh debt with unprovoked updates, if the display is not very active + +#ifdef SEEED_WIO_TRACKER_L1 +static constexpr uint32_t MAINTENANCE_MS_INITIAL = 5 * 1000UL; +#else static constexpr uint32_t MAINTENANCE_MS_INITIAL = 60 * 1000UL; +#endif static constexpr uint32_t MAINTENANCE_MS = 60 * 60 * 1000UL; InkHUD::DisplayHealth::DisplayHealth() : concurrency::OSThread("Mediator") diff --git a/variants/seeed_wio_tracker_L1_eink/nicheGraphics.h b/variants/seeed_wio_tracker_L1_eink/nicheGraphics.h new file mode 100644 index 000000000..7854de4b5 --- /dev/null +++ b/variants/seeed_wio_tracker_L1_eink/nicheGraphics.h @@ -0,0 +1,106 @@ +#pragma once + +#include "configuration.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +// InkHUD-specific components +// --------------------------- +#include "graphics/niche/InkHUD/InkHUD.h" + +// Applets +#include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h" +#include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h" +#include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h" +#include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h" +#include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h" +#include "graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h" + +// Shared NicheGraphics components +// -------------------------------- +#include "graphics/niche/Drivers/Backlight/LatchingBacklight.h" +#include "graphics/niche/Drivers/EInk/GDEY0213B74.h" +#include "graphics/niche/Inputs/TwoButton.h" + +// Special case - fix T-Echo's touch button +// ---------------------------------------- +// On a handful of T-Echos, LoRa TX triggers the capacitive touch +// To avoid this, we lockout the button during TX +#include "mesh/RadioLibInterface.h" + +void setupNicheGraphics() +{ + using namespace NicheGraphics; + + // SPI + // ----------------------------- + + // For NRF52 platforms, SPI pins are defined in variant.h + SPI1.begin(); + + // E-Ink Driver + // ----------------------------- + + Drivers::EInk *driver = new Drivers::GDEY0213B74; + driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); + + // InkHUD + // ---------------------------- + + InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance(); + + // Set the E-Ink driver + inkhud->setDriver(driver); + + // Set how many FAST updates per FULL update + // Set how unhealthy additional FAST updates beyond this number are + inkhud->setDisplayResilience(7, 1.5); + + // Select fonts + InkHUD::Applet::fontLarge = FREESANS_9PT_WIN1252; + InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; + + // Customize default settings + inkhud->persistence->settings.userTiles.maxCount = 2; // Two applets side-by-side + // 270 degrees clockwise + inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Device definitely has a battery + inkhud->persistence->settings.optionalMenuItems.backlight = true; // Until proves capacitive button works by touching it + inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users + inkhud->persistence->settings.optionalMenuItems.nextTile = false; // Behavior handled by aux button instead + + // Setup backlight controller + // Note: AUX button attached further down + Drivers::LatchingBacklight *backlight = Drivers::LatchingBacklight::getInstance(); + backlight->setPin(PIN_EINK_EN); + + // Pick applets + // Note: order of applets determines priority of "auto-show" feature + inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown + inkhud->addApplet("DMs", new InkHUD::DMApplet); // - + inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // - + inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // - + inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated + inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - + inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 + + inkhud->persistence->settings.rotation = 1; + // inkhud->persistence->printSettings(&inkhud->persistence->settings); + // Start running InkHUD + inkhud->begin(); + // inkhud->persistence->printSettings(&inkhud->persistence->settings); + // Buttons + // -------------------------- + + Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component + + // #0: Main User Button + buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin()); + buttons->setTiming(0, 75, 500); + buttons->setHandlerShortPress(0, [inkhud]() { inkhud->shortpress(); }); + buttons->setHandlerLongPress(0, [inkhud]() { inkhud->longpress(); }); + + // Begin handling button events + buttons->start(); +} + +#endif \ No newline at end of file diff --git a/variants/seeed_wio_tracker_L1_eink/platformio.ini b/variants/seeed_wio_tracker_L1_eink/platformio.ini new file mode 100644 index 000000000..b84757b9d --- /dev/null +++ b/variants/seeed_wio_tracker_L1_eink/platformio.ini @@ -0,0 +1,14 @@ +[env:seeed_wio_tracker_L1_eink] +board = seeed_wio_tracker_L1 +extends = nrf52840_base, inkhud +;board_level = extra +build_flags = ${nrf52840_base.build_flags} ${inkhud.build_flags} + -I $PROJECT_DIR/variants/seeed_wio_tracker_L1_eink + -D SEEED_WIO_TRACKER_L1 + -Isrc/platform/nrf52/softdevice -Isrc/platform/nrf52/softdevice/nrf52 +board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/seeed_wio_tracker_L1_eink> ${inkhud.build_src_filter} +lib_deps = + ${inkhud.lib_deps} + ${nrf52840_base.lib_deps} +debug_tool = jlink diff --git a/variants/seeed_wio_tracker_L1_eink/variant.cpp b/variants/seeed_wio_tracker_L1_eink/variant.cpp new file mode 100644 index 000000000..bcbe20ea5 --- /dev/null +++ b/variants/seeed_wio_tracker_L1_eink/variant.cpp @@ -0,0 +1,103 @@ +/* + * variant.cpp - Digital pin mapping for TRACKER L1 + * + * This file defines the pin mapping array that maps logical digital pins (D0-D17) + * to physical GPIO ports/pins on the Nordic nRF52 series microcontroller. + * + * Board: [Seeed Studio WIO TRACKER L1] + * Hardware Features: + * - LoRa module (CS/SCK/MISO/MOSI control pins) + * - GNSS module (TX/RX/Reset/Wakeup) + * - User LEDs (D11-D12) + * - User button (D13) + * - Grove/NFC interface (D14-D15) + * - Battery voltage monitoring (D16) + * + * Created [20250521] + * By [Dylan] + */ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +/** + * @brief Digital pin to GPIO port/pin mapping table + * + * Format: Logical Pin (Dx) -> nRF Port.Pin (Px.xx) + * + */ + +extern "C" { +const uint32_t g_ADigitalPinMap[] = { + // D0 .. D10 - Peripheral control pins + 41, // D0 P1.09 GNSS_WAKEUP + 7, // D1 P0.07 LORA_DIO1 + 39, // D2 P1.07 LORA_RESET + 42, // D3 P1.10 LORA_BUSY + 46, // D4 P1.14 (A4/SDA) LORA_CS + 40, // D5 P1.08 (A5/SCL) LORA_SW + 27, // D6 P0.27 (UART_TX) GNSS_TX + 26, // D7 P0.26 (UART_RX) GNSS_RX + 30, // D8 P0.30 (SPI_SCK) LORA_SCK + 3, // D9 P0.3 (SPI_MISO) LORA_MISO + 28, // D10 P0.28 (SPI_MOSI) LORA_MOSI + + // D11-D12 - LED outputs + 33, // D11 P1.1 User LED + // Buzzer + 32, // D12 P1.0 Buzzer + + // D13 - User input + 8, // D13 P0.08 User Button + + // D14-D15 - Grove interface + 6, // D14 P0.06 OLED SDA + 5, // D15 P0.05 OLED SCL + + // D16 - Battery voltage ADC input + 31, // D16 P0.31 VBAT_ADC + // GROVE + 43, // D17 P0.00 GROVESDA + 44, // D18 P0.01 GROVESCL + + // FLASH + 21, // D19 P0.21 (QSPI_SCK) + 25, // D20 P0.25 (QSPI_CSN) + 20, // D21 P0.20 (QSPI_SIO_0 DI) + 24, // D22 P0.24 (QSPI_SIO_1 DO) + 22, // D23 P0.22 (QSPI_SIO_2 WP) + 23, // D24 P0.23 (QSPI_SIO_3 HOLD) + + 36, // D25 TB_UP + 12, // D26 TB_DOWN + 11, // D27 TB_LEFT + 35, // D28 TB_RIGHT + 37, // D29 TB_PRESS + 4, // D30 BAT_CTL + + 13, // D31 EINK_SCK + 14, // D32 EINK_RST + 15, // D33 EINK_MOSI + 16, // D34 EINK_DC + 17, // D35 EINK_BUSY + 19, // D36 EINK_CS + +}; +} + +void initVariant() +{ + pinMode(PIN_QSPI_CS, OUTPUT); + digitalWrite(PIN_QSPI_CS, HIGH); + // This setup is crucial for ensuring low power consumption and proper initialization of the hardware components. + // VBAT_ENABLE + pinMode(BAT_READ, OUTPUT); + digitalWrite(BAT_READ, HIGH); + + pinMode(PIN_LED1, OUTPUT); + digitalWrite(PIN_LED1, LOW); + pinMode(PIN_LED2, OUTPUT); + digitalWrite(PIN_LED2, LOW); +} \ No newline at end of file diff --git a/variants/seeed_wio_tracker_L1_eink/variant.h b/variants/seeed_wio_tracker_L1_eink/variant.h new file mode 100644 index 000000000..98a7b2c39 --- /dev/null +++ b/variants/seeed_wio_tracker_L1_eink/variant.h @@ -0,0 +1,194 @@ +#ifndef _SEEED_TRACKER_L1_H_ +#define _SEEED_TRACKER_L1_H_ +#include "WVariant.h" +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Clock Configuration +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define VARIANT_MCK (64000000ul) // Master clock frequency +#define USE_LFXO // 32.768kHz crystal for LFCLK + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Pin Capacity Definitions +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define PINS_COUNT (38u) // Total GPIO pins +#define NUM_DIGITAL_PINS (38u) // Digital I/O pins +#define NUM_ANALOG_INPUTS (8u) // Analog inputs (A0-A5 + VBAT + AREF) +#define NUM_ANALOG_OUTPUTS (0u) + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// LED Configuration +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// LEDs +// LEDs +#define PIN_LED1 (11) // LED P1.15 +#define PIN_LED2 (12) // + +#define LED_BUILTIN PIN_LED1 +#define LED_CONN PIN_LED2 + +#define LED_GREEN PIN_LED1 +#define LED_BLUE PIN_LED2 +// #define LED_PIN PIN_LED2 +#define LED_STATE_ON 1 // State when LED is litted +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Button Configuration +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +#ifdef BUTTON_PIN +#undef BUTTON_PIN +#endif + +#define BUTTON_PIN D13 // This is the Program Button +// #define BUTTON_NEED_PULLUP 1 +#define BUTTON_ACTIVE_LOW true +#define BUTTON_ACTIVE_PULLUP false + +#define BUTTON_PIN_TOUCH 13 // Touch button +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Digital Pin Mapping (D0-D10) +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define D0 0 // P1.06 GNSS_WAKEUP/IO0 +#define D1 1 // P0.07 LORA_DIO1 +#define D2 2 // P1.07 LORA_RESET +#define D3 3 // P1.10 LORA_BUSY +#define D4 4 // P1.14 LORA_CS +#define D5 5 // P1.08 LORA_SW +#define D6 6 // P0.27 GNSS_TX +#define D7 7 // P0.26 GNSS_RX +#define D8 8 // P0.30 SPI_SCK +#define D9 9 // P0.03 SPI_MISO +#define D10 10 // P0.28 SPI_MOSI +#define D12 12 // P1.00 Buzzer +#define D13 13 // P0.08 User Button +#define D14 14 // P0.05 OLED SCL +#define D15 15 // P0.06 OLED SDA +#define D16 16 // P0.31 VBAT_ADC +#define D17 17 // P0.00 GROVE SDA +#define D18 18 // P0.01 GROVE_SCL +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Analog Pin Definitions +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define PIN_A0 0 // P0.02 Analog Input 0 +#define PIN_A1 1 // P0.03 Analog Input 1 +#define PIN_A2 2 // P0.28 Analog Input 2 +#define PIN_A3 3 // P0.29 Analog Input 3 +#define PIN_A4 4 // P0.04 Analog Input 4 +#define PIN_A5 5 // P0.05 Analog Input 5 +#define PIN_VBAT D16 // P0.31 Battery voltage sense +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Communication Interfaces +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// I2C Configuration +#define HAS_WIRE 1 +#define PIN_WIRE_SDA D18 // P0.09 +#define PIN_WIRE_SCL D17 // P0.10 +#define WIRE_INTERFACES_COUNT 1 + +static const uint8_t SDA = PIN_WIRE_SDA; +static const uint8_t SCL = PIN_WIRE_SCL; + +// SPI Configuration (SX1262) + +// #define SPI_INTERFACES_COUNT 1 +#define PIN_SPI_MISO 9 // P0.03 (D9) +#define PIN_SPI_MOSI 10 // P0.28 (D10) +#define PIN_SPI_SCK 8 // P0.30 (D8) + +// SX1262 LoRa Module Pins +#define USE_SX1262 +#define SX126X_CS D4 // Chip select +#define SX126X_DIO1 D1 // Digital IO 1 (Interrupt) +#define SX126X_BUSY D3 // Busy status +#define SX126X_RESET D2 // Reset control +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 // TCXO supply voltage +#define SX126X_RXEN D5 // RX enable control +#define SX126X_TXEN RADIOLIB_NC +#define SX126X_DIO2_AS_RF_SWITCH // This Line is really necessary for SX1262 to work with RF switch or will loss TX power + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// EINK +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define SPI_INTERFACES_COUNT 2 +#define PIN_EINK_CS 36 +#define PIN_EINK_BUSY 35 +#define PIN_EINK_DC 34 +#define PIN_EINK_RES 32 +#define PIN_EINK_SCLK 31 +#define PIN_EINK_MOSI 33 +#define PIN_EINK_EN 14 // unused +#define PIN_SPI1_MISO 15 // unused +#define PIN_SPI1_MOSI PIN_EINK_MOSI +#define PIN_SPI1_SCK PIN_EINK_SCLK + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Power Management +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +#define BAT_READ 30 // D30 = P0.04 Reads battery voltage from divider on signal board. +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define ADC_MULTIPLIER 2.0 +#define BATTERY_PIN PIN_VBAT // PIN_A7 +#define AREF_VOLTAGE 3.6 +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// GPS L76KB +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define GPS_L76K +#ifdef GPS_L76K +#define PIN_GPS_RX D6 // P0.26 +#define PIN_GPS_TX D7 +#define HAS_GPS 1 +#define GPS_BAUDRATE 9600 +#define GPS_THREAD_INTERVAL 50 +#define PIN_SERIAL1_RX PIN_GPS_TX +#define PIN_SERIAL1_TX PIN_GPS_RX + +#define GPS_RX_PIN PIN_GPS_TX +#define GPS_TX_PIN PIN_GPS_RX +#define PIN_GPS_STANDBY D0 + +// #define GPS_DEBUG +// #define GPS_EN D18 // P1.05 +#endif + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// On-board QSPI Flash +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// On-board QSPI Flash +#define PIN_QSPI_SCK (21) +#define PIN_QSPI_CS (22) +#define PIN_QSPI_IO0 (23) +#define PIN_QSPI_IO1 (24) +#define PIN_QSPI_IO2 (25) +#define PIN_QSPI_IO3 (26) + +#define EXTERNAL_FLASH_DEVICES P25Q16H +#define EXTERNAL_FLASH_USE_QSPI + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Buzzer +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Buzzer + +#define PIN_BUZZER D12 // P1.00, pwm output + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// joystick +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +#define CANNED_MESSAGE_MODULE_ENABLE 1 + +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Compatibility Definitions +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#ifdef __cplusplus +extern "C" { +#endif +// Serial port placeholders + +#define PIN_SERIAL2_RX (-1) +#define PIN_SERIAL2_TX (-1) +#ifdef __cplusplus +} +#endif + +#endif // _SEEED_TRACKER_L1_H_ \ No newline at end of file From ad23c065f6f80e27931ec27eb2822e553a7bd7ff Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 26 Jun 2025 07:56:34 -0500 Subject: [PATCH 09/25] Rate limiting fix and added 2 second rate limiting to text messages (#7139) * Rate limiting fix and added 1.5 second rate limiting to text messages * Remove copy-pasta * Update src/mesh/Default.h Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Two is more reasonable * Two too --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/mesh/Default.h | 1 + src/mesh/PhoneAPI.cpp | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/mesh/Default.h b/src/mesh/Default.h index fd3f10668..5a6eb61b1 100644 --- a/src/mesh/Default.h +++ b/src/mesh/Default.h @@ -5,6 +5,7 @@ #define ONE_DAY 24 * 60 * 60 #define ONE_MINUTE_MS 60 * 1000 #define THIRTY_SECONDS_MS 30 * 1000 +#define TWO_SECONDS_MS 2 * 1000 #define FIVE_SECONDS_MS 5 * 1000 #define TEN_SECONDS_MS 10 * 1000 diff --git a/src/mesh/PhoneAPI.cpp b/src/mesh/PhoneAPI.cpp index e2acd8463..287de38fa 100644 --- a/src/mesh/PhoneAPI.cpp +++ b/src/mesh/PhoneAPI.cpp @@ -670,7 +670,8 @@ bool PhoneAPI::handleToRadioPacket(meshtastic_MeshPacket &p) meshtastic_QueueStatus qs = router->getQueueStatus(); service->sendQueueStatusToPhone(qs, 0, p.id); return false; - } else if (IS_ONE_OF(meshtastic_PortNum_POSITION_APP, meshtastic_PortNum_WAYPOINT_APP, meshtastic_PortNum_ALERT_APP) && + } else if (IS_ONE_OF(p.decoded.portnum, meshtastic_PortNum_POSITION_APP, meshtastic_PortNum_WAYPOINT_APP, + meshtastic_PortNum_ALERT_APP, meshtastic_PortNum_TELEMETRY_APP) && lastPortNumToRadio[p.decoded.portnum] && Throttle::isWithinTimespanMs(lastPortNumToRadio[p.decoded.portnum], TEN_SECONDS_MS)) { // TODO: [Issue #6700] Make this rate limit throttling scale up / down with the preset @@ -680,6 +681,13 @@ bool PhoneAPI::handleToRadioPacket(meshtastic_MeshPacket &p) // FIXME: Figure out why this continues to happen // sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "Position can only be sent once every 5 seconds"); return false; + } else if (p.decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP && lastPortNumToRadio[p.decoded.portnum] && + Throttle::isWithinTimespanMs(lastPortNumToRadio[p.decoded.portnum], TWO_SECONDS_MS)) { + LOG_WARN("Rate limit portnum %d", p.decoded.portnum); + meshtastic_QueueStatus qs = router->getQueueStatus(); + service->sendQueueStatusToPhone(qs, 0, p.id); + sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "Text messages can only be sent once every 2 seconds"); + return false; } lastPortNumToRadio[p.decoded.portnum] = millis(); service->handleToRadio(p); From 2ab717cebb2e7ff1dced1ec9ee8c2d8510411619 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 26 Jun 2025 10:57:33 -0500 Subject: [PATCH 10/25] Remove bundling of web-ui from ESP32 devices (#7143) --- .github/actions/build-variant/action.yml | 50 ++++++++++++------------ .github/workflows/build_esp32.yml | 2 +- .github/workflows/build_esp32_c3.yml | 2 +- .github/workflows/build_esp32_c6.yml | 2 +- .github/workflows/build_esp32_s3.yml | 2 +- .github/workflows/main_matrix.yml | 1 - bin/build-esp32.sh | 11 +++--- bin/device-install.bat | 19 ++------- bin/device-install.sh | 30 ++++++-------- 9 files changed, 50 insertions(+), 69 deletions(-) diff --git a/.github/actions/build-variant/action.yml b/.github/actions/build-variant/action.yml index 67d002eea..f611908ee 100644 --- a/.github/actions/build-variant/action.yml +++ b/.github/actions/build-variant/action.yml @@ -27,10 +27,10 @@ inputs: description: A newline separated list of paths to store as artifacts required: false default: "" - include-web-ui: - description: Include the web UI in the build - required: false - default: "false" + # include-web-ui: + # description: Include the web UI in the build + # required: false + # default: "false" arch: description: Processor arch name required: true @@ -43,29 +43,29 @@ runs: id: base uses: ./.github/actions/setup-base - - name: Get web ui version - if: inputs.include-web-ui == 'true' - id: webver - shell: bash - run: | - echo "ver=$(cat bin/web.version)" >> $GITHUB_OUTPUT + # - name: Get web ui version + # if: inputs.include-web-ui == 'true' + # id: webver + # shell: bash + # run: | + # echo "ver=$(cat bin/web.version)" >> $GITHUB_OUTPUT - - name: Pull web ui - if: inputs.include-web-ui == 'true' - uses: dsaltares/fetch-gh-release-asset@master - with: - repo: meshtastic/web - file: build.tar - target: build.tar - token: ${{ inputs.github_token }} - version: tags/v${{ steps.webver.outputs.ver }} + # - name: Pull web ui + # if: inputs.include-web-ui == 'true' + # uses: dsaltares/fetch-gh-release-asset@master + # with: + # repo: meshtastic/web + # file: build.tar + # target: build.tar + # token: ${{ inputs.github_token }} + # version: tags/v${{ steps.webver.outputs.ver }} - - name: Unpack web ui - if: inputs.include-web-ui == 'true' - shell: bash - run: | - tar -xf build.tar -C data/static - rm build.tar + # - name: Unpack web ui + # if: inputs.include-web-ui == 'true' + # shell: bash + # run: | + # tar -xf build.tar -C data/static + # rm build.tar - name: Remove debug flags for release shell: bash diff --git a/.github/workflows/build_esp32.yml b/.github/workflows/build_esp32.yml index 4fc31f22c..616f51746 100644 --- a/.github/workflows/build_esp32.yml +++ b/.github/workflows/build_esp32.yml @@ -33,5 +33,5 @@ jobs: artifact-paths: | release/*.bin release/*.elf - include-web-ui: true + #include-web-ui: true arch: esp32 diff --git a/.github/workflows/build_esp32_c3.yml b/.github/workflows/build_esp32_c3.yml index 546762952..1b6b832e9 100644 --- a/.github/workflows/build_esp32_c3.yml +++ b/.github/workflows/build_esp32_c3.yml @@ -33,5 +33,5 @@ jobs: artifact-paths: | release/*.bin release/*.elf - include-web-ui: true + #include-web-ui: true arch: esp32c3 diff --git a/.github/workflows/build_esp32_c6.yml b/.github/workflows/build_esp32_c6.yml index 56d4d806d..29dac51e1 100644 --- a/.github/workflows/build_esp32_c6.yml +++ b/.github/workflows/build_esp32_c6.yml @@ -33,5 +33,5 @@ jobs: artifact-paths: | release/*.bin release/*.elf - include-web-ui: true + #include-web-ui: true arch: esp32c6 diff --git a/.github/workflows/build_esp32_s3.yml b/.github/workflows/build_esp32_s3.yml index a9c067ee1..7e0373503 100644 --- a/.github/workflows/build_esp32_s3.yml +++ b/.github/workflows/build_esp32_s3.yml @@ -33,5 +33,5 @@ jobs: artifact-paths: | release/*.bin release/*.elf - include-web-ui: true + #include-web-ui: true arch: esp32s3 diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index 9b9877e04..03e61d572 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -257,7 +257,6 @@ jobs: ./device-*.sh ./device-*.bat ./littlefs-*.bin - ./littlefswebui-*.bin ./bleota*bin ./Meshtastic_nRF52_factory_erase*.uf2 retention-days: 30 diff --git a/bin/build-esp32.sh b/bin/build-esp32.sh index a0635e997..96578e914 100755 --- a/bin/build-esp32.sh +++ b/bin/build-esp32.sh @@ -34,11 +34,12 @@ SRCBIN=.pio/build/$1/firmware.bin cp $SRCBIN $OUTDIR/$basename-update.bin echo "Building Filesystem for ESP32 targets" -pio run --environment $1 -t buildfs -cp .pio/build/$1/littlefs.bin $OUTDIR/littlefswebui-$1-$VERSION.bin -# Remove webserver files from the filesystem and rebuild -ls -l data/static # Diagnostic list of files -rm -rf data/static +# If you want to build the webui, uncomment the following lines +# pio run --environment $1 -t buildfs +# cp .pio/build/$1/littlefs.bin $OUTDIR/littlefswebui-$1-$VERSION.bin +# # Remove webserver files from the filesystem and rebuild +# ls -l data/static # Diagnostic list of files +# rm -rf data/static pio run --environment $1 -t buildfs cp .pio/build/$1/littlefs.bin $OUTDIR/littlefs-$1-$VERSION.bin cp bin/device-install.* $OUTDIR diff --git a/bin/device-install.bat b/bin/device-install.bat index 816d2fbba..12bfd4f6e 100755 --- a/bin/device-install.bat +++ b/bin/device-install.bat @@ -5,7 +5,6 @@ TITLE Meshtastic device-install SET "SCRIPT_NAME=%~nx0" SET "DEBUG=0" SET "PYTHON=" -SET "WEB_APP=0" SET "TFT_BUILD=0" SET "BIGDB8=0" SET "BIGDB16=0" @@ -25,7 +24,7 @@ GOTO getopts :help ECHO Flash image file to device, but first erasing and writing system information. ECHO. -ECHO Usage: %SCRIPT_NAME% -f filename [-p PORT] [-P python] (--web) [--1200bps-reset] +ECHO Usage: %SCRIPT_NAME% -f filename [-p PORT] [-P python] [--1200bps-reset] ECHO. ECHO Options: ECHO -f filename The firmware .bin file to flash. Custom to your device type and region. (required) @@ -35,13 +34,12 @@ ECHO If not set, ESPTOOL iterates all ports (Dangerous). ECHO -P python Specify alternate python interpreter to use to invoke esptool. (default: python) ECHO If supplied the script will use python. ECHO If not supplied the script will try to find esptool in Path. -ECHO --web Enable WebUI. (default: false) ECHO --1200bps-reset Attempt to place the device in correct mode. (1200bps Reset) ECHO Some hardware requires this twice. ECHO. ECHO Example: %SCRIPT_NAME% -p COM17 --1200bps-reset ECHO Example: %SCRIPT_NAME% -f firmware-t-deck-tft-2.6.0.0b106d4.bin -p COM11 -ECHO Example: %SCRIPT_NAME% -f firmware-unphone-2.6.0.0b106d4.bin -p COM11 --web +ECHO Example: %SCRIPT_NAME% -f firmware-unphone-2.6.0.0b106d4.bin -p COM11 GOTO eof :version @@ -61,7 +59,6 @@ IF /I "%~1"=="-f" SET "FILENAME=%~2" & SHIFT IF "%~1"=="-p" SET "ESPTOOL_PORT=%~2" & SHIFT IF /I "%~1"=="--port" SET "ESPTOOL_PORT=%~2" & SHIFT IF "%~1"=="-P" SET "PYTHON=%~2" & SHIFT -IF /I "%~1"=="--web" SET "WEB_APP=1" IF /I "%~1"=="--1200bps-reset" SET "BPS_RESET=1" SHIFT GOTO getopts @@ -153,9 +150,6 @@ IF %BPS_RESET% EQU 1 ( @REM https://github.com/meshtastic/web-flasher/blob/main/types/resources.ts#L3 IF NOT "!FILENAME:-tft-=!"=="!FILENAME!" ( CALL :LOG_MESSAGE DEBUG "We are working with a *-tft-* file. !FILENAME!" - IF %WEB_APP% EQU 1 ( - CALL :LOG_MESSAGE ERROR "Cannot enable WebUI (--web) and MUI." & GOTO eof - ) SET "TFT_BUILD=1" ) ELSE ( CALL :LOG_MESSAGE DEBUG "We are NOT working with a *-tft-* file. !FILENAME!" @@ -209,13 +203,8 @@ SET "OTA_FILENAME=bleota.bin" :end_loop_c3 CALL :LOG_MESSAGE DEBUG "Set OTA_FILENAME to: !OTA_FILENAME!" -@REM Check if (--web) is enabled and prefix BASENAME with "littlefswebui-" else "littlefs-". -IF %WEB_APP% EQU 1 ( - CALL :LOG_MESSAGE INFO "WebUI selected." - SET "SPIFFS_FILENAME=littlefswebui-%BASENAME%" -) ELSE ( - SET "SPIFFS_FILENAME=littlefs-%BASENAME%" -) +@REM Set SPIFFS filename with "littlefs-" prefix. +SET "SPIFFS_FILENAME=littlefs-%BASENAME%" CALL :LOG_MESSAGE DEBUG "Set SPIFFS_FILENAME to: !SPIFFS_FILENAME!" @REM Default offsets. diff --git a/bin/device-install.sh b/bin/device-install.sh index 613696d2f..42d0c4089 100755 --- a/bin/device-install.sh +++ b/bin/device-install.sh @@ -1,14 +1,18 @@ #!/bin/bash PYTHON=${PYTHON:-$(which python3 python | head -n 1)} -WEB_APP=false BPS_RESET=false TFT_BUILD=false MCU="" # Variant groups BIGDB_8MB=( - "picomputer-s3" + # Check if FILENAME contains "-tft-" and set target partitionScheme accordingly. +if [[ $FILENAME == *"-tft-"* ]]; then + TFT_BUILD=true +fi + +# Extract BASENAME from %FILENAME% for later use.r-s3" "unphone" "seeed-sensecap-indicator" "crowpanel-esp32s3" @@ -76,14 +80,13 @@ set -e # Usage info show_help() { cat < Date: Thu, 26 Jun 2025 11:19:54 -0500 Subject: [PATCH 11/25] Fixed triple click GPS toggle bungle --- src/main.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index f3147520f..2251241da 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1016,7 +1016,8 @@ void setup() BaseType_t higherWake = 0; mainDelay.interruptFromISR(&higherWake); }, - INPUT_BROKER_USER_PRESS, INPUT_BROKER_SHUTDOWN, 5000, INPUT_BROKER_SEND_PING, INPUT_BROKER_GPS_TOGGLE); + INPUT_BROKER_USER_PRESS, INPUT_BROKER_SHUTDOWN, 5000, INPUT_BROKER_SEND_PING, INPUT_BROKER_NONE, 0, + INPUT_BROKER_GPS_TOGGLE); #endif #endif From 50424d1035a0bb2d34e852cbf3bb47cc22a0559d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Jun 2025 12:39:03 -0500 Subject: [PATCH 12/25] chore(deps): update meshtastic/web to v2.6.4 (#7017) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- bin/web.version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/web.version b/bin/web.version index a4db534a2..e46a05b19 100644 --- a/bin/web.version +++ b/bin/web.version @@ -1 +1 @@ -2.5.3 \ No newline at end of file +2.6.4 \ No newline at end of file From 18fbc2149d5b3844e9de29966df536417c1d84ac Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 26 Jun 2025 19:23:08 -0500 Subject: [PATCH 13/25] Fix iOS bluetooth crash: Ensure UINT32_MAX is not used (#7147) --- src/mesh/Default.h | 1 + src/mesh/NodeDB.cpp | 32 ++++++++++++++++++++++++-------- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/mesh/Default.h b/src/mesh/Default.h index 5a6eb61b1..7a38e21f1 100644 --- a/src/mesh/Default.h +++ b/src/mesh/Default.h @@ -8,6 +8,7 @@ #define TWO_SECONDS_MS 2 * 1000 #define FIVE_SECONDS_MS 5 * 1000 #define TEN_SECONDS_MS 10 * 1000 +#define MAX_INTERVAL INT32_MAX // FIXME: INT32_MAX to avoid overflow issues with Apple clients but should be UINT32_MAX #define min_default_telemetry_interval_secs 30 * 60 #define default_gps_update_interval IF_ROUTER(ONE_DAY, 2 * 60) diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index f4f50f8b0..3eb3a5173 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -339,6 +339,22 @@ NodeDB::NodeDB() moduleConfig.telemetry.health_update_interval = Default::getConfiguredOrMinimumValue( moduleConfig.telemetry.health_update_interval, min_default_telemetry_interval_secs); } + // FIXME: UINT32_MAX intervals overflows Apple clients until they are fully patched + if (config.device.node_info_broadcast_secs > MAX_INTERVAL) + config.device.node_info_broadcast_secs = MAX_INTERVAL; + if (config.position.position_broadcast_secs > MAX_INTERVAL) + config.position.position_broadcast_secs = MAX_INTERVAL; + if (moduleConfig.neighbor_info.update_interval > MAX_INTERVAL) + moduleConfig.neighbor_info.update_interval = MAX_INTERVAL; + if (moduleConfig.telemetry.device_update_interval > MAX_INTERVAL) + moduleConfig.telemetry.device_update_interval = MAX_INTERVAL; + if (moduleConfig.telemetry.environment_update_interval > MAX_INTERVAL) + moduleConfig.telemetry.environment_update_interval = MAX_INTERVAL; + if (moduleConfig.telemetry.air_quality_interval > MAX_INTERVAL) + moduleConfig.telemetry.air_quality_interval = MAX_INTERVAL; + if (moduleConfig.telemetry.health_update_interval > MAX_INTERVAL) + moduleConfig.telemetry.health_update_interval = MAX_INTERVAL; + if (moduleConfig.mqtt.has_map_report_settings && moduleConfig.mqtt.map_report_settings.publish_interval_secs < default_map_publish_interval_secs) { moduleConfig.mqtt.map_report_settings.publish_interval_secs = default_map_publish_interval_secs; @@ -900,14 +916,14 @@ void NodeDB::installRoleDefaults(meshtastic_Config_DeviceConfig_Role role) moduleConfig.telemetry.device_update_interval = ONE_DAY; } else if (role == meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN) { config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_LOCAL_ONLY; - config.device.node_info_broadcast_secs = UINT32_MAX; + config.device.node_info_broadcast_secs = MAX_INTERVAL; config.position.position_broadcast_smart_enabled = false; - config.position.position_broadcast_secs = UINT32_MAX; - moduleConfig.neighbor_info.update_interval = UINT32_MAX; - moduleConfig.telemetry.device_update_interval = UINT32_MAX; - moduleConfig.telemetry.environment_update_interval = UINT32_MAX; - moduleConfig.telemetry.air_quality_interval = UINT32_MAX; - moduleConfig.telemetry.health_update_interval = UINT32_MAX; + config.position.position_broadcast_secs = MAX_INTERVAL; + moduleConfig.neighbor_info.update_interval = MAX_INTERVAL; + moduleConfig.telemetry.device_update_interval = MAX_INTERVAL; + moduleConfig.telemetry.environment_update_interval = MAX_INTERVAL; + moduleConfig.telemetry.air_quality_interval = MAX_INTERVAL; + moduleConfig.telemetry.health_update_interval = MAX_INTERVAL; } } @@ -917,7 +933,7 @@ void NodeDB::initModuleConfigIntervals() #ifdef USERPREFS_CONFIG_DEVICE_TELEM_UPDATE_INTERVAL moduleConfig.telemetry.device_update_interval = USERPREFS_CONFIG_DEVICE_TELEM_UPDATE_INTERVAL; #else - moduleConfig.telemetry.device_update_interval = UINT32_MAX; + moduleConfig.telemetry.device_update_interval = MAX_INTERVAL; #endif moduleConfig.telemetry.environment_update_interval = 0; moduleConfig.telemetry.air_quality_interval = 0; From 29e7a71c97b767d27998e5145a2243629550aef1 Mon Sep 17 00:00:00 2001 From: Jason P Date: Thu, 26 Jun 2025 22:11:20 -0500 Subject: [PATCH 14/25] 2.7 Miscellaneous Fixes - Week 1 (#7102) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update Favorite Node Message Options to unify against other screens * Rebuild Horizontal Battery, Resolve overlap concerns * Update positioning on Message frame and fix drawCommonHeader overlay * Beginnings of creating isHighResolution bool * Fixup determineResolution() * Implement isHighResolution in place of SCREEN_WIDTH > 128 checks * Line Spacing bound to isHighResolution * Analog Clock for all * Add AM/PM to Analog Clock if isHighResolution and not TWatch * Simple Menu Queue, and add time menu * Fix prompt string for 12/24 hour picker * More menu banners into functions * Fix Action Menu on Home frame * Correct pop-up calculation size and continue to leverage isHighResolution * Move menu bits to MenuHandler * Plumb in the digital/analog picker * Correct Clock Face Picker title * Clock picker fixes * Migrate the rest of the menus to MenuHandler.* * Add compass menu and needle point option * Minor fix for compass point menu * Correct Home menu into typical format * Fix emoji bounce, overlap, and missing commonHeader * Sanitize long_names and removed unused variables * Slightly better sanitizeString variation * Resolved apostrophe being shown as upside down question mark * Gotta keep height and width in expected order * Remove Second Hand for Analog Clock on EInk displays * Fix Clock menu option decision tree * Improvements to Eink Navigation * Pause Banner for Eink moved to bottom * Updated working for 12-/24-hour menu and Added US/Arizona to timezone picker * Add Adhoc Ping and resolve error with std::string sanitized * Hide quick toggle as option is available within Action Menu, commented out for the moment * Remove old battery icon and option, use drawCommonHeader throughout, re-add battery to Clock frames * fix misc build warnings. NFC * Update Analog Clock on EInk to show more digits * Establish Action Menu on all node list screens, add NodeDB reset (with confirmation) option * Add Toggle Backlight for EInk Displays * Suppress action screen Full refresh for Eink * Adjust drawBluetoothConnectedIcon on TWatch * Maintain clock frame when switching between Clock Faces * Move modules beyond the clock in navigation * addressed the conflicts, and changed target branch to 2.7-MiscFixes-Week1 * cleanup, cheers * Add AM/PM to low resolution clock also * Small adjustments to AM/PM replacement across various devices * Resolve dangling pointer issues with sanitize code * Update comments for Screen.cpp related to module load change * Trunk runs * Update message caching to correct aged timestamp * Menu wording adjustments * Time Format wording * Use all the rows on EInk since with autohide the navigation bar * Finalize Time Format picker word change * Retired drawFunctionOverlay code No longer being used * Actually honor the points-north setting * Trunk * Compressed action list * Update no-op showOverlayBanner function * trunk * Correct T_Watch_S3 specific line * Autosized Action menu per screen * Finalize Autosized Action menu per screen * Unify Message Titles * Reorder Timezones to match expectations * Adjust text location for pop-ups * Revert "Actually honor the points-north setting" This reverts commit 20988aa4fabb0975be644989d556fca7e1176680. * Make NodeDB sort its internal vector when lastheard is updated. Don't sort in NodeListRenderer * Update src/graphics/draw/NodeListRenderer.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/mesh/NodeDB.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Pass by reference -- Thanks Copilot! * Throttle sorting just a touch * Check more carefully for own node * Eliminate some now-unneeded sorting * Move function after include * Putting Modules back to position 0 and some trunk checks found * Add Scrollbar for Action menus * Second attempt to move modules down the navigation bar * Continue effort of moving modules in the navigation * Canned Messages tweak * Replicate Function + Space through the Menu System * Move init button parameters into config struct (#7145) * Remove bundling of web-ui from ESP32 devices (#7143) * Fixed triple click GPS toggle bungle * Move init button parameters into config struct * Reapply "Actually honor the points-north setting" This reverts commit 42c1967e7b3735ec9f5be8acd9582bc9edcbc78a. * Actually do compass pointings correctly * Tweak to node bearings * Menu wording tweaks * Get the compass_north_top logic right * Don't jump frames after setting Compass * Get rid of the extra bearingTo functions * Don't blink Mail on EInk Clock Screens * Actually set lat and long * Calibrate * Convert Radians to Degrees * More degree vs radians fixes * De-duplicate draw arrow function * Don't advertise compass calibration without an accell thread. --------- Co-authored-by: Ben Meadors Co-authored-by: Jonathan Bennett Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Co-authored-by: Thomas Göttgens Co-authored-by: csrutil Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/graphics/Screen.cpp | 308 +++-------- src/graphics/Screen.h | 24 +- src/graphics/SharedUIDisplay.cpp | 99 ++-- src/graphics/SharedUIDisplay.h | 4 +- src/graphics/draw/ClockRenderer.cpp | 105 ++-- src/graphics/draw/ClockRenderer.h | 4 +- src/graphics/draw/CompassRenderer.cpp | 27 +- src/graphics/draw/CompassRenderer.h | 3 - src/graphics/draw/DebugRenderer.cpp | 29 +- src/graphics/draw/MenuHandler.cpp | 479 ++++++++++++++++++ src/graphics/draw/MenuHandler.h | 40 ++ src/graphics/draw/MessageRenderer.cpp | 191 ++++--- src/graphics/draw/MessageRenderer.h | 12 + src/graphics/draw/NodeListRenderer.cpp | 132 ++--- src/graphics/draw/NodeListRenderer.h | 7 - src/graphics/draw/NotificationRenderer.cpp | 215 ++++---- src/graphics/draw/NotificationRenderer.h | 3 +- src/graphics/draw/UIRenderer.cpp | 246 ++++----- src/graphics/draw/UIRenderer.h | 5 - src/graphics/images.h | 38 +- src/input/ButtonThread.cpp | 48 +- src/input/ButtonThread.h | 27 +- src/main.cpp | 154 +++--- src/mesh/MeshModule.cpp | 7 +- src/mesh/MeshModule.h | 2 +- src/mesh/NodeDB.cpp | 26 + src/mesh/NodeDB.h | 2 + src/modules/CannedMessageModule.cpp | 7 +- src/modules/KeyVerificationModule.cpp | 4 +- src/modules/SystemCommandsModule.cpp | 8 +- .../Telemetry/EnvironmentTelemetry.cpp | 4 +- src/modules/Telemetry/PowerTelemetry.cpp | 4 +- src/modules/WaypointModule.cpp | 14 +- src/serialization/MeshPacketSerializer.cpp | 12 +- variants/portduino/platformio.ini | 3 - variants/rak4631/variant.h | 4 +- 36 files changed, 1429 insertions(+), 868 deletions(-) create mode 100644 src/graphics/draw/MenuHandler.cpp create mode 100644 src/graphics/draw/MenuHandler.h diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 0818619a6..c8c9d8b74 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -31,6 +31,7 @@ along with this program. If not, see . #include "TimeFormatters.h" #include "draw/ClockRenderer.h" #include "draw/DebugRenderer.h" +#include "draw/MenuHandler.h" #include "draw/MessageRenderer.h" #include "draw/NodeListRenderer.h" #include "draw/NotificationRenderer.h" @@ -135,13 +136,17 @@ extern bool hasUnreadMessage; // The banner appears in the center of the screen and disappears after the specified duration // Called to trigger a banner with custom message and duration -void Screen::showOverlayBanner(const char *message, uint32_t durationMs, uint8_t options, std::function bannerCallback, - int8_t InitialSelected) +void Screen::showOverlayBanner(const char *message, uint32_t durationMs, const char **optionsArrayPtr, uint8_t options, + std::function bannerCallback, int8_t InitialSelected) { +#ifdef USE_EINK + EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus +#endif // Store the message and set the expiration timestamp strncpy(NotificationRenderer::alertBannerMessage, message, 255); NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs; + NotificationRenderer::optionsArrayPtr = optionsArrayPtr; NotificationRenderer::alertBannerOptions = options; NotificationRenderer::alertBannerCallback = bannerCallback; NotificationRenderer::curSelected = InitialSelected; @@ -203,7 +208,7 @@ float Screen::estimatedHeading(double lat, double lon) if (d < 10) // haven't moved enough, just keep current bearing return b; - b = GeoCoord::bearing(oldLat, oldLon, lat, lon); + b = GeoCoord::bearing(oldLat, oldLon, lat, lon) * RAD_TO_DEG; oldLat = lat; oldLon = lon; @@ -413,8 +418,7 @@ void Screen::setup() // === Set custom overlay callbacks === static OverlayCallback overlays[] = { - graphics::UIRenderer::drawFunctionOverlay, // For mute/buzzer modifiers etc. - graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame + graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame }; ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); @@ -471,6 +475,7 @@ void Screen::setup() // === Turn on display and trigger first draw === handleSetOn(true); + determineResolution(dispdev->height(), dispdev->width()); ui->update(); #ifndef USE_EINK ui->update(); // Some SSD1306 clones drop the first draw, so run twice @@ -557,6 +562,7 @@ int32_t Screen::runOnce() if (displayHeight == 0) { displayHeight = dispdev->getHeight(); } + menuHandler::handleMenuSwitch(); // Show boot screen for first logo_timeout seconds, then switch to normal operation. // serialSinceMsec adjusts for additional serial wait time during nRF52 bootup @@ -585,7 +591,7 @@ int32_t Screen::runOnce() #ifndef DISABLE_WELCOME_UNSET if (!NotificationRenderer::isOverlayBannerShowing() && config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { - LoraRegionPicker(0); + menuHandler::LoraRegionPicker(0); } #endif if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) { @@ -768,32 +774,6 @@ void Screen::setFrames(FrameFocus focus) indicatorIcons.clear(); size_t numframes = 0; - moduleFrames = MeshModule::GetMeshModulesWithUIFrames(); - LOG_DEBUG("Show %d module frames", moduleFrames.size()); - - // put all of the module frames first. - // this is a little bit of a dirty hack; since we're going to call - // the same drawModuleFrame handler here for all of these module frames - // and then we'll just assume that the state->currentFrame value - // is the same offset into the moduleFrames vector - // so that we can invoke the module's callback - for (auto i = moduleFrames.begin(); i != moduleFrames.end(); ++i) { - // Draw the module frame, using the hack described above - normalFrames[numframes] = drawModuleFrame; - - // Check if the module being drawn has requested focus - // We will honor this request later, if setFrames was triggered by a UIFrameEvent - MeshModule *m = *i; - if (m->isRequestingFocus()) - fsi.positions.focusedModule = numframes; - if (m == waypointModule) - fsi.positions.waypoint = numframes; - - indicatorIcons.push_back(icon_module); - numframes++; - } - - LOG_DEBUG("Added modules. numframes: %d", numframes); // If we have a critical fault, show it first fsi.positions.fault = numframes; @@ -807,7 +787,7 @@ void Screen::setFrames(FrameFocus focus) fsi.positions.clock = numframes; normalFrames[numframes++] = graphics::ClockRenderer::digitalWatchFace ? graphics::ClockRenderer::drawDigitalClockFrame : &graphics::ClockRenderer::drawAnalogClockFrame; - indicatorIcons.push_back(icon_clock); + indicatorIcons.push_back(digital_icon_clock); #endif // Declare this early so it’s available in FOCUS_PRESERVE block @@ -822,22 +802,27 @@ void Screen::setFrames(FrameFocus focus) indicatorIcons.push_back(icon_mail); #ifndef USE_EINK + fsi.positions.nodelist = numframes; normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen; indicatorIcons.push_back(icon_nodes); #endif // Show detailed node views only on E-Ink builds #ifdef USE_EINK + fsi.positions.nodelist_lastheard = numframes; normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen; indicatorIcons.push_back(icon_nodes); + fsi.positions.nodelist_hopsignal = numframes; normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen; indicatorIcons.push_back(icon_signal); + fsi.positions.nodelist_distance = numframes; normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen; indicatorIcons.push_back(icon_distance); #endif #if HAS_GPS + fsi.positions.nodelist_bearings = numframes; normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses; indicatorIcons.push_back(icon_list); @@ -857,8 +842,9 @@ void Screen::setFrames(FrameFocus focus) } #if !defined(DISPLAY_CLOCK_FRAME) fsi.positions.clock = numframes; - normalFrames[numframes++] = graphics::ClockRenderer::drawDigitalClockFrame; - indicatorIcons.push_back(icon_clock); + normalFrames[numframes++] = graphics::ClockRenderer::digitalWatchFace ? graphics::ClockRenderer::drawDigitalClockFrame + : graphics::ClockRenderer::drawAnalogClockFrame; + indicatorIcons.push_back(digital_icon_clock); #endif // We don't show the node info of our node (if we have it yet - we should) @@ -885,6 +871,36 @@ void Screen::setFrames(FrameFocus focus) } #endif + // Beware of what changes you make in this code! + // We pass numfames into GetMeshModulesWithUIFrames() which is highly important! + // Inside of that callback, goes over to MeshModule.cpp and we run + // modulesWithUIFrames.resize(startIndex, nullptr), to insert nullptr + // entries until we're ready to start building the matching entries. + // We are doing our best to keep the normalFrames vector + // and the moduleFrames vector in lock step. + moduleFrames = MeshModule::GetMeshModulesWithUIFrames(numframes); + LOG_DEBUG("Show %d module frames", moduleFrames.size()); + + for (auto i = moduleFrames.begin(); i != moduleFrames.end(); ++i) { + // Draw the module frame, using the hack described above + if (*i != nullptr) { + normalFrames[numframes] = drawModuleFrame; + + // Check if the module being drawn has requested focus + // We will honor this request later, if setFrames was triggered by a UIFrameEvent + MeshModule *m = *i; + if (m && m->isRequestingFocus()) + fsi.positions.focusedModule = numframes; + if (m && m == waypointModule) + fsi.positions.waypoint = numframes; + + indicatorIcons.push_back(icon_module); + numframes++; + } + } + + LOG_DEBUG("Added modules. numframes: %d", numframes); + fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE this->frameCount = numframes; // ✅ Save frame count for use in custom overlay LOG_DEBUG("Finished build frames. numframes: %d", numframes); @@ -916,6 +932,11 @@ void Screen::setFrames(FrameFocus focus) // If no module requested focus, will show the first frame instead ui->switchToFrame(fsi.positions.focusedModule); break; + case FOCUS_CLOCK: + // Whichever frame was marked by MeshModule::requestFocus(), if any + // If no module requested focus, will show the first frame instead + ui->switchToFrame(fsi.positions.clock); + break; case FOCUS_PRESERVE: // No more adjustment — force stay on same index @@ -1204,6 +1225,8 @@ int Screen::handleInputEvent(const InputEvent *event) ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); setFastFramerate(); // Draw ASAP ui->update(); + + menuHandler::handleMenuSwitch(); return 0; } /* @@ -1229,7 +1252,7 @@ int Screen::handleInputEvent(const InputEvent *event) // Ask any MeshModules if they're handling keyboard input right now bool inputIntercepted = false; for (MeshModule *module : moduleFrames) { - if (module->interceptingKeyboardInput()) + if (module && module->interceptingKeyboardInput()) inputIntercepted = true; } @@ -1241,129 +1264,36 @@ int Screen::handleInputEvent(const InputEvent *event) showNextFrame(); } else if (event->inputEvent == INPUT_BROKER_SELECT) { if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) { - const char *banner_message; - int options; - if (kb_found) { - banner_message = "Action?\nBack\nSleep Screen\nNew Preset Msg\nNew Freetext Msg"; - options = 4; - } else { - banner_message = "Action?\nBack\nSleep Screen\nNew Preset Msg"; - options = 3; - } - showOverlayBanner(banner_message, 30000, options, [](int selected) -> void { - if (selected == 1) { - screen->setOn(false); - } else if (selected == 2) { - cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); - } else if (selected == 3) { - cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST); - } - }); + menuHandler::homeBaseMenu(); #if HAS_TFT } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.memory) { - showOverlayBanner("Switch to MUI?\nYes\nNo", 30000, 2, [](int selected) -> void { - if (selected == 0) { - config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR; - config.bluetooth.enabled = false; - service->reloadConfig(SEGMENT_CONFIG); - rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); - } - }); + menuHandler::switchToMUIMenu(); #else } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.memory) { - showOverlayBanner( - "Beeps Mode\nAll Enabled\nDisabled\nNotifications\nSystem Only", 30000, 4, - [](int selected) -> void { - config.device.buzzer_mode = (meshtastic_Config_DeviceConfig_BuzzerMode)selected; - service->reloadConfig(SEGMENT_CONFIG); - }, - config.device.buzzer_mode); + menuHandler::BuzzerModeMenu(); #endif #if HAS_GPS } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.gps && gps) { - showOverlayBanner( - "Toggle GPS\nBack\nEnabled\nDisabled", 30000, 3, - [](int selected) -> void { - if (selected == 1) { - config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; - playGPSEnableBeep(); - gps->enable(); - service->reloadConfig(SEGMENT_CONFIG); - } else if (selected == 2) { - config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED; - playGPSDisableBeep(); - gps->disable(); - service->reloadConfig(SEGMENT_CONFIG); - } - }, - config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED ? 1 - : 2); // set inital selection + menuHandler::positionBaseMenu(); #endif } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.clock) { - TZPicker(); + menuHandler::clockMenu(); } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) { - LoraRegionPicker(); + menuHandler::LoraRegionPicker(); } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage && devicestate.rx_text_message.from) { - const char *banner_message; - int options; - if (kb_found) { - banner_message = "Message Action?\nBack\nDismiss\nReply via Preset\nReply via Freetext"; - options = 4; - } else { - banner_message = "Message Action?\nBack\nDismiss\nReply via Preset"; - options = 3; - } -#ifdef HAS_I2S - banner_message = "Message Action?\nBack\nDismiss\nReply via Preset\nReply via Freetext\nRead Aloud"; - options = 5; -#endif - showOverlayBanner(banner_message, 30000, options, [](int selected) -> void { - if (selected == 1) { - screen->dismissCurrentFrame(); - } else if (selected == 2) { - if (devicestate.rx_text_message.to == NODENUM_BROADCAST) { - cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, - devicestate.rx_text_message.channel); - } else { - cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from); - } - } else if (selected == 3) { - if (devicestate.rx_text_message.to == NODENUM_BROADCAST) { - cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, - devicestate.rx_text_message.channel); - } else { - cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from); - } - } -#ifdef HAS_I2S - else if (selected == 4) { - const meshtastic_MeshPacket &mp = devicestate.rx_text_message; - const char *msg = reinterpret_cast(mp.decoded.payload.bytes); - - audioThread->readAloud(msg); - } -#endif - }); + menuHandler::messageResponseMenu(); } else if (framesetInfo.positions.firstFavorite != 255 && this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite && this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) { - const char *banner_message; - int options; - if (kb_found) { - banner_message = "Message Node?\nCancel\nNew Preset Msg\nNew Freetext Msg"; - options = 3; - } else { - banner_message = "Message Node?\nCancel\nConfirm"; - options = 2; - } - showOverlayBanner(banner_message, 30000, options, [](int selected) -> void { - if (selected == 1) { - cannedMessageModule->LaunchWithDestination(graphics::UIRenderer::currentFavoriteNodeNum); - } else if (selected == 2) { - cannedMessageModule->LaunchFreetextWithDestination(graphics::UIRenderer::currentFavoriteNodeNum); - } - }); + menuHandler::favoriteBaseMenu(); + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist || + this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard || + this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal || + this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance || + this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal || + this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_bearings) { + menuHandler::nodeListMenu(); } } else if (event->inputEvent == INPUT_BROKER_BACK) { showPrevFrame(); @@ -1397,96 +1327,6 @@ bool Screen::isOverlayBannerShowing() return NotificationRenderer::isOverlayBannerShowing(); } -void Screen::LoraRegionPicker(uint32_t duration) -{ - showOverlayBanner( - "Set the LoRa " - "region\nBack\nUS\nEU_433\nEU_868\nCN\nJP\nANZ\nKR\nTW\nRU\nIN\nNZ_865\nTH\nLORA_24\nUA_433\nUA_868\nMY_433\nMY_" - "919\nSG_" - "923\nPH_433\nPH_868\nPH_915\nANZ_433", - duration, 23, - [](int selected) -> void { - if (selected != 0 && config.lora.region != _meshtastic_Config_LoRaConfig_RegionCode(selected)) { - config.lora.region = _meshtastic_Config_LoRaConfig_RegionCode(selected); - // This is needed as we wait til picking the LoRa region to generate keys for the first time. - if (!owner.is_licensed) { - bool keygenSuccess = false; - if (config.security.private_key.size == 32) { - // public key is derived from private, so this will always have the same result. - if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { - keygenSuccess = true; - } - } else { - LOG_INFO("Generate new PKI keys"); - crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes); - keygenSuccess = true; - } - if (keygenSuccess) { - config.security.public_key.size = 32; - config.security.private_key.size = 32; - owner.public_key.size = 32; - memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32); - } - } - config.lora.tx_enabled = true; - initRegion(); - if (myRegion->dutyCycle < 100) { - config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit - } - service->reloadConfig(SEGMENT_CONFIG); - rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); - } - }, - 0); -} - -void Screen::TZPicker() -{ - showOverlayBanner( - "Pick " - "Timezone\nBack\nUS/Hawaii\nUS/Alaska\nUS/Pacific\nUS/Mountain\nUS/Central\nUS/Eastern\nUTC\nEU/Western\nEU/" - "Central\nEU/Eastern\nAsia/Kolkata\nAsia/Hong_Kong\nAU/AWST\nAU/ACST\nAU/AEST\nPacific/NZ", - 30000, 17, [](int selected) -> void { - if (selected == 1) { // Hawaii - strncpy(config.device.tzdef, "HST10", sizeof(config.device.tzdef)); - } else if (selected == 2) { // Alaska - strncpy(config.device.tzdef, "AKST9AKDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); - } else if (selected == 3) { // Pacific - strncpy(config.device.tzdef, "PST8PDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); - } else if (selected == 4) { // Mountain - strncpy(config.device.tzdef, "MST7MDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); - } else if (selected == 5) { // Central - strncpy(config.device.tzdef, "CST6CDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); - } else if (selected == 6) { // Eastern - strncpy(config.device.tzdef, "EST5EDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); - } else if (selected == 7) { // UTC - strncpy(config.device.tzdef, "UTC", sizeof(config.device.tzdef)); - } else if (selected == 8) { // EU/Western - strncpy(config.device.tzdef, "GMT0BST,M3.5.0/1,M10.5.0", sizeof(config.device.tzdef)); - } else if (selected == 9) { // EU/Central - strncpy(config.device.tzdef, "CET-1CEST,M3.5.0,M10.5.0/3", sizeof(config.device.tzdef)); - } else if (selected == 10) { // EU/Eastern - strncpy(config.device.tzdef, "EET-2EEST,M3.5.0/3,M10.5.0/4", sizeof(config.device.tzdef)); - } else if (selected == 11) { // Asia/Kolkata - strncpy(config.device.tzdef, "IST-5:30", sizeof(config.device.tzdef)); - } else if (selected == 12) { // China - strncpy(config.device.tzdef, "HKT-8", sizeof(config.device.tzdef)); - } else if (selected == 13) { // AU/AWST - strncpy(config.device.tzdef, "AWST-8", sizeof(config.device.tzdef)); - } else if (selected == 14) { // AU/ACST - strncpy(config.device.tzdef, "ACST-9:30ACDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef)); - } else if (selected == 15) { // AU/AEST - strncpy(config.device.tzdef, "AEST-10AEDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef)); - } else if (selected == 16) { // NZ - strncpy(config.device.tzdef, "NZST-12NZDT,M9.5.0,M4.1.0/3", sizeof(config.device.tzdef)); - } - if (selected != 0) { - setenv("TZ", config.device.tzdef, 1); - service->reloadConfig(SEGMENT_CONFIG); - } - }); -} - } // namespace graphics #else diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 8a836edfc..ac7d9aa69 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -24,6 +24,7 @@ class Screen FOCUS_FAULT, FOCUS_TEXTMESSAGE, FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus + FOCUS_CLOCK, }; explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY); @@ -38,8 +39,8 @@ class Screen void setFunctionSymbol(std::string) {} void removeFunctionSymbol(std::string) {} void startAlert(const char *) {} - void showOverlayBanner(const char *message, uint32_t durationMs = 3000, uint8_t options = 0, - std::function bannerCallback = NULL, int8_t InitialSelected = 0) + void showOverlayBanner(const char *message, uint32_t durationMs = 3000, const char **optionsArrayPtr = nullptr, + uint8_t options = 0, std::function bannerCallback = NULL, int8_t InitialSelected = 0) { } void setFrames(FrameFocus focus) {} @@ -209,6 +210,7 @@ class Screen : public concurrency::OSThread FOCUS_FAULT, FOCUS_TEXTMESSAGE, FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus + FOCUS_CLOCK, }; // Regenerate the normal set of frames, focusing a specific frame if requested @@ -223,6 +225,8 @@ class Screen : public concurrency::OSThread meshtastic_Config_DisplayConfig_OledType model; OLEDDISPLAY_GEOMETRY geometry; + bool ignoreCompass = false; + bool isOverlayBannerShowing(); // Stores the last 4 of our hardware ID, to make finding the device for pairing easier @@ -286,8 +290,8 @@ class Screen : public concurrency::OSThread enqueueCmd(cmd); } - void showOverlayBanner(const char *message, uint32_t durationMs = 3000, uint8_t options = 0, - std::function bannerCallback = NULL, int8_t InitialSelected = 0); + void showOverlayBanner(const char *message, uint32_t durationMs = 3000, const char **optionsArrayPtr = nullptr, + uint8_t options = 0, std::function bannerCallback = NULL, int8_t InitialSelected = 0); void startFirmwareUpdateScreen() { @@ -301,7 +305,7 @@ class Screen : public concurrency::OSThread void setHeading(long _heading) { hasCompass = true; - compassHeading = _heading; + compassHeading = fmod(_heading, 360); } bool hasHeading() { return hasCompass; } @@ -602,8 +606,6 @@ class Screen : public concurrency::OSThread void handleShowNextFrame(); void handleShowPrevFrame(); void handleStartFirmwareUpdateScreen(); - void TZPicker(); - void LoraRegionPicker(uint32_t duration = 30000); // Info collected by setFrames method. // Index location of specific frames. @@ -612,7 +614,6 @@ class Screen : public concurrency::OSThread struct FramesetInfo { struct FramePositions { uint8_t fault = 255; - uint8_t textMessage = 255; uint8_t waypoint = 255; uint8_t focusedModule = 255; uint8_t log = 255; @@ -622,6 +623,12 @@ class Screen : public concurrency::OSThread uint8_t memory = 255; uint8_t gps = 255; uint8_t home = 255; + uint8_t textMessage = 255; + uint8_t nodelist = 255; + uint8_t nodelist_lastheard = 255; + uint8_t nodelist_hopsignal = 255; + uint8_t nodelist_distance = 255; + uint8_t nodelist_bearings = 255; uint8_t clock = 255; uint8_t firstFavorite = 255; uint8_t lastFavorite = 255; @@ -679,5 +686,6 @@ class Screen : public concurrency::OSThread // Extern declarations for function symbols used in UIRenderer extern std::vector functionSymbol; extern std::string functionSymbolString; +extern graphics::Screen *screen; #endif \ No newline at end of file diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index af427cae4..07f2e5cde 100644 --- a/src/graphics/SharedUIDisplay.cpp +++ b/src/graphics/SharedUIDisplay.cpp @@ -10,9 +10,22 @@ namespace graphics { +void determineResolution(int16_t screenheight, int16_t screenwidth) +{ + if (screenwidth > 128) { + isHighResolution = true; + } + + // Special case for Heltec Wireless Tracker v1.1 + if (screenwidth == 160 && screenheight == 80) { + isHighResolution = false; + } +} + // === Shared External State === bool hasUnreadMessage = false; bool isMuted = false; +bool isHighResolution = false; // === Internal State === bool isBoltVisibleShared = true; @@ -40,7 +53,7 @@ void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, // ************************* // * Common Header Drawing * // ************************* -void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr) +void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool battery_only) { constexpr int HEADER_OFFSET_Y = 1; y += HEADER_OFFSET_Y; @@ -56,34 +69,40 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti const int screenW = display->getWidth(); const int screenH = display->getHeight(); - const bool useBigIcons = (screenW > 128); - - // === Inverted Header Background === - if (isInverted) { - drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2); - display->setColor(BLACK); - } else { - display->setColor(BLACK); - display->fillRect(0, 0, screenW, highlightHeight + 3); - display->setColor(WHITE); - if (screenW > 128) { - display->drawLine(0, 20, screenW, 20); + if (!battery_only) { + // === Inverted Header Background === + if (isInverted) { + display->setColor(BLACK); + display->fillRect(0, 0, screenW, highlightHeight + 2); + display->setColor(WHITE); + drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2); + display->setColor(BLACK); } else { - display->drawLine(0, 14, screenW, 14); + display->setColor(BLACK); + display->fillRect(0, 0, screenW, highlightHeight + 2); + display->setColor(WHITE); + if (isHighResolution) { + display->drawLine(0, 20, screenW, 20); + } else { + display->drawLine(0, 14, screenW, 14); + } } - } - // === Screen Title === - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(SCREEN_WIDTH / 2, y, titleStr); - if (config.display.heading_bold) { - display->drawString((SCREEN_WIDTH / 2) + 1, y, titleStr); + // === Screen Title === + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(SCREEN_WIDTH / 2, y, titleStr); + if (config.display.heading_bold) { + display->drawString((SCREEN_WIDTH / 2) + 1, y, titleStr); + } } display->setTextAlignment(TEXT_ALIGN_LEFT); // === Battery State === int chargePercent = powerStatus->getBatteryChargePercent(); bool isCharging = powerStatus->getIsCharging() == meshtastic::OptionalBool::OptTrue; + if (chargePercent == 100) { + isCharging = false; + } uint32_t now = millis(); #ifndef USE_EINK @@ -93,20 +112,22 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti } #endif - bool useHorizontalBattery = (screenW > 128 && screenW >= screenH); + bool useHorizontalBattery = (isHighResolution && screenW >= screenH); const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; // === Battery Icons === if (useHorizontalBattery) { int batteryX = 2; - int batteryY = HEADER_OFFSET_Y + 2; - display->drawXbm(batteryX, batteryY, 29, 15, batteryBitmap_h); + int batteryY = HEADER_OFFSET_Y + 3; + display->drawXbm(batteryX, batteryY, 9, 13, batteryBitmap_h_bottom); + display->drawXbm(batteryX + 9, batteryY, 9, 13, batteryBitmap_h_top); if (isCharging && isBoltVisibleShared) - display->drawXbm(batteryX + 9, batteryY + 1, 9, 13, lightning_bolt_h); + display->drawXbm(batteryX + 4, batteryY, 9, 13, lightning_bolt_h); else { - display->drawXbm(batteryX + 8, batteryY, 12, 15, batteryBitmap_sidegaps_h); - int fillWidth = 24 * chargePercent / 100; - display->fillRect(batteryX + 1, batteryY + 1, fillWidth, 13); + display->drawLine(batteryX + 5, batteryY, batteryX + 10, batteryY); + display->drawLine(batteryX + 5, batteryY + 12, batteryX + 10, batteryY + 12); + int fillWidth = 14 * chargePercent / 100; + display->fillRect(batteryX + 1, batteryY + 1, fillWidth, 11); } } else { int batteryX = 1; @@ -129,12 +150,8 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti char chargeStr[4]; snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent); int chargeNumWidth = display->getStringWidth(chargeStr); - const int batteryOffset = useHorizontalBattery ? 28 : 6; -#ifdef USE_EINK - const int percentX = x + xOffset + batteryOffset - 2; -#else - const int percentX = x + xOffset + batteryOffset; -#endif + const int batteryOffset = useHorizontalBattery ? 19 : 9; + const int percentX = x + batteryOffset; display->drawString(percentX, textY, chargeStr); display->drawString(percentX + chargeNumWidth - 1, textY, "%"); if (isBold) { @@ -148,7 +165,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti int timeStrWidth = display->getStringWidth("12:34"); // Default alignment int timeX = screenW - xOffset - timeStrWidth + 4; - if (rtc_sec > 0) { + if (rtc_sec > 0 && !battery_only) { // === Build Time String === long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY; int hour = hms / SEC_PER_HOUR; @@ -164,7 +181,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti } timeStrWidth = display->getStringWidth(timeStr); - timeX = screenW - xOffset - timeStrWidth + 4; + timeX = screenW - xOffset - timeStrWidth + 3; // === Show Mail or Mute Icon to the Left of Time === int iconRightEdge = timeX - 1; @@ -217,7 +234,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti display->drawXbm(iconX, iconY, mail_width, mail_height, mail); } } else if (isMuted) { - if (useBigIcons) { + if (isHighResolution) { int iconX = iconRightEdge - mute_symbol_big_width; int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2; @@ -259,6 +276,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti bool showMail = false; +#ifndef USE_EINK if (hasUnreadMessage) { if (now - lastMailBlink > 500) { isMailIconVisible = !isMailIconVisible; @@ -266,6 +284,11 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti } showMail = isMailIconVisible; } +#else + if (hasUnreadMessage) { + showMail = true; + } +#endif if (showMail) { if (useHorizontalBattery) { @@ -281,7 +304,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti display->drawXbm(iconX, iconY, mail_width, mail_height, mail); } } else if (isMuted) { - if (useBigIcons) { + if (isHighResolution) { int iconX = iconRightEdge - mute_symbol_big_width; int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2; display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big); @@ -300,7 +323,7 @@ const int *getTextPositions(OLEDDisplay *display) { static int textPositions[7]; // Static array that persists beyond function scope - if (display->getHeight() > 64) { + if (isHighResolution) { textPositions[0] = textZeroLine; textPositions[1] = textFirstLine_medium; textPositions[2] = textSecondLine_medium; diff --git a/src/graphics/SharedUIDisplay.h b/src/graphics/SharedUIDisplay.h index 41411ba7f..2e97052a8 100644 --- a/src/graphics/SharedUIDisplay.h +++ b/src/graphics/SharedUIDisplay.h @@ -41,12 +41,14 @@ namespace graphics // Shared state (declare inside namespace) extern bool hasUnreadMessage; extern bool isMuted; +extern bool isHighResolution; +void determineResolution(int16_t screenheight, int16_t screenwidth); // Rounded highlight (used for inverted headers) void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r); // Shared battery/time/mail header -void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = ""); +void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "", bool battery_only = false); const int *getTextPositions(OLEDDisplay *display); diff --git a/src/graphics/draw/ClockRenderer.cpp b/src/graphics/draw/ClockRenderer.cpp index 2e301b4e1..aa177078b 100644 --- a/src/graphics/draw/ClockRenderer.cpp +++ b/src/graphics/draw/ClockRenderer.cpp @@ -21,6 +21,7 @@ namespace graphics namespace ClockRenderer { +bool digitalWatchFace = true; void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale) { @@ -146,6 +147,7 @@ void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int heig display->fillTriangle(x, y + width, x + height - 1, y + width, x + halfHeight, y + width + halfHeight); } +/* void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode, float scale) { uint16_t segmentWidth = SEGMENT_WIDTH * scale; @@ -179,21 +181,22 @@ void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool drawVerticalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight); } } - +*/ // Draw a digital clock void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { display->clear(); display->setTextAlignment(TEXT_ALIGN_LEFT); int line = 1; + // === Set Title, Blank for Clock + const char *titleStr = ""; + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr, true); #ifdef T_WATCH_S3 if (nimbleBluetooth && nimbleBluetooth->isConnected()) { - graphics::ClockRenderer::drawBluetoothConnectedIcon(display, display->getWidth() - 18, y + 2); + graphics::ClockRenderer::drawBluetoothConnectedIcon(display, display->getWidth() - 18, display->getHeight() - 14); } - - drawWatchFaceToggleButton(display, display->getWidth() - 36, display->getHeight() - 36, - graphics::ClockRenderer::digitalWatchFace, 1); #endif uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone @@ -230,7 +233,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1 float scale = 1.5; #else float scale = 0.75; - if (SCREEN_WIDTH > 128) { + if (isHighResolution) { scale = 1.5; } #endif @@ -276,17 +279,17 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1 // draw seconds string display->setFont(FONT_SMALL); - int xOffset = (SCREEN_WIDTH > 128) ? 0 : -1; + int xOffset = (isHighResolution) ? 0 : -1; if (hour >= 10) { - xOffset += (SCREEN_WIDTH > 128) ? 32 : 18; + xOffset += (isHighResolution) ? 32 : 18; } - int yOffset = (SCREEN_WIDTH > 128) ? 3 : 1; + int yOffset = (isHighResolution) ? 3 : 1; if (config.display.use_12h_clock) { display->drawString(startingHourMinuteTextX + xOffset, (display->getHeight() - hourMinuteTextY) - yOffset - 2, isPM ? "pm" : "am"); } #ifndef USE_EINK - xOffset = (SCREEN_WIDTH > 128) ? 18 : 10; + xOffset = (isHighResolution) ? 18 : 10; display->drawString(startingHourMinuteTextX + timeStringWidth - xOffset, (display->getHeight() - hourMinuteTextY) - yOffset, secondString); #endif @@ -301,31 +304,30 @@ void drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y) void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { display->setTextAlignment(TEXT_ALIGN_LEFT); + // === Set Title, Blank for Clock + const char *titleStr = ""; + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr, true); - graphics::UIRenderer::drawBattery(display, x, y + 7, imgBattery, powerStatus); - - if (powerStatus->getHasBattery()) { - char batteryPercent[8]; - snprintf(batteryPercent, sizeof(batteryPercent), "%d%%", powerStatus->getBatteryChargePercent()); - - display->setFont(FONT_SMALL); - - display->drawString(x + 20, y + 2, batteryPercent); - } #ifdef T_WATCH_S3 if (nimbleBluetooth && nimbleBluetooth->isConnected()) { - drawBluetoothConnectedIcon(display, display->getWidth() - 18, y + 2); + drawBluetoothConnectedIcon(display, display->getWidth() - 18, display->getHeight() - 14); } #endif - drawWatchFaceToggleButton(display, display->getWidth() - 36, display->getHeight() - 36, - graphics::ClockRenderer::digitalWatchFace, 1); - // clock face center coordinates int16_t centerX = display->getWidth() / 2; int16_t centerY = display->getHeight() / 2; // clock face radius - int16_t radius = (display->getWidth() / 2) * 0.8; + int16_t radius = 0; + if (display->getHeight() < display->getWidth()) { + radius = (display->getHeight() / 2) * 0.9; + } else { + radius = (display->getWidth() / 2) * 0.9; + } +#ifdef T_WATCH_S3 + radius = (display->getWidth() / 2) * 0.8; +#endif // noon (0 deg) coordinates (outermost circle) int16_t noonX = centerX; @@ -338,10 +340,16 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 int16_t tickMarkOuterNoonY = secondHandNoonY; // seconds tick mark inner y coordinate; (second nested circle) - double secondsTickMarkInnerNoonY = (double)noonY + 8; + double secondsTickMarkInnerNoonY = (double)noonY + 4; + if (isHighResolution) { + secondsTickMarkInnerNoonY = (double)noonY + 8; + } // hours tick mark inner y coordinate; (third nested circle) - double hoursTickMarkInnerNoonY = (double)noonY + 16; + double hoursTickMarkInnerNoonY = (double)noonY + 6; + if (isHighResolution) { + hoursTickMarkInnerNoonY = (double)noonY + 16; + } // minute hand y coordinate int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4; @@ -350,7 +358,10 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 int16_t hourStringNoonY = minuteHandNoonY + 18; // hour hand radius and y coordinate - int16_t hourHandRadius = radius * 0.55; + int16_t hourHandRadius = radius * 0.35; + if (isHighResolution) { + int16_t hourHandRadius = radius * 0.55; + } int16_t hourHandNoonY = centerY - hourHandRadius; display->setColor(OLEDDISPLAY_COLOR::WHITE); @@ -366,7 +377,20 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; int second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN - hour = hour > 12 ? hour - 12 : hour; + bool isPM = hour >= 12; + if (config.display.use_12h_clock) { + bool isPM = hour >= 12; + display->setFont(FONT_SMALL); + int yOffset = isHighResolution ? 1 : 0; +#ifdef USE_EINK + yOffset += 3; +#endif + display->drawString(centerX - (display->getStringWidth(isPM ? "pm" : "am") / 2), centerY + yOffset, + isPM ? "pm" : "am"); + } + hour %= 12; + if (hour == 0) + hour = 12; int16_t degreesPerHour = 30; int16_t degreesPerMinuteOrSecond = 6; @@ -443,16 +467,32 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 double hourStringX = (sineAngleInRadians * (hourStringNoonY - centerY) + noonX) - hourStringXOffset; double hourStringY = (cosineAngleInRadians * (hourStringNoonY - centerY) + centerY) - hourStringYOffset; +#ifdef T_WATCH_S3 // draw hour number display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); +#else +#ifdef USE_EINK + if (isHighResolution) { + // draw hour number + display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); + } +#else + if (isHighResolution && (hourInt == 3 || hourInt == 6 || hourInt == 9 || hourInt == 12)) { + // draw hour number + display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); + } +#endif +#endif } if (angle % degreesPerMinuteOrSecond == 0) { double startX = sineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + noonX; double startY = cosineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + centerY; - // draw minute tick mark - display->drawLine(startX, startY, endX, endY); + if (isHighResolution) { + // draw minute tick mark + display->drawLine(startX, startY, endX, endY); + } } } @@ -461,9 +501,10 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 // draw minute hand display->drawLine(centerX, centerY, minuteX, minuteY); - +#ifndef USE_EINK // draw second hand display->drawLine(centerX, centerY, secondX, secondY); +#endif } } diff --git a/src/graphics/draw/ClockRenderer.h b/src/graphics/draw/ClockRenderer.h index 4660dcc35..9c3238b14 100644 --- a/src/graphics/draw/ClockRenderer.h +++ b/src/graphics/draw/ClockRenderer.h @@ -12,7 +12,7 @@ class Screen; namespace ClockRenderer { // Whether we are showing the digital watch face or the analog one -static bool digitalWatchFace = true; +extern bool digitalWatchFace; // Clock frame functions void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); @@ -25,7 +25,7 @@ void drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int he void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int height); // UI elements for clock displays -void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode = true, float scale = 1); +// void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode = true, float scale = 1); void drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y); } // namespace ClockRenderer diff --git a/src/graphics/draw/CompassRenderer.cpp b/src/graphics/draw/CompassRenderer.cpp index fef993e2d..6d8051546 100644 --- a/src/graphics/draw/CompassRenderer.cpp +++ b/src/graphics/draw/CompassRenderer.cpp @@ -4,6 +4,7 @@ #include "configuration.h" #include "gps/GeoCoord.h" #include "graphics/ScreenFonts.h" +#include "graphics/SharedUIDisplay.h" #include namespace graphics @@ -45,17 +46,18 @@ void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, // This could draw a "N" indicator or north arrow // For now, we'll draw a simple north indicator // const float radius = 17.0f; - if (display->width() > 128) { + if (isHighResolution) { radius += 4; } Point north(0, -radius); - north.rotate(-myHeading); + if (!config.display.compass_north_top) + north.rotate(-myHeading); north.translate(compassX, compassY); display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_CENTER); display->setColor(BLACK); - if (display->width() > 128) { + if (isHighResolution) { display->fillRect(north.x - 8, north.y - 1, display->getStringWidth("N") + 3, FONT_HEIGHT_SMALL - 6); } else { display->fillRect(north.x - 4, north.y - 1, display->getStringWidth("N") + 2, FONT_HEIGHT_SMALL - 6); @@ -91,18 +93,22 @@ void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, f float radians = bearing * DEG_TO_RAD; Point tip(0, -size / 2); - Point left(-size / 4, size / 4); - Point right(size / 4, size / 4); + Point left(-size / 6, size / 4); + Point right(size / 6, size / 4); + Point tail(0, size / 4.5); tip.rotate(radians); left.rotate(radians); right.rotate(radians); + tail.rotate(radians); tip.translate(x, y); left.translate(x, y); right.translate(x, y); + tail.translate(x, y); - display->drawTriangle(tip.x, tip.y, left.x, left.y, right.x, right.y); + display->fillTriangle(tip.x, tip.y, left.x, left.y, tail.x, tail.y); + display->fillTriangle(tip.x, tip.y, right.x, right.y, tail.x, tail.y); } float estimatedHeading(double lat, double lon) @@ -127,14 +133,5 @@ uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight) return maxDiam; } -float calculateBearing(double lat1, double lon1, double lat2, double lon2) -{ - double dLon = (lon2 - lon1) * DEG_TO_RAD; - double y = sin(dLon) * cos(lat2 * DEG_TO_RAD); - double x = cos(lat1 * DEG_TO_RAD) * sin(lat2 * DEG_TO_RAD) - sin(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * cos(dLon); - double bearing = atan2(y, x) * RAD_TO_DEG; - return fmod(bearing + 360.0, 360.0); -} - } // namespace CompassRenderer } // namespace graphics diff --git a/src/graphics/draw/CompassRenderer.h b/src/graphics/draw/CompassRenderer.h index 4b26e6463..ca7532b66 100644 --- a/src/graphics/draw/CompassRenderer.h +++ b/src/graphics/draw/CompassRenderer.h @@ -28,9 +28,6 @@ void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, f float estimatedHeading(double lat, double lon); uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight); -// Utility functions for bearing calculations -float calculateBearing(double lat1, double lon1, double lat2, double lon2); - } // namespace CompassRenderer } // namespace graphics diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index 2c3a3a3a8..92cf49610 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -67,21 +67,6 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16 char channelStr[20]; snprintf(channelStr, sizeof(channelStr), "#%s", channels.getName(channels.getPrimaryIndex())); - - // Display power status - if (powerStatus->getHasBattery()) { - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { - UIRenderer::drawBattery(display, x, y + 2, imgBattery, powerStatus); - } else { - UIRenderer::drawBattery(display, x + 1, y + 3, imgBattery, powerStatus); - } - } else if (powerStatus->knowsUSB()) { - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { - display->drawFastImage(x, y + 2, 16, 8, powerStatus->getHasUSB() ? imgUSB : imgPower); - } else { - display->drawFastImage(x + 1, y + 3, 16, 8, powerStatus->getHasUSB() ? imgUSB : imgPower); - } - } // Display nodes status if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { UIRenderer::drawNodes(display, x + (SCREEN_WIDTH * 0.25), y + 2, nodeStatus); @@ -393,7 +378,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int line = 1; // === Set Title - const char *titleStr = (SCREEN_WIDTH > 128) ? "LoRa Info" : "LoRa"; + const char *titleStr = (isHighResolution) ? "LoRa Info" : "LoRa"; // === Header === graphics::drawCommonHeader(display, x, y, titleStr); @@ -444,12 +429,12 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, char chUtilPercentage[10]; snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); - int chUtil_x = (SCREEN_WIDTH > 128) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5; + int chUtil_x = (isHighResolution) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5; int chUtil_y = getTextPositions(display)[line] + 3; - int chutil_bar_width = (SCREEN_WIDTH > 128) ? 100 : 50; - int chutil_bar_height = (SCREEN_WIDTH > 128) ? 12 : 7; - int extraoffset = (SCREEN_WIDTH > 128) ? 6 : 3; + int chutil_bar_width = (isHighResolution) ? 100 : 50; + int chutil_bar_height = (isHighResolution) ? 12 : 7; + int extraoffset = (isHighResolution) ? 6 : 3; int chutil_percent = airTime->channelUtilizationPercent(); int centerofscreen = SCREEN_WIDTH / 2; @@ -516,7 +501,7 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int line = 1; const int barHeight = 6; const int labelX = x; - const int barsOffset = (SCREEN_WIDTH > 128) ? 24 : 0; + const int barsOffset = (isHighResolution) ? 24 : 0; const int barX = x + 40 + barsOffset; auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) { @@ -526,7 +511,7 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int percent = (used * 100) / total; char combinedStr[24]; - if (SCREEN_WIDTH > 128) { + if (isHighResolution) { snprintf(combinedStr, sizeof(combinedStr), "%s%3d%% %u/%uKB", (percent > 80) ? "! " : "", percent, used / 1024, total / 1024); } else { diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp new file mode 100644 index 000000000..1c327117e --- /dev/null +++ b/src/graphics/draw/MenuHandler.cpp @@ -0,0 +1,479 @@ +#include "configuration.h" +#if HAS_SCREEN +#include "ClockRenderer.h" +#include "GPS.h" +#include "MenuHandler.h" +#include "MeshRadio.h" +#include "MeshService.h" +#include "NodeDB.h" +#include "buzz.h" +#include "graphics/Screen.h" +#include "graphics/draw/UIRenderer.h" +#include "main.h" +#include "modules/AdminModule.h" +#include "modules/CannedMessageModule.h" + +namespace graphics +{ +menuHandler::screenMenus menuHandler::menuQueue = menu_none; + +void menuHandler::LoraRegionPicker(uint32_t duration) +{ + static const char *optionsArray[] = {"Back", + "US", + "EU_433", + "EU_868", + "CN", + "JP", + "ANZ", + "KR", + "TW", + "RU", + "IN", + "NZ_865", + "TH", + "LORA_24", + "UA_433", + "UA_868", + "MY_433", + "MY_" + "919", + "SG_" + "923", + "PH_433", + "PH_868", + "PH_915", + "ANZ_433"}; + screen->showOverlayBanner( + "Set the LoRa region", duration, optionsArray, 23, + [](int selected) -> void { + if (selected != 0 && config.lora.region != _meshtastic_Config_LoRaConfig_RegionCode(selected)) { + config.lora.region = _meshtastic_Config_LoRaConfig_RegionCode(selected); + // This is needed as we wait til picking the LoRa region to generate keys for the first time. + if (!owner.is_licensed) { + bool keygenSuccess = false; + if (config.security.private_key.size == 32) { + // public key is derived from private, so this will always have the same result. + if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { + keygenSuccess = true; + } + } else { + LOG_INFO("Generate new PKI keys"); + crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes); + keygenSuccess = true; + } + if (keygenSuccess) { + config.security.public_key.size = 32; + config.security.private_key.size = 32; + owner.public_key.size = 32; + memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32); + } + } + config.lora.tx_enabled = true; + initRegion(); + if (myRegion->dutyCycle < 100) { + config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit + } + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } + }, + 0); +} + +void menuHandler::TwelveHourPicker() +{ + static const char *optionsArray[] = {"Back", "12-hour", "24-hour"}; + screen->showOverlayBanner("Time Format", 30000, optionsArray, 3, [](int selected) -> void { + if (selected == 0) { + menuHandler::menuQueue = menuHandler::clock_menu; + } else if (selected == 1) { + config.display.use_12h_clock = true; + } else { + config.display.use_12h_clock = false; + } + service->reloadConfig(SEGMENT_CONFIG); + }); +} + +void menuHandler::ClockFacePicker() +{ + static const char *optionsArray[] = {"Back", "Digital", "Analog"}; + screen->showOverlayBanner("Which Face?", 30000, optionsArray, 3, [](int selected) -> void { + if (selected == 0) { + menuHandler::menuQueue = menuHandler::clock_menu; + } else if (selected == 1) { + graphics::ClockRenderer::digitalWatchFace = true; + screen->setFrames(Screen::FOCUS_CLOCK); + } else { + graphics::ClockRenderer::digitalWatchFace = false; + screen->setFrames(Screen::FOCUS_CLOCK); + } + }); +} + +void menuHandler::TZPicker() +{ + static const char *optionsArray[] = {"Back", + "US/Hawaii", + "US/Alaska", + "US/Pacific", + "US/Arizona", + "US/Mountain", + "US/Central", + "US/Eastern", + "UTC", + "EU/Western", + "EU/" + "Central", + "EU/Eastern", + "Asia/Kolkata", + "Asia/Hong_Kong", + "AU/AWST", + "AU/ACST", + "AU/AEST", + "Pacific/NZ"}; + screen->showOverlayBanner("Pick Timezone", 30000, optionsArray, 17, [](int selected) -> void { + if (selected == 0) { + menuHandler::menuQueue = menuHandler::clock_menu; + } else if (selected == 1) { // Hawaii + strncpy(config.device.tzdef, "HST10", sizeof(config.device.tzdef)); + } else if (selected == 2) { // Alaska + strncpy(config.device.tzdef, "AKST9AKDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); + } else if (selected == 3) { // Pacific + strncpy(config.device.tzdef, "PST8PDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); + } else if (selected == 4) { // Arizona + strncpy(config.device.tzdef, "MST7", sizeof(config.device.tzdef)); + } else if (selected == 5) { // Mountain + strncpy(config.device.tzdef, "MST7MDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); + } else if (selected == 6) { // Central + strncpy(config.device.tzdef, "CST6CDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); + } else if (selected == 7) { // Eastern + strncpy(config.device.tzdef, "EST5EDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef)); + } else if (selected == 8) { // UTC + strncpy(config.device.tzdef, "UTC", sizeof(config.device.tzdef)); + } else if (selected == 9) { // EU/Western + strncpy(config.device.tzdef, "GMT0BST,M3.5.0/1,M10.5.0", sizeof(config.device.tzdef)); + } else if (selected == 10) { // EU/Central + strncpy(config.device.tzdef, "CET-1CEST,M3.5.0,M10.5.0/3", sizeof(config.device.tzdef)); + } else if (selected == 11) { // EU/Eastern + strncpy(config.device.tzdef, "EET-2EEST,M3.5.0/3,M10.5.0/4", sizeof(config.device.tzdef)); + } else if (selected == 12) { // Asia/Kolkata + strncpy(config.device.tzdef, "IST-5:30", sizeof(config.device.tzdef)); + } else if (selected == 13) { // China + strncpy(config.device.tzdef, "HKT-8", sizeof(config.device.tzdef)); + } else if (selected == 14) { // AU/AWST + strncpy(config.device.tzdef, "AWST-8", sizeof(config.device.tzdef)); + } else if (selected == 15) { // AU/ACST + strncpy(config.device.tzdef, "ACST-9:30ACDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef)); + } else if (selected == 16) { // AU/AEST + strncpy(config.device.tzdef, "AEST-10AEDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef)); + } else if (selected == 17) { // NZ + strncpy(config.device.tzdef, "NZST-12NZDT,M9.5.0,M4.1.0/3", sizeof(config.device.tzdef)); + } + if (selected != 0) { + setenv("TZ", config.device.tzdef, 1); + service->reloadConfig(SEGMENT_CONFIG); + } + }); +} + +void menuHandler::clockMenu() +{ + static const char *optionsArray[] = {"Back", "Clock Face", "Time Format", "Timezone"}; + screen->showOverlayBanner("Clock Action", 30000, optionsArray, 4, [](int selected) -> void { + if (selected == 1) { + menuHandler::menuQueue = menuHandler::clock_face_picker; + screen->setInterval(0); + runASAP = true; + } else if (selected == 2) { + menuHandler::menuQueue = menuHandler::twelve_hour_picker; + screen->setInterval(0); + runASAP = true; + } else if (selected == 3) { + menuHandler::menuQueue = menuHandler::TZ_picker; + screen->setInterval(0); + runASAP = true; + } + }); +} + +void menuHandler::messageResponseMenu() +{ + + static const char **optionsArrayPtr; + int options; + if (kb_found) { + static const char *optionsArray[] = {"Back", "Dismiss", "Reply via Preset", "Reply via Freetext"}; + optionsArrayPtr = optionsArray; + options = 4; + } else { + static const char *optionsArray[] = {"Back", "Dismiss", "Reply via Preset"}; + optionsArrayPtr = optionsArray; + options = 3; + } +#ifdef HAS_I2S + static const char *optionsArray[] = {"Back", "Dismiss", "Reply via Preset", "Reply via Freetext", "Read Aloud"}; + optionsArrayPtr = optionsArray; + options = 5; +#endif + screen->showOverlayBanner("Message Action", 30000, optionsArrayPtr, options, [](int selected) -> void { + if (selected == 1) { + screen->dismissCurrentFrame(); + } else if (selected == 2) { + if (devicestate.rx_text_message.to == NODENUM_BROADCAST) { + cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel); + } else { + cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from); + } + } else if (selected == 3) { + if (devicestate.rx_text_message.to == NODENUM_BROADCAST) { + cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel); + } else { + cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from); + } + } +#ifdef HAS_I2S + else if (selected == 4) { + const meshtastic_MeshPacket &mp = devicestate.rx_text_message; + const char *msg = reinterpret_cast(mp.decoded.payload.bytes); + + audioThread->readAloud(msg); + } +#endif + }); +} + +void menuHandler::homeBaseMenu() +{ + int options; + static const char **optionsArrayPtr; + + if (kb_found) { +#ifdef PIN_EINK_EN + static const char *optionsArray[] = {"Back", "Toggle Backlight", "Send Position", "New Preset Msg", "New Freetext Msg"}; +#else + static const char *optionsArray[] = {"Back", "Sleep Screen", "Send Position", "New Preset Msg", "New Freetext Msg"}; +#endif + optionsArrayPtr = optionsArray; + options = 5; + } else { +#ifdef PIN_EINK_EN + static const char *optionsArray[] = {"Back", "Toggle Backlight", "Send Position", "New Preset Msg"}; +#else + static const char *optionsArray[] = {"Back", "Sleep Screen", "Send Position", "New Preset Msg"}; +#endif + optionsArrayPtr = optionsArray; + options = 4; + } + screen->showOverlayBanner("Home Action", 30000, optionsArrayPtr, options, [](int selected) -> void { + if (selected == 1) { +#ifdef PIN_EINK_EN + if (digitalRead(PIN_EINK_EN) == HIGH) { + digitalWrite(PIN_EINK_EN, LOW); + } else { + digitalWrite(PIN_EINK_EN, HIGH); + } +#else + screen->setOn(false); +#endif + } else if (selected == 2) { + InputEvent event = {.inputEvent = (input_broker_event)175, .kbchar = 175, .touchX = 0, .touchY = 0}; + inputBroker->injectInputEvent(&event); + } else if (selected == 3) { + cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST); + } else if (selected == 4) { + cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST); + } + }); +} + +void menuHandler::favoriteBaseMenu() +{ + int options; + static const char **optionsArrayPtr; + + if (kb_found) { + static const char *optionsArray[] = {"Back", "New Preset Msg", "New Freetext Msg"}; + optionsArrayPtr = optionsArray; + options = 3; + } else { + static const char *optionsArray[] = {"Back", "New Preset Msg"}; + optionsArrayPtr = optionsArray; + options = 2; + } + screen->showOverlayBanner("Favorites Action", 30000, optionsArrayPtr, options, [](int selected) -> void { + if (selected == 1) { + cannedMessageModule->LaunchWithDestination(graphics::UIRenderer::currentFavoriteNodeNum); + } else if (selected == 2) { + cannedMessageModule->LaunchFreetextWithDestination(graphics::UIRenderer::currentFavoriteNodeNum); + } + }); +} + +void menuHandler::positionBaseMenu() +{ + int options; + static const char **optionsArrayPtr; + static const char *optionsArray[] = {"Back", "GPS Toggle", "Compass"}; + static const char *optionsArrayCalibrate[] = {"Back", "GPS Toggle", "Compass", "Compass Calibrate"}; + + if (accelerometerThread) { + optionsArrayPtr = optionsArrayCalibrate; + options = 4; + } else { + optionsArrayPtr = optionsArray; + options = 3; + } + screen->showOverlayBanner("Position Action", 30000, optionsArrayPtr, options, [](int selected) -> void { + if (selected == 1) { + menuQueue = gps_toggle_menu; + } else if (selected == 2) { + menuQueue = compass_point_north_menu; + } else if (selected == 3) { + accelerometerThread->calibrate(30); + } + }); +} + +void menuHandler::nodeListMenu() +{ + static const char *optionsArray[] = {"Back", "Reset NodeDB"}; + screen->showOverlayBanner("Node Action", 30000, optionsArray, 2, [](int selected) -> void { + if (selected == 1) { + menuQueue = reset_node_db_menu; + } + }); +} + +void menuHandler::resetNodeDBMenu() +{ + static const char *optionsArray[] = {"Back", "Confirm"}; + screen->showOverlayBanner("Confirm Reset NodeDB", 30000, optionsArray, 2, [](int selected) -> void { + if (selected == 1) { + disableBluetooth(); + LOG_INFO("Initiate node-db reset"); + nodeDB->resetNodes(); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } + }); +} + +void menuHandler::compassNorthMenu() +{ + static const char *optionsArray[] = {"Back", "Dynamic", "Fixed Ring", "Freeze Heading"}; + screen->showOverlayBanner("North Directions?", 30000, optionsArray, 4, [](int selected) -> void { + if (selected == 1) { + if (config.display.compass_north_top != false) { + config.display.compass_north_top = false; + service->reloadConfig(SEGMENT_CONFIG); + } + screen->ignoreCompass = false; + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + } else if (selected == 2) { + if (config.display.compass_north_top != true) { + config.display.compass_north_top = true; + service->reloadConfig(SEGMENT_CONFIG); + } + screen->ignoreCompass = false; + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + } else if (selected == 3) { + if (config.display.compass_north_top != true) { + config.display.compass_north_top = true; + service->reloadConfig(SEGMENT_CONFIG); + } + screen->ignoreCompass = true; + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + } else if (selected == 0) { + menuQueue = position_base_menu; + } + }); +} + +void menuHandler::GPSToggleMenu() +{ + static const char *optionsArray[] = {"Back", "Enabled", "Disabled"}; + screen->showOverlayBanner( + "Toggle GPS", 30000, optionsArray, 3, + [](int selected) -> void { + if (selected == 1) { + config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED; + playGPSEnableBeep(); + gps->enable(); + service->reloadConfig(SEGMENT_CONFIG); + } else if (selected == 2) { + config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED; + playGPSDisableBeep(); + gps->disable(); + service->reloadConfig(SEGMENT_CONFIG); + } else { + menuQueue = position_base_menu; + } + }, + config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED ? 1 : 2); // set inital selection +} + +void menuHandler::BuzzerModeMenu() +{ + static const char *optionsArray[] = {"All Enabled", "Disabled", "Notifications", "System Only"}; + screen->showOverlayBanner( + "Beep Action", 30000, optionsArray, 4, + [](int selected) -> void { + config.device.buzzer_mode = (meshtastic_Config_DeviceConfig_BuzzerMode)selected; + service->reloadConfig(SEGMENT_CONFIG); + }, + config.device.buzzer_mode); +} + +void menuHandler::switchToMUIMenu() +{ + static const char *optionsArray[] = {"Yes", "No"}; + screen->showOverlayBanner("Switch to MUI?", 30000, optionsArray, 2, [](int selected) -> void { + if (selected == 0) { + config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR; + config.bluetooth.enabled = false; + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } + }); +} + +void menuHandler::handleMenuSwitch() +{ + switch (menuQueue) { + case menu_none: + break; + case lora_picker: + LoraRegionPicker(); + break; + case TZ_picker: + TZPicker(); + break; + case twelve_hour_picker: + TwelveHourPicker(); + break; + case clock_face_picker: + ClockFacePicker(); + break; + case clock_menu: + clockMenu(); + break; + case position_base_menu: + positionBaseMenu(); + break; + case gps_toggle_menu: + GPSToggleMenu(); + break; + case compass_point_north_menu: + compassNorthMenu(); + break; + case reset_node_db_menu: + resetNodeDBMenu(); + break; + } + menuQueue = menu_none; +} + +} // namespace graphics + +#endif \ No newline at end of file diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h new file mode 100644 index 000000000..a5bea5176 --- /dev/null +++ b/src/graphics/draw/MenuHandler.h @@ -0,0 +1,40 @@ +#include "configuration.h" +namespace graphics +{ + +class menuHandler +{ + public: + enum screenMenus { + menu_none, + lora_picker, + TZ_picker, + twelve_hour_picker, + clock_face_picker, + clock_menu, + position_base_menu, + gps_toggle_menu, + compass_point_north_menu, + reset_node_db_menu + }; + static screenMenus menuQueue; + + static void LoraRegionPicker(uint32_t duration = 30000); + static void handleMenuSwitch(); + static void clockMenu(); + static void TZPicker(); + static void TwelveHourPicker(); + static void ClockFacePicker(); + static void messageResponseMenu(); + static void homeBaseMenu(); + static void favoriteBaseMenu(); + static void positionBaseMenu(); + static void compassNorthMenu(); + static void GPSToggleMenu(); + static void BuzzerModeMenu(); + static void switchToMUIMenu(); + static void nodeListMenu(); + static void resetNodeDBMenu(); +}; + +} // namespace graphics \ No newline at end of file diff --git a/src/graphics/draw/MessageRenderer.cpp b/src/graphics/draw/MessageRenderer.cpp index 707517d82..3df8a003c 100644 --- a/src/graphics/draw/MessageRenderer.cpp +++ b/src/graphics/draw/MessageRenderer.cpp @@ -56,6 +56,11 @@ namespace graphics namespace MessageRenderer { +// Simple cache based on text hash +static size_t cachedKey = 0; +static std::vector cachedLines; +static std::vector cachedHeights; + void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount) { int cursorX = x; @@ -225,6 +230,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 sender); } + uint32_t now = millis(); #ifndef EXCLUDE_EMOJI // === Bounce animation setup === static uint32_t lastBounceTime = 0; @@ -232,7 +238,6 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 const int bounceRange = 2; // Max pixels to bounce up/down const int bounceInterval = 10; // How quickly to change bounce direction (ms) - uint32_t now = millis(); if (now - lastBounceTime >= bounceInterval) { lastBounceTime = now; bounceY = (bounceY + 1) % (bounceRange * 2); @@ -246,82 +251,51 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 display->drawString(x + 4, headerY, headerStr); // Draw separator (same as scroll version) - for (int separatorX = 0; separatorX <= (display->getStringWidth(headerStr) + 3); separatorX += 2) { - display->setPixel(separatorX, headerY + ((SCREEN_WIDTH > 128) ? 19 : 13)); + for (int separatorX = 1; separatorX <= (display->getStringWidth(headerStr) + 2); separatorX += 2) { + display->setPixel(separatorX, headerY + ((isHighResolution) ? 19 : 13)); } // Center the emote below the header line + separator + nav int remainingHeight = SCREEN_HEIGHT - (headerY + FONT_HEIGHT_SMALL) - navHeight; - int emoteY = headerY + FONT_HEIGHT_SMALL + (remainingHeight - e.height) / 2 + bounceY - bounceRange; + int emoteY = headerY + 6 + FONT_HEIGHT_SMALL + (remainingHeight - e.height) / 2 + bounceY - bounceRange; display->drawXbm((SCREEN_WIDTH - e.width) / 2, emoteY, e.width, e.height, e.bitmap); + + // Draw header at the end to sort out overlapping elements + graphics::drawCommonHeader(display, x, y, titleStr); return; } } #endif + // === Generate the cache key === + size_t currentKey = (size_t)mp.from; + currentKey ^= ((size_t)mp.to << 8); + currentKey ^= ((size_t)mp.rx_time << 16); + currentKey ^= ((size_t)mp.id << 24); - // === Word-wrap and build line list === - std::vector lines; - lines.push_back(std::string(headerStr)); // Header line is always first + if (cachedKey != currentKey) { + LOG_INFO("Message cache key is misssed cachedKey=0x%0x, currentKey=0x%x", cachedKey, currentKey); - std::string line, word; - for (int i = 0; messageBuf[i]; ++i) { - char ch = messageBuf[i]; - if (ch == '\n') { - if (!word.empty()) - line += word; - if (!line.empty()) - lines.push_back(line); - line.clear(); - word.clear(); - } else if (ch == ' ') { - line += word + ' '; - word.clear(); - } else { - word += ch; - std::string test = line + word; - if (display->getStringWidth(test.c_str()) > textWidth) { - if (!line.empty()) - lines.push_back(line); - line = word; - word.clear(); - } - } + // Cache miss - regenerate lines and heights + cachedLines = generateLines(display, headerStr, messageBuf, textWidth); + cachedHeights = calculateLineHeights(cachedLines, emotes); + cachedKey = currentKey; + } else { + // Cache hit but update the header line with current time information + cachedLines[0] = std::string(headerStr); + // The header always has a fixed height since it doesn't contain emotes + // As per calculateLineHeights logic for lines without emotes: + cachedHeights[0] = FONT_HEIGHT_SMALL - 2; + if (cachedHeights[0] < 8) + cachedHeights[0] = 8; // minimum safety } - if (!word.empty()) - line += word; - if (!line.empty()) - lines.push_back(line); // === Scrolling logic === - std::vector rowHeights; - - for (const auto &_line : lines) { - int lineHeight = FONT_HEIGHT_SMALL; - bool hasEmote = false; - - for (int i = 0; i < numEmotes; ++i) { - const Emote &e = emotes[i]; - if (_line.find(e.label) != std::string::npos) { - lineHeight = std::max(lineHeight, e.height); - hasEmote = true; - } - } - - // Apply tighter spacing if no emotes on this line - if (!hasEmote) { - lineHeight -= 2; // reduce by 2px for tighter spacing - if (lineHeight < 8) - lineHeight = 8; // minimum safety - } - - rowHeights.push_back(lineHeight); - } int totalHeight = 0; - for (size_t i = 1; i < rowHeights.size(); ++i) { - totalHeight += rowHeights[i]; + for (size_t i = 1; i < cachedHeights.size(); ++i) { + totalHeight += cachedHeights[i]; } - int usableScrollHeight = usableHeight - rowHeights[0]; // remove header height - int scrollStop = std::max(0, totalHeight - usableScrollHeight + rowHeights.back()); + int usableScrollHeight = usableHeight - cachedHeights[0]; // remove header height + int scrollStop = std::max(0, totalHeight - usableScrollHeight + cachedHeights.back()); static float scrollY = 0.0f; static uint32_t lastTime = 0, scrollStartDelay = 0, pauseStart = 0; @@ -363,28 +337,109 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 int scrollOffset = static_cast(scrollY); int yOffset = -scrollOffset + getTextPositions(display)[1]; - for (int separatorX = 0; separatorX <= (display->getStringWidth(headerStr) + 3); separatorX += 2) { - display->setPixel(separatorX, yOffset + ((SCREEN_WIDTH > 128) ? 19 : 13)); + for (int separatorX = 1; separatorX <= (display->getStringWidth(headerStr) + 2); separatorX += 2) { + display->setPixel(separatorX, yOffset + ((isHighResolution) ? 19 : 13)); } // === Render visible lines === + renderMessageContent(display, cachedLines, cachedHeights, x, yOffset, scrollBottom, emotes, numEmotes, isInverted, isBold); + + // Draw header at the end to sort out overlapping elements + graphics::drawCommonHeader(display, x, y, titleStr); +} + +std::vector generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth) +{ + std::vector lines; + lines.push_back(std::string(headerStr)); // Header line is always first + + std::string line, word; + for (int i = 0; messageBuf[i]; ++i) { + char ch = messageBuf[i]; + if ((unsigned char)messageBuf[i] == 0xE2 && (unsigned char)messageBuf[i + 1] == 0x80 && + (unsigned char)messageBuf[i + 2] == 0x99) { + ch = '\''; // plain apostrophe + i += 2; // skip over the extra UTF-8 bytes + } + if (ch == '\n') { + if (!word.empty()) + line += word; + if (!line.empty()) + lines.push_back(line); + line.clear(); + word.clear(); + } else if (ch == ' ') { + line += word + ' '; + word.clear(); + } else { + word += ch; + std::string test = line + word; + // Keep these lines for diagnostics + // LOG_INFO("Char: '%c' (0x%02X)", ch, (unsigned char)ch); + // LOG_INFO("Current String: %s", test.c_str()); + if (display->getStringWidth(test.c_str()) > textWidth) { + if (!line.empty()) + lines.push_back(line); + line = word; + word.clear(); + } + } + } + + if (!word.empty()) + line += word; + if (!line.empty()) + lines.push_back(line); + + return lines; +} + +std::vector calculateLineHeights(const std::vector &lines, const Emote *emotes) +{ + std::vector rowHeights; + + for (const auto &_line : lines) { + int lineHeight = FONT_HEIGHT_SMALL; + bool hasEmote = false; + + for (int i = 0; i < numEmotes; ++i) { + const Emote &e = emotes[i]; + if (_line.find(e.label) != std::string::npos) { + lineHeight = std::max(lineHeight, e.height); + hasEmote = true; + } + } + + // Apply tighter spacing if no emotes on this line + if (!hasEmote) { + lineHeight -= 2; // reduce by 2px for tighter spacing + if (lineHeight < 8) + lineHeight = 8; // minimum safety + } + + rowHeights.push_back(lineHeight); + } + + return rowHeights; +} + +void renderMessageContent(OLEDDisplay *display, const std::vector &lines, const std::vector &rowHeights, int x, + int yOffset, int scrollBottom, const Emote *emotes, int numEmotes, bool isInverted, bool isBold) +{ for (size_t i = 0; i < lines.size(); ++i) { int lineY = yOffset; for (size_t j = 0; j < i; ++j) lineY += rowHeights[j]; if (lineY > -rowHeights[i] && lineY < scrollBottom) { if (i == 0 && isInverted) { - display->drawString(x + 3, lineY, lines[i].c_str()); + display->drawString(x, lineY, lines[i].c_str()); if (isBold) - display->drawString(x + 4, lineY, lines[i].c_str()); + display->drawString(x, lineY, lines[i].c_str()); } else { drawStringWithEmotes(display, x, lineY, lines[i], emotes, numEmotes); } } } - - // Draw header at the end to sort out overlapping elements - graphics::drawCommonHeader(display, x, y, titleStr); } } // namespace MessageRenderer diff --git a/src/graphics/draw/MessageRenderer.h b/src/graphics/draw/MessageRenderer.h index d92b96014..c15a699f7 100644 --- a/src/graphics/draw/MessageRenderer.h +++ b/src/graphics/draw/MessageRenderer.h @@ -2,6 +2,8 @@ #include "OLEDDisplay.h" #include "OLEDDisplayUi.h" #include "graphics/emotes.h" +#include +#include namespace graphics { @@ -14,5 +16,15 @@ void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string /// Draws the text message frame for displaying received messages void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +// Function to generate lines with word wrapping +std::vector generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth); + +// Function to calculate heights for each line +std::vector calculateLineHeights(const std::vector &lines, const Emote *emotes); + +// Function to render the message content +void renderMessageContent(OLEDDisplay *display, const std::vector &lines, const std::vector &rowHeights, int x, + int yOffset, int scrollBottom, const Emote *emotes, int numEmotes, bool isInverted, bool isBold); + } // namespace MessageRenderer } // namespace graphics diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index 13b71546e..3f47a3a09 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -80,7 +80,11 @@ const char *getCurrentModeTitle(int screenWidth) case MODE_LAST_HEARD: return "Last Heard"; case MODE_HOP_SIGNAL: - return (screenWidth > 128) ? "Hops/Signal" : "Hops/Sig"; +#ifdef USE_EINK + return "Hops/Sig"; +#else + return (isHighResolution) ? "Hops/Signal" : "Hops/Sig"; +#endif case MODE_DISTANCE: return "Distance"; default: @@ -94,50 +98,11 @@ unsigned long getModeCycleIntervalMs() return 3000; } -// Calculate bearing between two lat/lon points -float calculateBearing(double lat1, double lon1, double lat2, double lon2) -{ - double dLon = (lon2 - lon1) * DEG_TO_RAD; - double y = sin(dLon) * cos(lat2 * DEG_TO_RAD); - double x = cos(lat1 * DEG_TO_RAD) * sin(lat2 * DEG_TO_RAD) - sin(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * cos(dLon); - double bearing = atan2(y, x) * RAD_TO_DEG; - return fmod(bearing + 360.0, 360.0); -} - int calculateMaxScroll(int totalEntries, int visibleRows) { return std::max(0, (totalEntries - 1) / (visibleRows * 2)); } -void retrieveAndSortNodes(std::vector &nodeList) -{ - size_t numNodes = nodeDB->getNumMeshNodes(); - for (size_t i = 0; i < numNodes; i++) { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); - if (!node || node->num == nodeDB->getNodeNum()) - continue; - - NodeEntry entry; - entry.node = node; - entry.sortValue = sinceLastSeen(node); - - nodeList.push_back(entry); - } - - // Sort nodes: favorites first, then by last heard (most recent first) - std::sort(nodeList.begin(), nodeList.end(), [](const NodeEntry &a, const NodeEntry &b) { - bool aFav = a.node->is_favorite; - bool bFav = b.node->is_favorite; - if (aFav != bFav) - return aFav; - if (a.sortValue == 0 || a.sortValue == UINT32_MAX) - return false; - if (b.sortValue == 0 || b.sortValue == UINT32_MAX) - return true; - return a.sortValue < b.sortValue; - }); -} - void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd) { int columnWidth = display->getWidth() / 2; @@ -170,7 +135,7 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries, void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { bool isLeftCol = (x < SCREEN_WIDTH / 2); - int timeOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7); + int timeOffset = (isHighResolution) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7); const char *nodeName = getSafeNodeName(node); @@ -191,9 +156,9 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - display->drawString(x + ((SCREEN_WIDTH > 128) ? 6 : 3), y, nodeName); + display->drawString(x + ((isHighResolution) ? 6 : 3), y, nodeName); if (node->is_favorite) { - if (SCREEN_WIDTH > 128) { + if (isHighResolution) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); } else { display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); @@ -212,8 +177,8 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int bool isLeftCol = (x < SCREEN_WIDTH / 2); int nameMaxWidth = columnWidth - 25; - int barsOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19); - int hopOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17); + int barsOffset = (isHighResolution) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19); + int hopOffset = (isHighResolution) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17); int barsXOffset = columnWidth - barsOffset; @@ -222,9 +187,9 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 3), y, nameMaxWidth, nodeName); + display->drawStringMaxWidth(x + ((isHighResolution) ? 6 : 3), y, nameMaxWidth, nodeName); if (node->is_favorite) { - if (SCREEN_WIDTH > 128) { + if (isHighResolution) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); } else { display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); @@ -259,7 +224,7 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { bool isLeftCol = (x < SCREEN_WIDTH / 2); - int nameMaxWidth = columnWidth - (SCREEN_WIDTH > 128 ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); + int nameMaxWidth = columnWidth - (isHighResolution ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); const char *nodeName = getSafeNodeName(node); char distStr[10] = ""; @@ -314,9 +279,9 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 3), y, nameMaxWidth, nodeName); + display->drawStringMaxWidth(x + ((isHighResolution) ? 6 : 3), y, nameMaxWidth, nodeName); if (node->is_favorite) { - if (SCREEN_WIDTH > 128) { + if (isHighResolution) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); } else { display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); @@ -324,8 +289,8 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 } if (strlen(distStr) > 0) { - int offset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column) - : (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column) + int offset = (isHighResolution) ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column) + : (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column) int rightEdge = x + columnWidth - offset; int textWidth = display->getStringWidth(distStr); display->drawString(rightEdge - textWidth, y, distStr); @@ -354,15 +319,15 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 bool isLeftCol = (x < SCREEN_WIDTH / 2); // Adjust max text width depending on column and screen width - int nameMaxWidth = columnWidth - (SCREEN_WIDTH > 128 ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); + int nameMaxWidth = columnWidth - (isHighResolution ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); const char *nodeName = getSafeNodeName(node); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 3), y, nameMaxWidth, nodeName); + display->drawStringMaxWidth(x + ((isHighResolution) ? 6 : 3), y, nameMaxWidth, nodeName); if (node->is_favorite) { - if (SCREEN_WIDTH > 128) { + if (isHighResolution) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); } else { display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); @@ -377,19 +342,21 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 return; bool isLeftCol = (x < SCREEN_WIDTH / 2); - int arrowXOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18); + int arrowXOffset = (isHighResolution) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18); int centerX = x + columnWidth - arrowXOffset; int centerY = y + FONT_HEIGHT_SMALL / 2; double nodeLat = node->position.latitude_i * 1e-7; double nodeLon = node->position.longitude_i * 1e-7; - float bearingToNode = calculateBearing(userLat, userLon, nodeLat, nodeLon); + float bearing = GeoCoord::bearing(userLat, userLon, nodeLat, nodeLon); + float bearingToNode = RAD_TO_DEG * bearing; float relativeBearing = fmod((bearingToNode - myHeading + 360), 360); float angle = relativeBearing * DEG_TO_RAD; - // Shrink size by 2px int size = FONT_HEIGHT_SMALL - 5; + CompassRenderer::drawArrowToNode(display, centerX, centerY, size, relativeBearing); + /* float halfSize = size / 2.0; // Point of the arrow @@ -414,6 +381,7 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 // Draw the chevron-style arrowhead display->fillTriangle(tipX, tipY, leftX, leftY, notchX, notchY); display->fillTriangle(tipX, tipY, notchX, notchY, rightX, rightY); + */ } // ============================= @@ -436,19 +404,16 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t // Space below header y += COMMON_HEADER_HEIGHT; - // Fetch and display sorted node list - std::vector nodeList; - retrieveAndSortNodes(nodeList); - - int totalEntries = nodeList.size(); + int totalEntries = nodeDB->getNumMeshNodes(); int totalRowsAvailable = (display->getHeight() - y) / rowYOffset; -#ifdef USE_EINK - totalRowsAvailable -= 1; -#endif + int visibleNodeRows = totalRowsAvailable; int totalColumns = 2; int startIndex = scrollIndex * visibleNodeRows * totalColumns; + if (nodeDB->getMeshNodeByIndex(startIndex)->num == nodeDB->getNodeNum()) { + startIndex++; // skip own node + } int endIndex = std::min(startIndex + visibleNodeRows * totalColumns, totalEntries); int yOffset = 0; @@ -460,10 +425,10 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t for (int i = startIndex; i < endIndex; ++i) { int xPos = x + (col * columnWidth); int yPos = y + yOffset; - renderer(display, nodeList[i].node, xPos, yPos, columnWidth); + renderer(display, nodeDB->getMeshNodeByIndex(i), xPos, yPos, columnWidth); if (extras) { - extras(display, nodeList[i].node, xPos, yPos, columnWidth, heading, lat, lon); + extras(display, nodeDB->getMeshNodeByIndex(i), xPos, yPos, columnWidth, heading, lat, lon); } lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL); @@ -533,7 +498,12 @@ void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { +#ifdef USE_EINK + const char *title = "Hops/Sig"; +#else + const char *title = "Hops/Signal"; +#endif drawNodeListScreen(display, state, x, y, title, drawEntryHopSignal); } @@ -548,22 +518,24 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, { float heading = 0; bool validHeading = false; - double lat = 0; - double lon = 0; + auto ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + double lat = DegD(ourNode->position.latitude_i); + double lon = DegD(ourNode->position.longitude_i); + if (!screen->ignoreCompass) { #if HAS_GPS - if (screen->hasHeading()) { - heading = screen->getHeading(); // degrees - validHeading = true; - } else { - heading = screen->estimatedHeading(lat, lon); - validHeading = !isnan(heading); - } + if (screen->hasHeading()) { + heading = screen->getHeading(); // degrees + validHeading = true; + } else { + heading = screen->estimatedHeading(lat, lon); + validHeading = !isnan(heading); + } #endif - if (!validHeading) - return; - + if (!validHeading) + return; + } drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow, heading, lat, lon); } diff --git a/src/graphics/draw/NodeListRenderer.h b/src/graphics/draw/NodeListRenderer.h index 63f0d1c69..ea8df8bd9 100644 --- a/src/graphics/draw/NodeListRenderer.h +++ b/src/graphics/draw/NodeListRenderer.h @@ -23,12 +23,6 @@ namespace NodeListRenderer typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int); typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int, float, double, double); -// Node entry structure -struct NodeEntry { - meshtastic_NodeInfoLite *node; - uint32_t sortValue; -}; - // Node list mode enumeration enum NodeListMode { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_DISTANCE = 2, MODE_COUNT = 3 }; @@ -57,7 +51,6 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, // Utility functions const char *getCurrentModeTitle(int screenWidth); -void retrieveAndSortNodes(std::vector &nodeList); const char *getSafeNodeName(meshtastic_NodeInfoLite *node); void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields); diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index ed5257012..4866b4060 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -31,6 +31,7 @@ int8_t NotificationRenderer::curSelected = 0; char NotificationRenderer::alertBannerMessage[256] = {0}; uint32_t NotificationRenderer::alertBannerUntil = 0; // 0 is a special case meaning forever uint8_t NotificationRenderer::alertBannerOptions = 0; // last x lines are seelctable options +const char **NotificationRenderer::optionsArrayPtr = nullptr; std::function NotificationRenderer::alertBannerCallback = NULL; bool NotificationRenderer::pauseBanner = false; @@ -56,29 +57,22 @@ void NotificationRenderer::drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiStat void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) { - // Exit if no message is active or duration has passed - if (!isOverlayBannerShowing()) - return; - - if (pauseBanner) + if (!isOverlayBannerShowing() || pauseBanner) return; // === Layout Configuration === - constexpr uint16_t padding = 5; // Padding around text inside the box - constexpr uint16_t vPadding = 2; // Padding around text inside the box - constexpr uint8_t lineSpacing = 1; // Extra space between lines + constexpr uint16_t hPadding = 5; + constexpr uint16_t vPadding = 2; + constexpr uint8_t lineSpacing = 1; - // Search the message to determine if we need the bell added bool needs_bell = (strstr(alertBannerMessage, "Alert Received") != nullptr); - uint8_t firstOption = 0; - uint8_t firstOptionToShow = 0; - // Setup font and alignment display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_LEFT); // We will manually center per line - const int MAX_LINES = 24; + display->setTextAlignment(TEXT_ALIGN_LEFT); + constexpr int MAX_LINES = 5; + uint16_t optionWidths[alertBannerOptions] = {0}; uint16_t maxWidth = 0; uint16_t arrowsWidth = display->getStringWidth("> <", 4, true); uint16_t lineWidths[MAX_LINES] = {0}; @@ -86,30 +80,33 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp char *lineStarts[MAX_LINES + 1]; uint16_t lineCount = 0; char lineBuffer[40] = {0}; - // pointer to the terminating null + + // Parse lines char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage)); lineStarts[lineCount] = alertBannerMessage; - // loop through lines finding \n characters while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) { lineStarts[lineCount + 1] = std::find(lineStarts[lineCount], alertEnd, '\n'); lineLengths[lineCount] = lineStarts[lineCount + 1] - lineStarts[lineCount]; - if (lineStarts[lineCount + 1][0] == '\n') { - lineStarts[lineCount + 1] += 1; // Move the start pointer beyond the \n - } + if (lineStarts[lineCount + 1][0] == '\n') + lineStarts[lineCount + 1] += 1; lineWidths[lineCount] = display->getStringWidth(lineStarts[lineCount], lineLengths[lineCount], true); - if (lineWidths[lineCount] > maxWidth) { + if (lineWidths[lineCount] > maxWidth) maxWidth = lineWidths[lineCount]; - } - if (alertBannerOptions > 0 && lineCount > 0 && lineWidths[lineCount] + arrowsWidth > maxWidth) { - maxWidth = lineWidths[lineCount] + arrowsWidth; - } lineCount++; - // if we are doing a selection, add extra width for arrows } + // Measure option widths + for (int i = 0; i < alertBannerOptions; i++) { + optionWidths[i] = display->getStringWidth(optionsArrayPtr[i], strlen(optionsArrayPtr[i]), true); + if (optionWidths[i] > maxWidth) + maxWidth = optionWidths[i]; + if (optionWidths[i] + arrowsWidth > maxWidth) + maxWidth = optionWidths[i] + arrowsWidth; + } + + // Handle input if (alertBannerOptions > 0) { - // respond to input if (inEvent == INPUT_BROKER_UP || inEvent == INPUT_BROKER_ALT_PRESS) { curSelected--; } else if (inEvent == INPUT_BROKER_DOWN || inEvent == INPUT_BROKER_USER_PRESS) { @@ -120,113 +117,133 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp } else if ((inEvent == INPUT_BROKER_CANCEL || inEvent == INPUT_BROKER_ALT_LONG) && alertBannerUntil != 0) { alertBannerMessage[0] = '\0'; } + if (curSelected == -1) curSelected = alertBannerOptions - 1; if (curSelected == alertBannerOptions) curSelected = 0; - // compare number of options to number of lines - if (lineCount < alertBannerOptions) - return; - firstOption = lineCount - alertBannerOptions; - if (curSelected > 1 && alertBannerOptions > 3) { - firstOptionToShow = curSelected + firstOption - 1; - // put the selected option in the middle - } else { - firstOptionToShow = firstOption; - } - } else { // not in an alert with a callback - // TODO: check that at least a second has passed since the alert started + } else { if (inEvent == INPUT_BROKER_SELECT || inEvent == INPUT_BROKER_ALT_LONG || inEvent == INPUT_BROKER_CANCEL) { - alertBannerMessage[0] = '\0'; // end the alert early + alertBannerMessage[0] = '\0'; } } + inEvent = INPUT_BROKER_NONE; if (alertBannerMessage[0] == '\0') return; - // set width from longest line - uint16_t boxWidth = padding * 2 + maxWidth; + // === Box Size Calculation === + uint16_t boxWidth = hPadding * 2 + maxWidth; if (needs_bell) { - if (SCREEN_WIDTH > 128 && boxWidth <= 150) { + if (isHighResolution && boxWidth <= 150) boxWidth += 26; - } - if (SCREEN_WIDTH <= 128 && boxWidth <= 100) { + if (!isHighResolution && boxWidth <= 100) boxWidth += 20; - } - } - // calculate max lines on screen? for now it's 4 - // set height from line count - uint16_t boxHeight; - if (lineCount <= 4) { - boxHeight = vPadding * 2 + lineCount * FONT_HEIGHT_SMALL + (lineCount - 1) * lineSpacing; - } else { - boxHeight = vPadding * 2 + 4 * FONT_HEIGHT_SMALL + 4 * lineSpacing; } + uint16_t totalLines = lineCount + alertBannerOptions; + uint16_t screenHeight = display->height(); + uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3; + uint8_t visibleTotalLines = std::min(totalLines, (screenHeight - vPadding * 2) / effectiveLineHeight); + uint16_t contentHeight = visibleTotalLines * effectiveLineHeight; + uint16_t boxHeight = contentHeight + vPadding * 2; + int16_t boxLeft = (display->width() / 2) - (boxWidth / 2); int16_t boxTop = (display->height() / 2) - (boxHeight / 2); - // === Draw background box === + + // === Draw Box === display->setColor(BLACK); - display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); // Slightly oversized box - display->fillRect(boxLeft, boxTop - 2, boxWidth, 1); // Top Line - display->fillRect(boxLeft, boxTop + boxHeight + 1, boxWidth, 1); // Bottom Line - display->fillRect(boxLeft - 2, boxTop, 1, boxHeight); // Left Line - display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight); // Right Line + display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); + display->fillRect(boxLeft, boxTop - 2, boxWidth, 1); + display->fillRect(boxLeft, boxTop + boxHeight + 1, boxWidth, 1); + display->fillRect(boxLeft - 2, boxTop, 1, boxHeight); + display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight); display->setColor(WHITE); - display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); // Border + display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); display->setColor(BLACK); - display->fillRect(boxLeft, boxTop, 1, 1); // Top Left - display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1); // Top Right - display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1); // Bottom Left - display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1); // Bottom Right + display->fillRect(boxLeft, boxTop, 1, 1); + display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1); + display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1); + display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1); display->setColor(WHITE); - // === Draw each line centered in the box === + // === Draw Content === int16_t lineY = boxTop + vPadding; + uint8_t linesShown = 0; - for (int i = 0; i < lineCount; i++) { - // is this line selected? - // if so, start the buffer with -> and strncpy to the 4th location - if (i < lineCount - alertBannerOptions || alertBannerOptions == 0) { - strncpy(lineBuffer, lineStarts[i], 40); - if (lineLengths[i] > 39) - lineBuffer[39] = '\0'; - else - lineBuffer[lineLengths[i]] = '\0'; - } else if (i >= firstOptionToShow && i < firstOptionToShow + 3) { - if (i == curSelected + firstOption) { - if (lineLengths[i] > 35) - lineLengths[i] = 35; - strncpy(lineBuffer, "> ", 3); - strncpy(lineBuffer + 2, lineStarts[i], 36); - strncpy(lineBuffer + lineLengths[i] + 2, " <", 3); - lineLengths[i] += 4; - lineWidths[i] += display->getStringWidth("> <", 4, true); - if (lineLengths[i] > 35) - lineBuffer[39] = '\0'; - else - lineBuffer[lineLengths[i]] = '\0'; - } else { - strncpy(lineBuffer, lineStarts[i], 40); - if (lineLengths[i] > 39) - lineBuffer[39] = '\0'; - else - lineBuffer[lineLengths[i]] = '\0'; - } - } else { // add break for the additional lines - continue; - } + for (int i = 0; i < lineCount && linesShown < visibleTotalLines; i++, linesShown++) { + strncpy(lineBuffer, lineStarts[i], 40); + lineBuffer[lineLengths[i] > 39 ? 39 : lineLengths[i]] = '\0'; int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2; - if (needs_bell && i == 0) { int bellY = lineY + (FONT_HEIGHT_SMALL - 8) / 2; display->drawXbm(textX - 10, bellY, 8, 8, bell_alert); display->drawXbm(textX + lineWidths[i] + 2, bellY, 8, 8, bell_alert); } + // Determine if this is a pop-up or a pick list + if (alertBannerOptions > 0) { + // Pick List + display->setColor(WHITE); + int background_yOffset = 1; + // Determine if we have low hanging characters + if (strchr(lineBuffer, 'p') || strchr(lineBuffer, 'g') || strchr(lineBuffer, 'y') || strchr(lineBuffer, 'j')) { + background_yOffset = -1; + } + display->fillRect(boxLeft, boxTop + 1, boxWidth, effectiveLineHeight - background_yOffset); + display->setColor(BLACK); + int yOffset = 3; + display->drawString(textX, lineY - yOffset, lineBuffer); + display->setColor(WHITE); + lineY += (effectiveLineHeight - 2 - background_yOffset); + } else { + // Pop-up + display->drawString(textX, lineY - 2, lineBuffer); + lineY += (effectiveLineHeight); + } + } + + uint8_t firstOptionToShow = 0; + if (alertBannerOptions > 0) { + if (curSelected > 1 && alertBannerOptions > visibleTotalLines - lineCount) + firstOptionToShow = curSelected - 1; + else + firstOptionToShow = 0; + } + + for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) { + if (i == curSelected) { + strncpy(lineBuffer, "> ", 3); + strncpy(lineBuffer + 2, optionsArrayPtr[i], 36); + strncpy(lineBuffer + strlen(optionsArrayPtr[i]) + 2, " <", 3); + lineBuffer[39] = '\0'; + } else { + strncpy(lineBuffer, optionsArrayPtr[i], 40); + lineBuffer[39] = '\0'; + } + + int16_t textX = boxLeft + (boxWidth - optionWidths[i] - (i == curSelected ? arrowsWidth : 0)) / 2; display->drawString(textX, lineY, lineBuffer); - lineY += FONT_HEIGHT_SMALL + lineSpacing; + lineY += effectiveLineHeight; + } + + // === Scroll Bar (Thicker, inside box, not over title) === + if (totalLines > visibleTotalLines) { + const uint8_t scrollBarWidth = 5; + const uint8_t scrollPadding = 2; + + int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2; + int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight; // start after title line + uint16_t scrollBarHeight = boxHeight - vPadding * 2 - effectiveLineHeight; + + float ratio = (float)visibleTotalLines / totalLines; + uint16_t indicatorHeight = std::max((int)(scrollBarHeight * ratio), 4); + float scrollRatio = (float)(firstOptionToShow + linesShown - visibleTotalLines) / (totalLines - visibleTotalLines); + uint16_t indicatorY = scrollBarY + scrollRatio * (scrollBarHeight - indicatorHeight); + + display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight); + display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight); } } diff --git a/src/graphics/draw/NotificationRenderer.h b/src/graphics/draw/NotificationRenderer.h index 3ed931dc6..2ec5fd9ec 100644 --- a/src/graphics/draw/NotificationRenderer.h +++ b/src/graphics/draw/NotificationRenderer.h @@ -12,7 +12,8 @@ class NotificationRenderer static char inEvent; static int8_t curSelected; static char alertBannerMessage[256]; - static uint32_t alertBannerUntil; // 0 is a special case meaning forever + static uint32_t alertBannerUntil; // 0 is a special case meaning forever + static const char **optionsArrayPtr; static uint8_t alertBannerOptions; // last x lines are seelctable options static std::function alertBannerCallback; diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index a77d5b44b..1738a8246 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -18,6 +18,32 @@ #include #include +bool isAllowedPunctuation(char c) +{ + const std::string allowed = ".,!?;:-_()[]{}'\"@#$/\\&+=%~^ "; + return allowed.find(c) != std::string::npos; +} + +std::string sanitizeString(const std::string &input) +{ + std::string output; + bool inReplacement = false; + + for (char c : input) { + if (std::isalnum(static_cast(c)) || isAllowedPunctuation(c)) { + output += c; + inReplacement = false; + } else { + if (!inReplacement) { + output += 0xbf; // ISO-8859-1 for inverted question mark + inReplacement = true; + } + } + } + + return output; +} + #if !MESHTASTIC_EXCLUDE_GPS // External variables @@ -38,7 +64,7 @@ NodeNum UIRenderer::currentFavoriteNodeNum = 0; void UIRenderer::drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) { // Draw satellite image - if (SCREEN_WIDTH > 128) { + if (isHighResolution) { NodeListRenderer::drawScaledXBitmap16x16(x, y - 2, imgSatellite_width, imgSatellite_height, imgSatellite, display); } else { display->drawXbm(x + 1, y + 1, imgSatellite_width, imgSatellite_height, imgSatellite); @@ -58,7 +84,7 @@ void UIRenderer::drawGps(OLEDDisplay *display, int16_t x, int16_t y, const mesht } else { snprintf(textString, sizeof(textString), "%u sats", gps->getNumSatellites()); } - if (SCREEN_WIDTH > 128) { + if (isHighResolution) { display->drawString(x + 18, y, textString); } else { display->drawString(x + 11, y, textString); @@ -163,46 +189,6 @@ void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, } } -void UIRenderer::drawBattery(OLEDDisplay *display, int16_t x, int16_t y, uint8_t *imgBuffer, - const meshtastic::PowerStatus *powerStatus) -{ - static const uint8_t powerBar[3] = {0x81, 0xBD, 0xBD}; - static const uint8_t lightning[8] = {0xA1, 0xA1, 0xA5, 0xAD, 0xB5, 0xA5, 0x85, 0x85}; - - // Clear the bar area inside the battery image - for (int i = 1; i < 14; i++) { - imgBuffer[i] = 0x81; - } - - // Fill with lightning or power bars - if (powerStatus->getIsCharging()) { - memcpy(imgBuffer + 3, lightning, 8); - } else { - for (int i = 0; i < 4; i++) { - if (powerStatus->getBatteryChargePercent() >= 25 * i) - memcpy(imgBuffer + 1 + (i * 3), powerBar, 3); - } - } - - // Slightly more conservative scaling based on screen width - int scale = 1; - - if (SCREEN_WIDTH >= 200) - scale = 2; - if (SCREEN_WIDTH >= 300) - scale = 2; // Do NOT go higher than 2 - - // Draw scaled battery image (16 columns × 8 rows) - for (int col = 0; col < 16; col++) { - uint8_t colBits = imgBuffer[col]; - for (int row = 0; row < 8; row++) { - if (colBits & (1 << row)) { - display->fillRect(x + col * scale, y + row * scale, scale, scale); - } - } - } -} - // Draw nodes status void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, int node_offset, bool show_total, String additional_words) @@ -221,19 +207,19 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS)) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) - if (SCREEN_WIDTH > 128) { + if (isHighResolution) { NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display); } else { display->drawFastImage(x, y + 3, 8, 8, imgUser); } #else - if (SCREEN_WIDTH > 128) { + if (isHighResolution) { NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display); } else { display->drawFastImage(x, y + 1, 8, 8, imgUser); } #endif - int string_offset = (SCREEN_WIDTH > 128) ? 9 : 0; + int string_offset = (isHighResolution) ? 9 : 0; display->drawString(x + 10 + string_offset, y - 2, usersString); } @@ -293,12 +279,14 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st // List of available macro Y positions in order, from top to bottom. int line = 1; // which slot to use next + std::string usernameStr; // === 1. Long Name (always try to show first) === const char *username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr; - if (username && line < 5) { + if (username) { + usernameStr = sanitizeString(username); // Sanitize the incoming long_name just in case // Print node's long name (e.g. "Backpack Node") - display->drawString(x, getTextPositions(display)[line++], username); + display->drawString(x, getTextPositions(display)[line++], usernameStr.c_str()); } // === 2. Signal and Hops (combined on one line, if available) === @@ -456,8 +444,11 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); */ float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); - if (!config.display.compass_north_top) + if (screen->ignoreCompass) { + myHeading = 0; + } else { bearing -= myHeading; + } display->drawCircle(compassX, compassY, compassRadius); CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius); @@ -476,7 +467,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st const int margin = 4; // --------- PATCH FOR EINK NAV BAR (ONLY CHANGE BELOW) ----------- #if defined(USE_EINK) - const int iconSize = (SCREEN_WIDTH > 128) ? 16 : 8; + const int iconSize = (isHighResolution) ? 16 : 8; const int navBarHeight = iconSize + 6; #else const int navBarHeight = 0; @@ -497,8 +488,11 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st int compassY = yBelowContent + availableHeight / 2; const auto &op = ourNode->position; - float myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 - : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); + float myHeading = 0; + if (!screen->ignoreCompass) { + myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 + : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); + } graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius); const auto &p = node->position; @@ -507,7 +501,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); */ float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); - if (!config.display.compass_north_top) + if (!screen->ignoreCompass) bearing -= myHeading; graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, bearing); @@ -570,15 +564,15 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta } else { displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; } - int yOffset = (SCREEN_WIDTH > 128) ? 3 : 1; - if (SCREEN_WIDTH > 128) { + int yOffset = (isHighResolution) ? 3 : 1; + if (isHighResolution) { NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width, imgSatellite_height, imgSatellite, display); } else { display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height, imgSatellite); } - int xOffset = (SCREEN_WIDTH > 128) ? 6 : 0; + int xOffset = (isHighResolution) ? 6 : 0; display->drawString(x + 11 + xOffset, getTextPositions(display)[line], displayLine); } else { UIRenderer::drawGps(display, 0, getTextPositions(display)[line], gpsStatus); @@ -602,17 +596,17 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta char chUtilPercentage[10]; snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); - int chUtil_x = (SCREEN_WIDTH > 128) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5; + int chUtil_x = (isHighResolution) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5; int chUtil_y = getTextPositions(display)[line] + 3; - int chutil_bar_width = (SCREEN_WIDTH > 128) ? 100 : 50; + int chutil_bar_width = (isHighResolution) ? 100 : 50; if (!config.bluetooth.enabled) { - chutil_bar_width = (SCREEN_WIDTH > 128) ? 80 : 40; + chutil_bar_width = (isHighResolution) ? 80 : 40; } - int chutil_bar_height = (SCREEN_WIDTH > 128) ? 12 : 7; - int extraoffset = (SCREEN_WIDTH > 128) ? 6 : 3; + int chutil_bar_height = (isHighResolution) ? 12 : 7; + int extraoffset = (isHighResolution) ? 6 : 3; if (!config.bluetooth.enabled) { - extraoffset = (SCREEN_WIDTH > 128) ? 6 : 1; + extraoffset = (isHighResolution) ? 6 : 1; } int chutil_percent = airTime->channelUtilizationPercent(); @@ -672,21 +666,20 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta // === Fourth & Fifth Rows: Node Identity === int textWidth = 0; int nameX = 0; - int yOffset = (SCREEN_WIDTH > 128) ? 0 : 5; + int yOffset = (isHighResolution) ? 0 : 5; const char *longName = nullptr; + std::string longNameStr; + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { - longName = ourNode->user.long_name; + longNameStr = sanitizeString(ourNode->user.long_name); } - uint8_t dmac[6]; char shortnameble[35]; - getMacAddr(dmac); - snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]); snprintf(shortnameble, sizeof(shortnameble), "%s", graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : ""); char combinedName[50]; - snprintf(combinedName, sizeof(combinedName), "%s (%s)", longName, shortnameble); + snprintf(combinedName, sizeof(combinedName), "%s (%s)", longNameStr.empty() ? "" : longNameStr.c_str(), shortnameble); if (SCREEN_WIDTH - (display->getStringWidth(longName) + display->getStringWidth(shortnameble)) > 10) { size_t len = strlen(combinedName); if (len >= 3 && strcmp(combinedName + len - 3, " ()") == 0) { @@ -700,7 +693,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta // === LongName Centered === textWidth = display->getStringWidth(longName); nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, getTextPositions(display)[line++], longName); + display->drawString(nameX, getTextPositions(display)[line++], longNameStr.c_str()); // === ShortName Centered === textWidth = display->getStringWidth(shortnameble); @@ -808,44 +801,42 @@ void UIRenderer::drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState { LOG_DEBUG("Draw screensaver overlay"); - EINK_ADD_FRAMEFLAG(display, COSMETIC); // Take the opportunity for a full-refresh + EINK_ADD_FRAMEFLAG(display, COSMETIC); // Full refresh for screensaver // Config display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_LEFT); const char *pauseText = "Screen Paused"; const char *idText = owner.short_name; - const bool useId = haveGlyphs(idText); // This bool is used to hide the idText box if we can't render the short name - constexpr uint16_t padding = 5; + const bool useId = haveGlyphs(idText); + constexpr uint8_t padding = 2; constexpr uint8_t dividerGap = 1; - constexpr uint8_t imprecision = 5; // How far the box origins can drift from center. Combat burn-in. - // Dimensions - const uint16_t idTextWidth = display->getStringWidth(idText, strlen(idText), true); // "true": handle utf8 chars + // Text widths + const uint16_t idTextWidth = display->getStringWidth(idText, strlen(idText), true); const uint16_t pauseTextWidth = display->getStringWidth(pauseText, strlen(pauseText)); - const uint16_t boxWidth = padding + (useId ? idTextWidth + padding + padding : 0) + pauseTextWidth + padding; - const uint16_t boxHeight = padding + FONT_HEIGHT_SMALL + padding; + const uint16_t boxWidth = padding + (useId ? idTextWidth + padding : 0) + pauseTextWidth + padding; + const uint16_t boxHeight = FONT_HEIGHT_SMALL + (padding * 2); - // Position - const int16_t boxLeft = (display->width() / 2) - (boxWidth / 2) + random(-imprecision, imprecision + 1); - // const int16_t boxRight = boxLeft + boxWidth - 1; - const int16_t boxTop = (display->height() / 2) - (boxHeight / 2 + random(-imprecision, imprecision + 1)); - const int16_t boxBottom = boxTop + boxHeight - 1; + // Flush with bottom + const int16_t boxLeft = (display->width() / 2) - (boxWidth / 2); + const int16_t boxTop = display->height() - boxHeight; + const int16_t boxBottom = display->height() - 1; const int16_t idTextLeft = boxLeft + padding; const int16_t idTextTop = boxTop + padding; - const int16_t pauseTextLeft = boxLeft + (useId ? padding + idTextWidth + padding : 0) + padding; + const int16_t pauseTextLeft = boxLeft + (useId ? idTextWidth + (padding * 2) : 0) + padding; const int16_t pauseTextTop = boxTop + padding; const int16_t dividerX = boxLeft + padding + idTextWidth + padding; - const int16_t dividerTop = boxTop + 1 + dividerGap; - const int16_t dividerBottom = boxBottom - 1 - dividerGap; + const int16_t dividerTop = boxTop + dividerGap; + const int16_t dividerBottom = boxBottom - dividerGap; // Draw: box display->setColor(EINK_WHITE); - display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); // Clear a slightly oversized area for the box + display->fillRect(boxLeft, boxTop, boxWidth, boxHeight); display->setColor(EINK_BLACK); display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); - // Draw: Text + // Draw: text if (useId) display->drawString(idTextLeft, idTextTop, idText); display->drawString(pauseTextLeft, pauseTextTop, pauseText); @@ -920,15 +911,15 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU } else { displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; } - int yOffset = (SCREEN_WIDTH > 128) ? 3 : 1; - if (SCREEN_WIDTH > 128) { + int yOffset = (isHighResolution) ? 3 : 1; + if (isHighResolution) { NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width, imgSatellite_height, imgSatellite, display); } else { display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height, imgSatellite); } - int xOffset = (SCREEN_WIDTH > 128) ? 6 : 0; + int xOffset = (isHighResolution) ? 6 : 0; display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine); } else { UIRenderer::drawGps(display, 0, getTextPositions(display)[line++], gpsStatus); @@ -941,15 +932,18 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU int32_t(gpsStatus->getAltitude())); // === Determine Compass Heading === - float heading; + float heading = 0; bool validHeading = false; - - if (screen->hasHeading()) { - heading = radians(screen->getHeading()); + if (screen->ignoreCompass) { validHeading = true; } else { - heading = screen->estimatedHeading(geoCoord.getLatitude() * 1e-7, geoCoord.getLongitude() * 1e-7); - validHeading = !isnan(heading); + if (screen->hasHeading()) { + heading = radians(screen->getHeading()); + validHeading = true; + } else { + heading = screen->estimatedHeading(geoCoord.getLatitude() * 1e-7, geoCoord.getLongitude() * 1e-7); + validHeading = !isnan(heading); + } } // If GPS is off, no need to display these parts @@ -1005,7 +999,9 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU display->drawCircle(compassX, compassY, compassRadius); // "N" label - float northAngle = -heading; + float northAngle = 0; + if (!config.display.compass_north_top) + northAngle = -heading; float radius = compassRadius; int16_t nX = compassX + (radius - 1) * sin(northAngle); int16_t nY = compassY - (radius - 1) * cos(northAngle); @@ -1046,7 +1042,9 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU display->drawCircle(compassX, compassY, compassRadius); // "N" label - float northAngle = -heading; + float northAngle = 0; + if (!config.display.compass_north_top) + northAngle = -heading; float radius = compassRadius; int16_t nX = compassX + (radius - 1) * sin(northAngle); int16_t nY = compassY - (radius - 1) * cos(northAngle); @@ -1114,18 +1112,6 @@ void UIRenderer::drawOEMBootScreen(OLEDDisplay *display, OLEDDisplayUiState *sta #endif -// Function overlay for showing mute/buzzer modifiers etc. -void UIRenderer::drawFunctionOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) -{ - // LOG_DEBUG("Draw function overlay"); - if (functionSymbol.begin() != functionSymbol.end()) { - char buf[64]; - display->setFont(FONT_SMALL); - snprintf(buf, sizeof(buf), "%s", functionSymbolString.c_str()); - display->drawString(SCREEN_WIDTH - display->getStringWidth(buf), SCREEN_HEIGHT - FONT_HEIGHT_SMALL, buf); - } -} - // Navigation bar overlay implementation static int8_t lastFrameIndex = -1; static uint32_t lastFrameChangeTime = 0; @@ -1141,10 +1127,9 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta lastFrameChangeTime = millis(); } - const bool useBigIcons = (SCREEN_WIDTH > 128); - const int iconSize = useBigIcons ? 16 : 8; - const int spacing = useBigIcons ? 8 : 4; - const int bigOffset = useBigIcons ? 1 : 0; + const int iconSize = isHighResolution ? 16 : 8; + const int spacing = isHighResolution ? 8 : 4; + const int bigOffset = isHighResolution ? 1 : 0; const size_t totalIcons = screen->indicatorIcons.size(); if (totalIcons == 0) @@ -1158,14 +1143,35 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta const int totalWidth = (pageEnd - pageStart) * iconSize + (pageEnd - pageStart - 1) * spacing; const int xStart = (SCREEN_WIDTH - totalWidth) / 2; - // Only show bar briefly after switching frames (unless on E-Ink) + // Only show bar briefly after switching frames + static uint32_t navBarLastShown = 0; + static bool cosmeticRefreshDone = false; + + bool navBarVisible = millis() - lastFrameChangeTime <= ICON_DISPLAY_DURATION_MS; + int y = navBarVisible ? (SCREEN_HEIGHT - iconSize - 1) : SCREEN_HEIGHT; + #if defined(USE_EINK) - int y = SCREEN_HEIGHT - iconSize - 1; -#else - int y = SCREEN_HEIGHT - iconSize - 1; - if (millis() - lastFrameChangeTime > ICON_DISPLAY_DURATION_MS) { - y = SCREEN_HEIGHT; + static bool navBarPrevVisible = false; + + if (navBarVisible && !navBarPrevVisible) { + EINK_ADD_FRAMEFLAG(display, DEMAND_FAST); // Fast refresh when showing nav bar + cosmeticRefreshDone = false; + navBarLastShown = millis(); } + + if (!navBarVisible && navBarPrevVisible) { + EINK_ADD_FRAMEFLAG(display, DEMAND_FAST); // Fast refresh when hiding nav bar + navBarLastShown = millis(); // Mark when it disappeared + } + + if (!navBarVisible && navBarLastShown != 0 && !cosmeticRefreshDone) { + if (millis() - navBarLastShown > 10000) { // 10s after hidden + EINK_ADD_FRAMEFLAG(display, COSMETIC); // One-time ghost cleanup + cosmeticRefreshDone = true; + } + } + + navBarPrevVisible = navBarVisible; #endif // Pre-calculate bounding rect @@ -1191,7 +1197,7 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta display->setColor(BLACK); } - if (useBigIcons) { + if (isHighResolution) { NodeListRenderer::drawScaledXBitmap16x16(x, y, 8, 8, icon, display); } else { display->drawXbm(x, y, iconSize, iconSize, icon); diff --git a/src/graphics/draw/UIRenderer.h b/src/graphics/draw/UIRenderer.h index 21e4aef61..9e5e8c4b4 100644 --- a/src/graphics/draw/UIRenderer.h +++ b/src/graphics/draw/UIRenderer.h @@ -32,8 +32,6 @@ class UIRenderer { public: // Common UI elements - static void drawBattery(OLEDDisplay *display, int16_t x, int16_t y, uint8_t *imgBuffer, - const meshtastic::PowerStatus *powerStatus); static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, int node_offset = 0, bool show_total = true, String additional_words = ""); @@ -49,9 +47,6 @@ class UIRenderer // Overlay and special screens static void drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *text); - // Function overlay for showing mute/buzzer modifiers etc. - static void drawFunctionOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); - // Navigation bar overlay static void drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state); diff --git a/src/graphics/images.h b/src/graphics/images.h index e9c2f00ea..c5865878a 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -23,9 +23,6 @@ const uint8_t bluetoothConnectedIcon[36] PROGMEM = {0xfe, 0x01, 0xff, 0x03, 0x03 0xf3, 0x3f, 0x33, 0x30, 0x33, 0x33, 0x33, 0x33, 0x03, 0x33, 0xff, 0x33, 0xfe, 0x31, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0xf0, 0x3f, 0xe0, 0x1f}; -// This image definition is here instead of images.h because it's modified dynamically by the drawBattery function -static uint8_t imgBattery[16] = {0xFF, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0xE7, 0x3C}; - #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || ARCH_PORTDUINO) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) @@ -45,19 +42,15 @@ const uint8_t imgSF[] PROGMEM = {0xd2, 0xb7, 0xad, 0xbb, 0x92, 0x01, 0xfd, 0xfd, // === Horizontal battery === // Basic battery design and all related pieces -const unsigned char batteryBitmap_h[] PROGMEM = { - 0b11111110, 0b00000000, 0b11110000, 0b00000111, 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b00000001, 0b00000000, - 0b00000000, 0b00001000, 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b00000001, 0b00000000, 0b00000000, 0b00001000, - 0b00000001, 0b00000000, 0b00000000, 0b00011000, 0b00000001, 0b00000000, 0b00000000, 0b00011000, 0b00000001, 0b00000000, - 0b00000000, 0b00011000, 0b00000001, 0b00000000, 0b00000000, 0b00011000, 0b00000001, 0b00000000, 0b00000000, 0b00011000, - 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b00000001, 0b00000000, - 0b00000000, 0b00001000, 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b11111110, 0b00000000, 0b11110000, 0b00000111}; +const unsigned char batteryBitmap_h_bottom[] PROGMEM = { + 0b00011110, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, + 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, + 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00011110, 0b00000000}; -// This is the left and right bars for the fill in -const unsigned char batteryBitmap_sidegaps_h[] PROGMEM = { - 0b11111111, 0b00001111, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b11111111, 0b00001111}; +const unsigned char batteryBitmap_h_top[] PROGMEM = { + 0b00111100, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, + 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b01000000, 0b00000000, + 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b00111100, 0b00000000}; // Lightning Bolt const unsigned char lightning_bolt_h[] PROGMEM = { @@ -280,11 +273,16 @@ const uint8_t bluetoothdisabled[] PROGMEM = {0b11101100, 0b01010100, 0b01001100, const uint8_t smallbulletpoint[] PROGMEM = {0b00000011, 0b00000011, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000}; -// Clock -#define icon_clock_width 8 -#define icon_clock_height 8 -const uint8_t icon_clock[] PROGMEM = {0b00111100, 0b01000010, 0b10000101, 0b10101001, - 0b10010001, 0b10000001, 0b01000010, 0b00111100}; +// Digital Clock +#define digital_icon_clock_width 8 +#define digital_icon_clock_height 8 +const uint8_t digital_icon_clock[] PROGMEM = {0b00111100, 0b01000010, 0b10000101, 0b10101001, + 0b10010001, 0b10000001, 0b01000010, 0b00111100}; +// Analog Clock +#define analog_icon_clock_width 8 +#define analog_icon_clock_height 8 +const uint8_t analog_icon_clock[] PROGMEM = {0b11111111, 0b01000010, 0b00100100, 0b00011000, + 0b00100100, 0b01000010, 0b01000010, 0b11111111}; #include "img/icon.xbm" static_assert(sizeof(icon_bits) >= 0, "Silence unused variable warning"); \ No newline at end of file diff --git a/src/input/ButtonThread.cpp b/src/input/ButtonThread.cpp index bc75e0a54..da9878fa4 100644 --- a/src/input/ButtonThread.cpp +++ b/src/input/ButtonThread.cpp @@ -27,28 +27,25 @@ ButtonThread::ButtonThread(const char *name) : OSThread(name) _originName = name; } -bool ButtonThread::initButton(uint8_t pinNumber, bool activeLow, bool activePullup, uint32_t pullupSense, voidFuncPtr intRoutine, - input_broker_event singlePress, input_broker_event longPress, uint16_t longPressTime, - input_broker_event doublePress, input_broker_event longLongPress, uint16_t longLongPressTime, - input_broker_event triplePress, input_broker_event shortLong, bool touchQuirk) +bool ButtonThread::initButton(const ButtonConfig &config) { if (inputBroker) inputBroker->registerSource(this); - _longPressTime = longPressTime; - _longLongPressTime = longLongPressTime; - _pinNum = pinNumber; - _activeLow = activeLow; - _touchQuirk = touchQuirk; - _intRoutine = intRoutine; - _longLongPress = longLongPress; + _longPressTime = config.longPressTime; + _longLongPressTime = config.longLongPressTime; + _pinNum = config.pinNumber; + _activeLow = config.activeLow; + _touchQuirk = config.touchQuirk; + _intRoutine = config.intRoutine; + _longLongPress = config.longLongPress; - userButton = OneButton(pinNumber, activeLow, activePullup); + userButton = OneButton(config.pinNumber, config.activeLow, config.activePullup); - if (pullupSense != 0) { - pinMode(pinNumber, pullupSense); + if (config.pullupSense != 0) { + pinMode(config.pinNumber, config.pullupSense); } - _singlePress = singlePress; + _singlePress = config.singlePress; userButton.attachClick( [](void *callerThread) -> void { ButtonThread *thread = (ButtonThread *)callerThread; @@ -56,8 +53,8 @@ bool ButtonThread::initButton(uint8_t pinNumber, bool activeLow, bool activePull }, this); - if (longPress != INPUT_BROKER_NONE) { - _longPress = longPress; + if (config.longPress != INPUT_BROKER_NONE) { + _longPress = config.longPress; userButton.attachLongPressStart( [](void *callerThread) -> void { ButtonThread *thread = (ButtonThread *)callerThread; @@ -74,8 +71,8 @@ bool ButtonThread::initButton(uint8_t pinNumber, bool activeLow, bool activePull this); } - if (doublePress != INPUT_BROKER_NONE) { - _doublePress = doublePress; + if (config.doublePress != INPUT_BROKER_NONE) { + _doublePress = config.doublePress; userButton.attachDoubleClick( [](void *callerThread) -> void { ButtonThread *thread = (ButtonThread *)callerThread; @@ -84,8 +81,8 @@ bool ButtonThread::initButton(uint8_t pinNumber, bool activeLow, bool activePull this); } - if (triplePress != INPUT_BROKER_NONE) { - _triplePress = triplePress; + if (config.triplePress != INPUT_BROKER_NONE) { + _triplePress = config.triplePress; userButton.attachMultiClick( [](void *callerThread) -> void { ButtonThread *thread = (ButtonThread *)callerThread; @@ -94,8 +91,8 @@ bool ButtonThread::initButton(uint8_t pinNumber, bool activeLow, bool activePull }, this); } - if (shortLong != INPUT_BROKER_NONE) { - _shortLong = shortLong; + if (config.shortLong != INPUT_BROKER_NONE) { + _shortLong = config.shortLong; } userButton.setDebounceMs(1); @@ -266,6 +263,11 @@ int32_t ButtonThread::runOnce() break; } + + // doesn't handle BUTTON_EVENT_PRESSED_SCREEN BUTTON_EVENT_TOUCH_LONG_PRESSED BUTTON_EVENT_COMBO_SHORT_LONG + default: { + break; + } } } btnEvent = BUTTON_EVENT_NONE; diff --git a/src/input/ButtonThread.h b/src/input/ButtonThread.h index 033f92b8b..949048de1 100644 --- a/src/input/ButtonThread.h +++ b/src/input/ButtonThread.h @@ -7,6 +7,26 @@ typedef void (*voidFuncPtr)(void); +struct ButtonConfig { + uint8_t pinNumber; + bool activeLow = true; + bool activePullup = true; + uint32_t pullupSense = 0; + voidFuncPtr intRoutine = nullptr; + input_broker_event singlePress = INPUT_BROKER_NONE; + input_broker_event longPress = INPUT_BROKER_NONE; + uint16_t longPressTime = 500; + input_broker_event doublePress = INPUT_BROKER_NONE; + input_broker_event longLongPress = INPUT_BROKER_NONE; + uint16_t longLongPressTime = 5000; + input_broker_event triplePress = INPUT_BROKER_NONE; + input_broker_event shortLong = INPUT_BROKER_NONE; + bool touchQuirk = false; + + // Constructor to set required parameter + ButtonConfig(uint8_t pin = 0) : pinNumber(pin) {} +}; + #ifndef BUTTON_CLICK_MS #define BUTTON_CLICK_MS 250 #endif @@ -28,12 +48,7 @@ class ButtonThread : public Observable, public concurrency:: public: const char *_originName; static const uint32_t c_holdOffTime = 30000; // hold off 30s after boot - bool initButton(uint8_t pinNumber, bool activeLow, bool activePullup, uint32_t pullupSense, voidFuncPtr intRoutine, - input_broker_event singlePress, input_broker_event longPress = INPUT_BROKER_NONE, - uint16_t longPressTime = 500, input_broker_event doublePress = INPUT_BROKER_NONE, - input_broker_event longLongPress = INPUT_BROKER_NONE, uint16_t longLongPressTime = 5000, - input_broker_event triplePress = INPUT_BROKER_NONE, input_broker_event shortLong = INPUT_BROKER_NONE, - bool touchQuirk = false); + bool initButton(const ButtonConfig &config); enum ButtonEventType { BUTTON_EVENT_NONE, diff --git a/src/main.cpp b/src/main.cpp index 2251241da..4b64a78ea 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -927,58 +927,81 @@ void setup() LOG_DEBUG("Use GPIO%02d for button", settingsMap[userButtonPin]); UserButtonThread = new ButtonThread("UserButton"); - if (screen) - UserButtonThread->initButton( - settingsMap[userButtonPin], true, true, INPUT_PULLUP, // pull up bias - []() { - UserButtonThread->userButton.tick(); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }, - INPUT_BROKER_USER_PRESS, INPUT_BROKER_SELECT); + if (screen) { + ButtonConfig config; + config.pinNumber = (uint8_t)settingsMap[userButtonPin]; + config.activeLow = true; + config.activePullup = true; + config.pullupSense = INPUT_PULLUP; + config.intRoutine = []() { + UserButtonThread->userButton.tick(); + runASAP = true; + BaseType_t higherWake = 0; + mainDelay.interruptFromISR(&higherWake); + }; + config.singlePress = INPUT_BROKER_USER_PRESS; + config.longPress = INPUT_BROKER_SELECT; + UserButtonThread->initButton(config); + } } #endif #ifdef BUTTON_PIN_TOUCH TouchButtonThread = new ButtonThread("BackButton"); - TouchButtonThread->initButton( - BUTTON_PIN_TOUCH, true, true, pullup_sense, - []() { - TouchButtonThread->userButton.tick(); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }, - INPUT_BROKER_NONE, INPUT_BROKER_BACK); + ButtonConfig touchConfig; + touchConfig.pinNumber = BUTTON_PIN_TOUCH; + touchConfig.activeLow = true; + touchConfig.activePullup = true; + touchConfig.pullupSense = pullup_sense; + touchConfig.intRoutine = []() { + TouchButtonThread->userButton.tick(); + runASAP = true; + BaseType_t higherWake = 0; + mainDelay.interruptFromISR(&higherWake); + }; + touchConfig.singlePress = INPUT_BROKER_NONE; + touchConfig.longPress = INPUT_BROKER_BACK; + TouchButtonThread->initButton(touchConfig); #endif #if defined(CANCEL_BUTTON_PIN) // Buttons. Moved here cause we need NodeDB to be initialized CancelButtonThread = new ButtonThread("CancelButton"); - CancelButtonThread->initButton( - CANCEL_BUTTON_PIN, CANCEL_BUTTON_ACTIVE_LOW, CANCEL_BUTTON_ACTIVE_PULLUP, pullup_sense, - []() { - CancelButtonThread->userButton.tick(); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }, - INPUT_BROKER_CANCEL, INPUT_BROKER_SHUTDOWN, 4000); + ButtonConfig cancelConfig; + cancelConfig.pinNumber = CANCEL_BUTTON_PIN; + cancelConfig.activeLow = CANCEL_BUTTON_ACTIVE_LOW; + cancelConfig.activePullup = CANCEL_BUTTON_ACTIVE_PULLUP; + cancelConfig.pullupSense = pullup_sense; + cancelConfig.intRoutine = []() { + CancelButtonThread->userButton.tick(); + runASAP = true; + BaseType_t higherWake = 0; + mainDelay.interruptFromISR(&higherWake); + }; + cancelConfig.singlePress = INPUT_BROKER_CANCEL; + cancelConfig.longPress = INPUT_BROKER_SHUTDOWN; + cancelConfig.longPressTime = 4000; + CancelButtonThread->initButton(cancelConfig); #endif #if defined(ALT_BUTTON_PIN) // Buttons. Moved here cause we need NodeDB to be initialized BackButtonThread = new ButtonThread("BackButton"); - BackButtonThread->initButton( - ALT_BUTTON_PIN, ALT_BUTTON_ACTIVE_LOW, ALT_BUTTON_ACTIVE_PULLUP, pullup_sense, - []() { - BackButtonThread->userButton.tick(); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }, - INPUT_BROKER_ALT_PRESS, INPUT_BROKER_ALT_LONG, 500); + ButtonConfig backConfig; + backConfig.pinNumber = ALT_BUTTON_PIN; + backConfig.activeLow = ALT_BUTTON_ACTIVE_LOW; + backConfig.activePullup = ALT_BUTTON_ACTIVE_PULLUP; + backConfig.pullupSense = pullup_sense; + backConfig.intRoutine = []() { + BackButtonThread->userButton.tick(); + runASAP = true; + BaseType_t higherWake = 0; + mainDelay.interruptFromISR(&higherWake); + }; + backConfig.singlePress = INPUT_BROKER_ALT_PRESS; + backConfig.longPress = INPUT_BROKER_ALT_LONG; + backConfig.longPressTime = 500; + BackButtonThread->initButton(backConfig); #endif #if defined(BUTTON_PIN) @@ -997,27 +1020,42 @@ void setup() // Buttons. Moved here cause we need NodeDB to be initialized // If your variant.h has a BUTTON_PIN defined, go ahead and define BUTTON_ACTIVE_LOW and BUTTON_ACTIVE_PULLUP UserButtonThread = new ButtonThread("UserButton"); - if (screen) - UserButtonThread->initButton( - _pinNum, BUTTON_ACTIVE_LOW, BUTTON_ACTIVE_PULLUP, pullup_sense, - []() { - UserButtonThread->userButton.tick(); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }, - INPUT_BROKER_USER_PRESS, INPUT_BROKER_SELECT, 500, INPUT_BROKER_NONE, INPUT_BROKER_SHUTDOWN); - else - UserButtonThread->initButton( - _pinNum, BUTTON_ACTIVE_LOW, BUTTON_ACTIVE_PULLUP, pullup_sense, - []() { - UserButtonThread->userButton.tick(); - runASAP = true; - BaseType_t higherWake = 0; - mainDelay.interruptFromISR(&higherWake); - }, - INPUT_BROKER_USER_PRESS, INPUT_BROKER_SHUTDOWN, 5000, INPUT_BROKER_SEND_PING, INPUT_BROKER_NONE, 0, - INPUT_BROKER_GPS_TOGGLE); + if (screen) { + ButtonConfig userConfig; + userConfig.pinNumber = (uint8_t)_pinNum; + userConfig.activeLow = BUTTON_ACTIVE_LOW; + userConfig.activePullup = BUTTON_ACTIVE_PULLUP; + userConfig.pullupSense = pullup_sense; + userConfig.intRoutine = []() { + UserButtonThread->userButton.tick(); + runASAP = true; + BaseType_t higherWake = 0; + mainDelay.interruptFromISR(&higherWake); + }; + userConfig.singlePress = INPUT_BROKER_USER_PRESS; + userConfig.longPress = INPUT_BROKER_SELECT; + userConfig.longPressTime = 500; + userConfig.longLongPress = INPUT_BROKER_SHUTDOWN; + UserButtonThread->initButton(userConfig); + } else { + ButtonConfig userConfigNoScreen; + userConfigNoScreen.pinNumber = (uint8_t)_pinNum; + userConfigNoScreen.activeLow = BUTTON_ACTIVE_LOW; + userConfigNoScreen.activePullup = BUTTON_ACTIVE_PULLUP; + userConfigNoScreen.pullupSense = pullup_sense; + userConfigNoScreen.intRoutine = []() { + UserButtonThread->userButton.tick(); + runASAP = true; + BaseType_t higherWake = 0; + mainDelay.interruptFromISR(&higherWake); + }; + userConfigNoScreen.singlePress = INPUT_BROKER_USER_PRESS; + userConfigNoScreen.longPress = INPUT_BROKER_SHUTDOWN; + userConfigNoScreen.longPressTime = 5000; + userConfigNoScreen.doublePress = INPUT_BROKER_SEND_PING; + userConfigNoScreen.triplePress = INPUT_BROKER_GPS_TOGGLE; + UserButtonThread->initButton(userConfigNoScreen); + } #endif #endif diff --git a/src/mesh/MeshModule.cpp b/src/mesh/MeshModule.cpp index 62d3c82bc..c5748a560 100644 --- a/src/mesh/MeshModule.cpp +++ b/src/mesh/MeshModule.cpp @@ -244,10 +244,13 @@ void setReplyTo(meshtastic_MeshPacket *p, const meshtastic_MeshPacket &to) p->decoded.request_id = to.id; } -std::vector MeshModule::GetMeshModulesWithUIFrames() +std::vector MeshModule::GetMeshModulesWithUIFrames(int startIndex) { - std::vector modulesWithUIFrames; + + // Fill with nullptr up to startIndex + modulesWithUIFrames.resize(startIndex, nullptr); + if (modules) { for (auto i = modules->begin(); i != modules->end(); ++i) { auto &pi = **i; diff --git a/src/mesh/MeshModule.h b/src/mesh/MeshModule.h index f08b8f49c..eda3f8881 100644 --- a/src/mesh/MeshModule.h +++ b/src/mesh/MeshModule.h @@ -75,7 +75,7 @@ class MeshModule */ static void callModules(meshtastic_MeshPacket &mp, RxSource src = RX_SRC_RADIO); - static std::vector GetMeshModulesWithUIFrames(); + static std::vector GetMeshModulesWithUIFrames(int startIndex); static void observeUIEvents(Observer *observer); static AdminMessageHandleResult handleAdminMessageForAllModules(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 3eb3a5173..9433cc75d 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -1582,6 +1582,7 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact) // Mark the node's key as manually verified to indicate trustworthiness. updateGUIforNode = info; // powerFSM.trigger(EVENT_NODEDB_UPDATED); This event has been retired + sortMeshDB(); notifyObservers(true); // Force an update whether or not our node counts have changed } saveNodeDatabaseToDisk(); @@ -1685,6 +1686,31 @@ void NodeDB::updateFrom(const meshtastic_MeshPacket &mp) info->has_hops_away = true; info->hops_away = mp.hop_start - mp.hop_limit; } + sortMeshDB(); + } +} + +void NodeDB::sortMeshDB() +{ + if (!Throttle::isWithinTimespanMs(lastSort, 1000 * 5)) { + lastSort = millis(); + std::sort(meshNodes->begin(), meshNodes->end(), [](const meshtastic_NodeInfoLite &a, const meshtastic_NodeInfoLite &b) { + if (a.num == myNodeInfo.my_node_num) { + return true; + } + if (b.num == myNodeInfo.my_node_num) { + return false; + } + bool aFav = a.is_favorite; + bool bFav = b.is_favorite; + if (aFav != bFav) + return aFav; + if (a.last_heard == 0 || a.last_heard == UINT32_MAX) + return false; + if (b.last_heard == 0 || b.last_heard == UINT32_MAX) + return true; + return a.last_heard > b.last_heard; + }); } } diff --git a/src/mesh/NodeDB.h b/src/mesh/NodeDB.h index 90ca5aefd..b6e4d600b 100644 --- a/src/mesh/NodeDB.h +++ b/src/mesh/NodeDB.h @@ -282,6 +282,7 @@ class NodeDB bool duplicateWarned = false; uint32_t lastNodeDbSave = 0; // when we last saved our db to flash uint32_t lastBackupAttempt = 0; // when we last tried a backup automatically or manually + uint32_t lastSort = 0; // When last sorted the nodeDB /// Find a node in our DB, create an empty NodeInfoLite if missing meshtastic_NodeInfoLite *getOrCreateMeshNode(NodeNum n); @@ -310,6 +311,7 @@ class NodeDB bool saveChannelsToDisk(); bool saveDeviceStateToDisk(); bool saveNodeDatabaseToDisk(); + void sortMeshDB(); }; extern NodeDB *nodeDB; diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index b24f3ca00..4d8d6ce4b 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -154,7 +154,7 @@ int CannedMessageModule::splitConfiguredMessages() } void CannedMessageModule::drawHeader(OLEDDisplay *display, int16_t x, int16_t y, char *buffer) { - if (display->getWidth() > 128) { + if (graphics::isHighResolution) { if (this->dest == NODENUM_BROADCAST) { display->drawStringf(x, y, buffer, "To: Broadcast@%s", channels.getName(this->channel)); } else { @@ -245,12 +245,15 @@ void CannedMessageModule::updateDestinationSelectionList() } } + /* As the nodeDB is sorted, can skip this step // Sort by favorite, then last heard std::sort(this->filteredNodes.begin(), this->filteredNodes.end(), [](const NodeEntry &a, const NodeEntry &b) { if (a.node->is_favorite != b.node->is_favorite) return a.node->is_favorite > b.node->is_favorite; return a.lastHeard < b.lastHeard; }); + */ + scrollIndex = 0; // Show first result at the top destIndex = 0; // Highlight the first entry if (nodesChanged && runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { @@ -387,6 +390,7 @@ bool CannedMessageModule::handleTabSwitch(const InputEvent *event) // RESTORE THIS! if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) updateDestinationSelectionList(); + requestFocus(); UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; @@ -986,6 +990,7 @@ int32_t CannedMessageModule::runOnce() default: // Only insert ASCII printable characters (32–126) if (this->payload >= 32 && this->payload <= 126) { + requestFocus(); if (this->cursor == this->freetext.length()) { this->freetext += (char)this->payload; } else { diff --git a/src/modules/KeyVerificationModule.cpp b/src/modules/KeyVerificationModule.cpp index f5a9f2359..c0972c155 100644 --- a/src/modules/KeyVerificationModule.cpp +++ b/src/modules/KeyVerificationModule.cpp @@ -79,10 +79,10 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket & memset(message, 0, sizeof(message)); sprintf(message, "Verification: \n"); generateVerificationCode(message + 15); - sprintf(message + 24, "\nACCEPT\nREJECT"); + static const char *optionsArray[] = {"ACCEPT", "REJECT"}; LOG_INFO("Hash1 matches!"); if (screen) { - screen->showOverlayBanner(message, 30000, 2, [=](int selected) { + screen->showOverlayBanner(message, 30000, optionsArray, 2, [=](int selected) { if (selected == 0) { auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode); remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; diff --git a/src/modules/SystemCommandsModule.cpp b/src/modules/SystemCommandsModule.cpp index a6b01d68a..6a7da95af 100644 --- a/src/modules/SystemCommandsModule.cpp +++ b/src/modules/SystemCommandsModule.cpp @@ -100,9 +100,9 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event) case INPUT_BROKER_SEND_PING: service->refreshLocalMeshNode(); if (service->trySendPosition(NODENUM_BROADCAST, true)) { - IF_SCREEN(screen->showOverlayBanner("Position\nUpdate Sent", 3000)); + IF_SCREEN(screen->showOverlayBanner("Position\nSent", 3000)); } else { - IF_SCREEN(screen->showOverlayBanner("Node Info\nUpdate Sent", 3000)); + IF_SCREEN(screen->showOverlayBanner("Node Info\nSent", 3000)); } return true; // Power control @@ -113,6 +113,10 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event) shutdownAtMsec = millis() + DEFAULT_SHUTDOWN_SECONDS * 1000; // runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; return true; + + default: + // No other input events handled here + break; } return false; } \ No newline at end of file diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index 375d1e596..46a24a816 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -30,7 +30,7 @@ namespace graphics { -extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr); +extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool battery_only); } #if __has_include() #include "Sensor/AHT10.h" @@ -358,7 +358,7 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt int line = 1; // === Set Title - const char *titleStr = (SCREEN_WIDTH > 128) ? "Environment" : "Env."; + const char *titleStr = (graphics::isHighResolution) ? "Environment" : "Env."; // === Header === graphics::drawCommonHeader(display, x, y, titleStr); diff --git a/src/modules/Telemetry/PowerTelemetry.cpp b/src/modules/Telemetry/PowerTelemetry.cpp index df1505226..a92013d01 100644 --- a/src/modules/Telemetry/PowerTelemetry.cpp +++ b/src/modules/Telemetry/PowerTelemetry.cpp @@ -24,7 +24,7 @@ namespace graphics { -extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr); +extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool battery_only); } int32_t PowerTelemetryModule::runOnce() @@ -115,7 +115,7 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s int line = 1; // === Set Title - const char *titleStr = (SCREEN_WIDTH > 128) ? "Power Telem." : "Power"; + const char *titleStr = (graphics::isHighResolution) ? "Power Telem." : "Power"; // === Header === graphics::drawCommonHeader(display, x, y, titleStr); diff --git a/src/modules/WaypointModule.cpp b/src/modules/WaypointModule.cpp index 578e7183a..cab668406 100644 --- a/src/modules/WaypointModule.cpp +++ b/src/modules/WaypointModule.cpp @@ -137,10 +137,14 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading())) { const meshtastic_PositionLite &op = ourNode->position; float myHeading; - if (screen->hasHeading()) - myHeading = (screen->getHeading()) * PI / 180; // gotta convert compass degrees to Radians - else - myHeading = screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); + if (screen->ignoreCompass) { + myHeading = 0; + } else { + if (screen->hasHeading()) + myHeading = (screen->getHeading()) * PI / 180; // gotta convert compass degrees to Radians + else + myHeading = screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); + } graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, (compassDiam / 2)); // Compass bearing to waypoint @@ -148,7 +152,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(wp.latitude_i), DegD(wp.longitude_i)); // If the top of the compass is a static north then bearingToOther can be drawn on the compass directly // If the top of the compass is not a static north we need adjust bearingToOther based on heading - if (!config.display.compass_north_top) + if (!screen->ignoreCompass) bearingToOther -= myHeading; graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther); diff --git a/src/serialization/MeshPacketSerializer.cpp b/src/serialization/MeshPacketSerializer.cpp index fc8531298..29a9b6840 100644 --- a/src/serialization/MeshPacketSerializer.cpp +++ b/src/serialization/MeshPacketSerializer.cpp @@ -79,7 +79,8 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, msgPayload["relative_humidity"] = new JSONValue(decoded->variant.environment_metrics.relative_humidity); } if (decoded->variant.environment_metrics.has_barometric_pressure) { - msgPayload["barometric_pressure"] = new JSONValue(decoded->variant.environment_metrics.barometric_pressure); + msgPayload["barometric_pressure"] = + new JSONValue(decoded->variant.environment_metrics.barometric_pressure); } if (decoded->variant.environment_metrics.has_gas_resistance) { msgPayload["gas_resistance"] = new JSONValue(decoded->variant.environment_metrics.gas_resistance); @@ -125,13 +126,16 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp, msgPayload["pm100"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_standard); } if (decoded->variant.air_quality_metrics.has_pm10_environmental) { - msgPayload["pm10_e"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_environmental); + msgPayload["pm10_e"] = + new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_environmental); } if (decoded->variant.air_quality_metrics.has_pm25_environmental) { - msgPayload["pm25_e"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_environmental); + msgPayload["pm25_e"] = + new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_environmental); } if (decoded->variant.air_quality_metrics.has_pm100_environmental) { - msgPayload["pm100_e"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_environmental); + msgPayload["pm100_e"] = + new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_environmental); } } else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) { if (decoded->variant.power_metrics.has_ch1_voltage) { diff --git a/variants/portduino/platformio.ini b/variants/portduino/platformio.ini index 6da827508..5293b12b9 100644 --- a/variants/portduino/platformio.ini +++ b/variants/portduino/platformio.ini @@ -22,7 +22,6 @@ lib_deps = ${native_base.lib_deps} ${device-ui_base.lib_deps} build_flags = ${native_base.build_flags} -Os -lX11 -linput -lxkbcommon -ffunction-sections -fdata-sections -Wl,--gc-sections - -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 -D RAM_SIZE=16384 -D USE_X11=1 -D HAS_TFT=1 @@ -51,7 +50,6 @@ lib_deps = ${device-ui_base.lib_deps} board_level = extra build_flags = ${native_base.build_flags} -Os -ffunction-sections -fdata-sections -Wl,--gc-sections - -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 -D RAM_SIZE=8192 -D USE_FRAMEBUFFER=1 -D LV_COLOR_DEPTH=32 @@ -81,7 +79,6 @@ lib_deps = ${device-ui_base.lib_deps} board_level = extra build_flags = ${native_base.build_flags} -O0 -fsanitize=address -lX11 -linput -lxkbcommon - -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 -D DEBUG_HEAP -D RAM_SIZE=16384 -D USE_X11=1 diff --git a/variants/rak4631/variant.h b/variants/rak4631/variant.h index cd8f46153..f5ec11ef2 100644 --- a/variants/rak4631/variant.h +++ b/variants/rak4631/variant.h @@ -88,8 +88,8 @@ static const uint8_t A7 = PIN_A7; #define ADC_RESOLUTION 14 // Other pins - #define WB_I2C1_SDA (13) // SENSOR_SLOT IO_SLOT - #define WB_I2C1_SCL (14) // SENSOR_SLOT IO_SLOT +#define WB_I2C1_SDA (13) // SENSOR_SLOT IO_SLOT +#define WB_I2C1_SCL (14) // SENSOR_SLOT IO_SLOT #define PIN_AREF (2) #define PIN_NFC1 (9) From 2b97576b187e63d139fa1c211c323f76e8dc5f7f Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 27 Jun 2025 06:26:34 -0500 Subject: [PATCH 15/25] NRF52 BLE fixes / tweaks (#7152) * Try-fix: Flaky NRF52 bluetooth pairing for some users * Safe access for screen pointer --- src/platform/nrf52/NRF52Bluetooth.cpp | 57 +++++++++++++++------------ 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/src/platform/nrf52/NRF52Bluetooth.cpp b/src/platform/nrf52/NRF52Bluetooth.cpp index 89e92afc6..6f0e7250f 100644 --- a/src/platform/nrf52/NRF52Bluetooth.cpp +++ b/src/platform/nrf52/NRF52Bluetooth.cpp @@ -314,7 +314,9 @@ void NRF52Bluetooth::onConnectionSecured(uint16_t conn_handle) } bool NRF52Bluetooth::onPairingPasskey(uint16_t conn_handle, uint8_t const passkey[6], bool match_request) { - LOG_INFO("BLE pair process started with passkey %.3s %.3s", passkey, passkey + 3); + char passkey1[4] = {passkey[0], passkey[1], passkey[2], '\0'}; + char passkey2[4] = {passkey[3], passkey[4], passkey[5], '\0'}; + LOG_INFO("BLE pair process started with passkey %s %s", passkey1, passkey2); powerFSM.trigger(EVENT_BLUETOOTH_PAIR); // Get passkey as string @@ -327,31 +329,33 @@ bool NRF52Bluetooth::onPairingPasskey(uint16_t conn_handle, uint8_t const passke bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(textkey)); #if !defined(MESHTASTIC_EXCLUDE_SCREEN) // Todo: migrate this display code back into Screen class, and observe bluetoothStatus - screen->startAlert([](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { - char btPIN[16] = "888888"; - snprintf(btPIN, sizeof(btPIN), "%06u", configuredPasskey); - int x_offset = display->width() / 2; - int y_offset = display->height() <= 80 ? 0 : 12; - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_MEDIUM); - display->drawString(x_offset + x, y_offset + y, "Bluetooth"); + if (screen) { + screen->startAlert([](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { + char btPIN[16] = "888888"; + snprintf(btPIN, sizeof(btPIN), "%06u", configuredPasskey); + int x_offset = display->width() / 2; + int y_offset = display->height() <= 80 ? 0 : 12; + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_MEDIUM); + display->drawString(x_offset + x, y_offset + y, "Bluetooth"); - display->setFont(FONT_SMALL); - y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_MEDIUM - 4 : y_offset + FONT_HEIGHT_MEDIUM + 5; - display->drawString(x_offset + x, y_offset + y, "Enter this code"); + display->setFont(FONT_SMALL); + y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_MEDIUM - 4 : y_offset + FONT_HEIGHT_MEDIUM + 5; + display->drawString(x_offset + x, y_offset + y, "Enter this code"); - display->setFont(FONT_LARGE); - String displayPin(btPIN); - String pin = displayPin.substring(0, 3) + " " + displayPin.substring(3, 6); - y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_SMALL - 5 : y_offset + FONT_HEIGHT_SMALL + 5; - display->drawString(x_offset + x, y_offset + y, pin); + display->setFont(FONT_LARGE); + String displayPin(btPIN); + String pin = displayPin.substring(0, 3) + " " + displayPin.substring(3, 6); + y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_SMALL - 5 : y_offset + FONT_HEIGHT_SMALL + 5; + display->drawString(x_offset + x, y_offset + y, pin); - display->setFont(FONT_SMALL); - String deviceName = "Name: "; - deviceName.concat(getDeviceName()); - y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_LARGE - 6 : y_offset + FONT_HEIGHT_LARGE + 5; - display->drawString(x_offset + x, y_offset + y, deviceName); - }); + display->setFont(FONT_SMALL); + String deviceName = "Name: "; + deviceName.concat(getDeviceName()); + y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_LARGE - 6 : y_offset + FONT_HEIGHT_LARGE + 5; + display->drawString(x_offset + x, y_offset + y, deviceName); + }); + } #endif if (match_request) { uint32_t start_time = millis(); @@ -394,8 +398,7 @@ void NRF52Bluetooth::onPairingCompleted(uint16_t conn_handle, uint8_t auth_statu { if (auth_status == BLE_GAP_SEC_STATUS_SUCCESS) { LOG_INFO("BLE pair success"); - bluetoothStatus->updateStatus( - new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED)); + bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED)); } else { LOG_INFO("BLE pair failed"); // Notify UI (or any other interested firmware components) @@ -404,7 +407,9 @@ void NRF52Bluetooth::onPairingCompleted(uint16_t conn_handle, uint8_t auth_statu } // Todo: migrate this display code back into Screen class, and observe bluetoothStatus - screen->endAlert(); + if (screen) { + screen->endAlert(); + } } void NRF52Bluetooth::sendLog(const uint8_t *logMessage, size_t length) From de5b55921e84f477cecaff6db4ff65655e0a94a1 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Fri, 27 Jun 2025 11:06:19 -0500 Subject: [PATCH 16/25] Extra check on UDP packets --- src/mesh/udp/UdpMulticastHandler.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/udp/UdpMulticastHandler.h b/src/mesh/udp/UdpMulticastHandler.h index 39bd61021..ac4f86020 100644 --- a/src/mesh/udp/UdpMulticastHandler.h +++ b/src/mesh/udp/UdpMulticastHandler.h @@ -44,7 +44,7 @@ class UdpMulticastHandler final meshtastic_MeshPacket mp; LOG_DEBUG("Decoding MeshPacket from UDP len=%u", packetLength); bool isPacketDecoded = pb_decode_from_bytes(packet.data(), packetLength, &meshtastic_MeshPacket_msg, &mp); - if (isPacketDecoded && router) { + if (isPacketDecoded && router && mp.which_payload_variant == meshtastic_MeshPacket_encrypted_tag) { UniquePacketPoolPacket p = packetPool.allocUniqueCopy(mp); // Unset received SNR/RSSI p->rx_snr = 0; From 2ea70927c88ab2da3a525c93a1798ce02dce9b1a Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 27 Jun 2025 11:06:50 -0500 Subject: [PATCH 17/25] Revert "automated bumps (#7097)" This reverts commit 4308bbc156c81a240f31c1860fd792264f5b755f. --- bin/org.meshtastic.meshtasticd.metainfo.xml | 3 --- debian/changelog | 7 ++----- version.properties | 2 +- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/bin/org.meshtastic.meshtasticd.metainfo.xml b/bin/org.meshtastic.meshtasticd.metainfo.xml index f9f647dae..4b07f6388 100644 --- a/bin/org.meshtastic.meshtasticd.metainfo.xml +++ b/bin/org.meshtastic.meshtasticd.metainfo.xml @@ -87,9 +87,6 @@ - - https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.1 - https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.0 diff --git a/debian/changelog b/debian/changelog index 4629e8c3a..d607be68c 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -meshtasticd (2.7.1.0) UNRELEASED; urgency=medium +meshtasticd (2.7.0.0) UNRELEASED; urgency=medium [ Austin Lane ] * Initial packaging @@ -22,7 +22,4 @@ meshtasticd (2.7.1.0) UNRELEASED; urgency=medium [ ] * GitHub Actions Automatic version bump - [ ] - * GitHub Actions Automatic version bump - - -- Sat, 21 Jun 2025 15:51:49 +0000 + -- Mon, 16 Jun 2025 02:10:49 +0000 diff --git a/version.properties b/version.properties index 3fe1aa385..91c81a0c9 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ [VERSION] major = 2 minor = 7 -build = 1 +build = 0 From f6743798e2db0c518cd257ff2ade95eb06999ee2 Mon Sep 17 00:00:00 2001 From: porkcube Date: Fri, 27 Jun 2025 12:09:04 -0400 Subject: [PATCH 18/25] cleanup Shutting down -> Shutting Down awkwardness (#7099) Co-authored-by: Jonathan Bennett --- src/Power.cpp | 2 +- src/input/ExpressLRSFiveWay.cpp | 4 ++-- src/modules/SystemCommandsModule.cpp | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Power.cpp b/src/Power.cpp index 400b6c6eb..fb5db416e 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -682,7 +682,7 @@ bool Power::setup() void Power::shutdown() { - LOG_INFO("Shutting down"); + LOG_INFO("Shutting Down"); #if defined(ARCH_NRF52) || defined(ARCH_ESP32) || defined(ARCH_RP2040) #ifdef PIN_LED1 diff --git a/src/input/ExpressLRSFiveWay.cpp b/src/input/ExpressLRSFiveWay.cpp index 1981a45d4..53bcedc63 100644 --- a/src/input/ExpressLRSFiveWay.cpp +++ b/src/input/ExpressLRSFiveWay.cpp @@ -235,7 +235,7 @@ void ExpressLRSFiveWay::shutdown() { LOG_INFO("Shutdown from long press"); powerFSM.trigger(EVENT_PRESS); - screen->startAlert("Shutting down..."); + screen->startAlert("Shutting Down..."); // Don't set alerting = true. We don't want to auto-dismiss this alert. playShutdownMelody(); // In case user adds a buzzer @@ -250,4 +250,4 @@ void ExpressLRSFiveWay::click() ExpressLRSFiveWay *expressLRSFiveWayInput = nullptr; -#endif \ No newline at end of file +#endif diff --git a/src/modules/SystemCommandsModule.cpp b/src/modules/SystemCommandsModule.cpp index 6a7da95af..08c87ec64 100644 --- a/src/modules/SystemCommandsModule.cpp +++ b/src/modules/SystemCommandsModule.cpp @@ -107,8 +107,8 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event) return true; // Power control case INPUT_BROKER_SHUTDOWN: - LOG_ERROR("Shutting down"); - IF_SCREEN(screen->showOverlayBanner("Shutting down...")); + LOG_ERROR("Shutting Down"); + IF_SCREEN(screen->showOverlayBanner("Shutting Down...")); nodeDB->saveToDisk(); shutdownAtMsec = millis() + DEFAULT_SHUTDOWN_SECONDS * 1000; // runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; @@ -119,4 +119,4 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event) break; } return false; -} \ No newline at end of file +} From a97df4bb524d0f53276a0662e286b8b385a625b1 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Fri, 27 Jun 2025 11:22:01 -0500 Subject: [PATCH 19/25] Sanity check incoming UDP --- src/mesh/udp/UdpMulticastHandler.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/mesh/udp/UdpMulticastHandler.h b/src/mesh/udp/UdpMulticastHandler.h index ac4f86020..d1cc1065c 100644 --- a/src/mesh/udp/UdpMulticastHandler.h +++ b/src/mesh/udp/UdpMulticastHandler.h @@ -45,6 +45,9 @@ class UdpMulticastHandler final LOG_DEBUG("Decoding MeshPacket from UDP len=%u", packetLength); bool isPacketDecoded = pb_decode_from_bytes(packet.data(), packetLength, &meshtastic_MeshPacket_msg, &mp); if (isPacketDecoded && router && mp.which_payload_variant == meshtastic_MeshPacket_encrypted_tag) { + mp.pki_encrypted = false; + mp.public_key.size = 0; + memset(mp.public_key.bytes, 0, sizeof(mp.public_key.bytes)); UniquePacketPoolPacket p = packetPool.allocUniqueCopy(mp); // Unset received SNR/RSSI p->rx_snr = 0; From 705515ace23e8a104f20ce078a3d3c650f16c5bc Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Fri, 27 Jun 2025 11:46:33 -0500 Subject: [PATCH 20/25] Resize meshNodes to MAX + 1 to avoid crash during sort --- src/mesh/NodeDB.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 9433cc75d..cc3639f19 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -1156,7 +1156,7 @@ void NodeDB::loadFromDisk() LOG_WARN("Node count %d exceeds MAX_NUM_NODES %d, truncating", numMeshNodes, MAX_NUM_NODES); numMeshNodes = MAX_NUM_NODES; } - meshNodes->resize(MAX_NUM_NODES); + meshNodes->resize(MAX_NUM_NODES + 1); // The rp2040, rp2035, and maybe other targets, have a problem doing a sort() when full // static DeviceState scratch; We no longer read into a tempbuf because this structure is 15KB of valuable RAM state = loadProto(deviceStateFileName, meshtastic_DeviceState_size, sizeof(meshtastic_DeviceState), From 2bcf608654facd685321779b339584644a1ecc5d Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sat, 28 Jun 2025 08:19:31 -0500 Subject: [PATCH 21/25] Last second fixes (#7156) * Ditch the 30 second delay for button presses * Only order strictly weakly * Too many comments! * Only sort the populated meshNodes --- src/input/ButtonThread.cpp | 11 ++++++----- src/mesh/NodeDB.cpp | 33 ++++++++++++++++----------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/input/ButtonThread.cpp b/src/input/ButtonThread.cpp index da9878fa4..ad667f003 100644 --- a/src/input/ButtonThread.cpp +++ b/src/input/ButtonThread.cpp @@ -58,15 +58,15 @@ bool ButtonThread::initButton(const ButtonConfig &config) userButton.attachLongPressStart( [](void *callerThread) -> void { ButtonThread *thread = (ButtonThread *)callerThread; - if (millis() > 30000) // hold off 30s after boot - thread->btnEvent = BUTTON_EVENT_LONG_PRESSED; + // if (millis() > 30000) // hold off 30s after boot + thread->btnEvent = BUTTON_EVENT_LONG_PRESSED; }, this); userButton.attachLongPressStop( [](void *callerThread) -> void { ButtonThread *thread = (ButtonThread *)callerThread; - if (millis() > 30000) // hold off 30s after boot - thread->btnEvent = BUTTON_EVENT_LONG_RELEASED; + // if (millis() > 30000) // hold off 30s after boot + thread->btnEvent = BUTTON_EVENT_LONG_RELEASED; }, this); } @@ -254,7 +254,8 @@ int32_t ButtonThread::runOnce() case BUTTON_EVENT_LONG_RELEASED: { LOG_INFO("LONG PRESS RELEASE"); - if (_longLongPress != INPUT_BROKER_NONE && (millis() - buttonPressStartTime) >= _longLongPressTime) { + if (millis() > 30000 && _longLongPress != INPUT_BROKER_NONE && + (millis() - buttonPressStartTime) >= _longLongPressTime) { evt.inputEvent = _longLongPress; this->notifyObservers(&evt); } diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index cc3639f19..8990d4b4f 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -1694,23 +1694,22 @@ void NodeDB::sortMeshDB() { if (!Throttle::isWithinTimespanMs(lastSort, 1000 * 5)) { lastSort = millis(); - std::sort(meshNodes->begin(), meshNodes->end(), [](const meshtastic_NodeInfoLite &a, const meshtastic_NodeInfoLite &b) { - if (a.num == myNodeInfo.my_node_num) { - return true; - } - if (b.num == myNodeInfo.my_node_num) { - return false; - } - bool aFav = a.is_favorite; - bool bFav = b.is_favorite; - if (aFav != bFav) - return aFav; - if (a.last_heard == 0 || a.last_heard == UINT32_MAX) - return false; - if (b.last_heard == 0 || b.last_heard == UINT32_MAX) - return true; - return a.last_heard > b.last_heard; - }); + std::sort(meshNodes->begin(), meshNodes->begin() + numMeshNodes, + [](const meshtastic_NodeInfoLite &a, const meshtastic_NodeInfoLite &b) { + if (a.num == myNodeInfo.my_node_num) { + return true; + } + if (b.num == myNodeInfo.my_node_num) { + return false; + } + bool aFav = a.is_favorite; + bool bFav = b.is_favorite; + if (aFav != bFav) + return aFav; + if (a.last_heard != b.last_heard) + return a.last_heard > b.last_heard; + return a.num > b.num; + }); } } From b6a13f1114ed2c259881fd8761832a5aaae97d3b Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sat, 28 Jun 2025 22:54:03 -0500 Subject: [PATCH 22/25] Add check for theoretically impossible comparison, and drop nodenum comparison (#7165) --- src/mesh/NodeDB.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 8990d4b4f..bd4911a9b 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -1696,6 +1696,8 @@ void NodeDB::sortMeshDB() lastSort = millis(); std::sort(meshNodes->begin(), meshNodes->begin() + numMeshNodes, [](const meshtastic_NodeInfoLite &a, const meshtastic_NodeInfoLite &b) { + if (a.num == myNodeInfo.my_node_num && b.num == myNodeInfo.my_node_num) // in theory impossible + return false; if (a.num == myNodeInfo.my_node_num) { return true; } @@ -1706,9 +1708,7 @@ void NodeDB::sortMeshDB() bool bFav = b.is_favorite; if (aFav != bFav) return aFav; - if (a.last_heard != b.last_heard) - return a.last_heard > b.last_heard; - return a.num > b.num; + return a.last_heard > b.last_heard; }); } } From 26df4f81420936390bdbbbac9e593161d8e943dc Mon Sep 17 00:00:00 2001 From: Andrew Yong Date: Mon, 30 Jun 2025 19:05:24 +0800 Subject: [PATCH 23/25] fix(xiao_ble): Define xiao_ble I2C pins in parent variant (fixes #7163) (#7164) This restores the previously-defined I2C pins that got lost in the cleanup (#7024) Signed-off-by: Andrew Yong --- variants/seeed_xiao_nrf52840_kit/variant.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/variants/seeed_xiao_nrf52840_kit/variant.h b/variants/seeed_xiao_nrf52840_kit/variant.h index d2bbfdda9..a65500612 100644 --- a/variants/seeed_xiao_nrf52840_kit/variant.h +++ b/variants/seeed_xiao_nrf52840_kit/variant.h @@ -179,7 +179,11 @@ static const uint8_t SCK = PIN_SPI_SCK; #define I2C_NO_RESCAN // I2C is a bit finicky, don't scan too much #define WIRE_INTERFACES_COUNT 1 -#if !defined(XIAO_BLE_LEGACY_PINOUT) && !defined(GPS_L76K) +#if defined(XIAO_BLE_LEGACY_PINOUT) +// Used for I2C by DIY xiao_ble variant +#define PIN_WIRE_SDA D4 +#define PIN_WIRE_SCL D5 +#elif !defined(GPS_L76K) // If D6 and D7 are free, I2C is probably the most versatile assignment #define PIN_WIRE_SDA D6 #define PIN_WIRE_SCL D7 From be06a7d88121ce5a0f3741d3968525e15610e893 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 06:05:43 -0500 Subject: [PATCH 24/25] automated bumps (#7155) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --- bin/org.meshtastic.meshtasticd.metainfo.xml | 3 +++ debian/changelog | 7 +++++-- version.properties | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/bin/org.meshtastic.meshtasticd.metainfo.xml b/bin/org.meshtastic.meshtasticd.metainfo.xml index 4b07f6388..ed57386a3 100644 --- a/bin/org.meshtastic.meshtasticd.metainfo.xml +++ b/bin/org.meshtastic.meshtasticd.metainfo.xml @@ -87,6 +87,9 @@ + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.1 + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.0 diff --git a/debian/changelog b/debian/changelog index d607be68c..70a01bab4 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -meshtasticd (2.7.0.0) UNRELEASED; urgency=medium +meshtasticd (2.7.1.0) UNRELEASED; urgency=medium [ Austin Lane ] * Initial packaging @@ -22,4 +22,7 @@ meshtasticd (2.7.0.0) UNRELEASED; urgency=medium [ ] * GitHub Actions Automatic version bump - -- Mon, 16 Jun 2025 02:10:49 +0000 + [ ] + * GitHub Actions Automatic version bump + + -- Fri, 27 Jun 2025 20:12:21 +0000 diff --git a/version.properties b/version.properties index 91c81a0c9..3fe1aa385 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ [VERSION] major = 2 minor = 7 -build = 0 +build = 1 From 4bd416413a2ecad41f9ba3d95e6c68c8b8b2278a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 16:04:12 -0500 Subject: [PATCH 25/25] chore(deps): update meshtastic/device-ui digest to 4b7bf36 (#7178) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 693fdc9c3..0143038af 100644 --- a/platformio.ini +++ b/platformio.ini @@ -109,7 +109,7 @@ lib_deps = [device-ui_base] lib_deps = # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master - https://github.com/meshtastic/device-ui/archive/cdc6e5bdeedb8293d10e4a02be6ca64e95a7c515.zip + https://github.com/meshtastic/device-ui/archive/4b7bf369adfa5a7bd419fa8293d21206576d52d0.zip ; Common libs for environmental measurements in telemetry module [environmental_base]