From 6f56ccd283f4279af88a4f241cabc7e0c19392f8 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Fri, 19 Sep 2025 21:16:19 -0500 Subject: [PATCH 01/14] C6l fixes (#8047) --- src/gps/GPS.cpp | 2 ++ src/nimble/NimbleBluetooth.cpp | 7 +++++++ src/nimble/NimbleBluetooth.h | 1 + 3 files changed, 10 insertions(+) diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index a4c7464b8..3bf2710fe 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -516,6 +516,7 @@ bool GPS::setup() } } // Rare Serial Speeds +#ifndef CONFIG_IDF_TARGET_ESP32C6 if (probeTries == GPS_PROBETRIES) { LOG_DEBUG("Probe for GPS at %d", rareSerialSpeeds[speedSelect]); gnssModel = probe(rareSerialSpeeds[speedSelect]); @@ -526,6 +527,7 @@ bool GPS::setup() } } } +#endif } if (gnssModel != GNSS_MODEL_UNKNOWN) { diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp index 0eb8e9bdd..accf6c5dc 100644 --- a/src/nimble/NimbleBluetooth.cpp +++ b/src/nimble/NimbleBluetooth.cpp @@ -231,6 +231,10 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks { LOG_INFO("BLE disconnect"); #endif +#ifdef NIMBLE_TWO + if (ble->isDeInit) + return; +#endif meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED); bluetoothStatus->updateStatus(&newStatus); @@ -270,6 +274,7 @@ void NimbleBluetooth::deinit() { #ifdef ARCH_ESP32 LOG_INFO("Disable bluetooth until reboot"); + isDeInit = true; #ifdef BLE_LED #ifdef BLE_LED_INVERTED @@ -278,8 +283,10 @@ void NimbleBluetooth::deinit() digitalWrite(BLE_LED, LOW); #endif #endif +#ifndef NIMBLE_TWO NimBLEDevice::deinit(); #endif +#endif } // Has initial setup been completed diff --git a/src/nimble/NimbleBluetooth.h b/src/nimble/NimbleBluetooth.h index 899355b4d..458fa4a67 100644 --- a/src/nimble/NimbleBluetooth.h +++ b/src/nimble/NimbleBluetooth.h @@ -15,6 +15,7 @@ class NimbleBluetooth : BluetoothApi #if defined(NIMBLE_TWO) void startAdvertising(); #endif + bool isDeInit = false; private: void setupService(); From 8db9b24934f04f2475efcf803ea9a5093a7c3d6e Mon Sep 17 00:00:00 2001 From: Markus <974709+Links2004@users.noreply.github.com> Date: Sat, 20 Sep 2025 13:33:41 +0200 Subject: [PATCH 02/14] fix build with HAS_TELEMETRY 0 (#8051) --- src/modules/Modules.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index 757753d45..b3c15e764 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -241,7 +241,7 @@ void setupModules() #if HAS_TELEMETRY new DeviceTelemetryModule(); #endif -#if HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +#if HAS_TELEMETRY && HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR if (moduleConfig.has_telemetry && (moduleConfig.telemetry.environment_measurement_enabled || moduleConfig.telemetry.environment_screen_enabled)) { new EnvironmentTelemetryModule(); From 44968415a5a71a7320bbed13c1534e3686aefd9f Mon Sep 17 00:00:00 2001 From: Markus <974709+Links2004@users.noreply.github.com> Date: Sat, 20 Sep 2025 13:33:41 +0200 Subject: [PATCH 03/14] fix build with HAS_TELEMETRY 0 (#8051) --- src/modules/Modules.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index 757753d45..b3c15e764 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -241,7 +241,7 @@ void setupModules() #if HAS_TELEMETRY new DeviceTelemetryModule(); #endif -#if HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +#if HAS_TELEMETRY && HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR if (moduleConfig.has_telemetry && (moduleConfig.telemetry.environment_measurement_enabled || moduleConfig.telemetry.environment_screen_enabled)) { new EnvironmentTelemetryModule(); From 1d3c47c5fa813e6fe44627063fa041aa28735057 Mon Sep 17 00:00:00 2001 From: GUVWAF <78759985+GUVWAF@users.noreply.github.com> Date: Sat, 20 Sep 2025 13:37:40 +0200 Subject: [PATCH 04/14] Make sure to ACK ACKs/replies if next-hop routing is used (#8052) * Make sure to ACK ACKs/replies if next-hop routing is used To stop their retransmissions; hop limit of 0 is enough * Update src/mesh/ReliableRouter.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Ben Meadors Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/mesh/ReliableRouter.cpp | 45 +++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/src/mesh/ReliableRouter.cpp b/src/mesh/ReliableRouter.cpp index 6d098b669..cca838ff0 100644 --- a/src/mesh/ReliableRouter.cpp +++ b/src/mesh/ReliableRouter.cpp @@ -97,27 +97,34 @@ bool ReliableRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) void ReliableRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtastic_Routing *c) { if (isToUs(p)) { // ignore ack/nak/want_ack packets that are not address to us (we only handle 0 hop reliability) - if (p->want_ack) { - if (MeshModule::currentReply) { - LOG_DEBUG("Another module replied to this message, no need for 2nd ack"); - } else if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { - // A response may be set to want_ack for retransmissions, but we don't need to ACK a response if it received an - // implicit ACK already. If we received it directly, only ACK with a hop limit of 0 - if (!p->decoded.request_id) - sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, + if (!MeshModule::currentReply) { + if (p->want_ack) { + if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag) { + /* A response may be set to want_ack for retransmissions, but we don't need to ACK a response if it received + an implicit ACK already. If we received it directly or via NextHopRouter, only ACK with a hop limit of 0 to + make sure the other side stops retransmitting. */ + if (!p->decoded.request_id && !p->decoded.reply_id) { + sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, + routingModule->getHopLimitForResponse(p->hop_start, p->hop_limit)); + } else if ((p->hop_start > 0 && p->hop_start == p->hop_limit) || p->next_hop != NO_NEXT_HOP_PREFERENCE) { + sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, 0); + } + } else if (p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag && p->channel == 0 && + (nodeDB->getMeshNode(p->from) == nullptr || nodeDB->getMeshNode(p->from)->user.public_key.size == 0)) { + LOG_INFO("PKI packet from unknown node, send PKI_UNKNOWN_PUBKEY"); + sendAckNak(meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY, getFrom(p), p->id, channels.getPrimaryIndex(), routingModule->getHopLimitForResponse(p->hop_start, p->hop_limit)); - else if (p->hop_start > 0 && p->hop_start == p->hop_limit) - sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, 0); - } else if (p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag && p->channel == 0 && - (nodeDB->getMeshNode(p->from) == nullptr || nodeDB->getMeshNode(p->from)->user.public_key.size == 0)) { - LOG_INFO("PKI packet from unknown node, send PKI_UNKNOWN_PUBKEY"); - sendAckNak(meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY, getFrom(p), p->id, channels.getPrimaryIndex(), - routingModule->getHopLimitForResponse(p->hop_start, p->hop_limit)); - } else { - // Send a 'NO_CHANNEL' error on the primary channel if want_ack packet destined for us cannot be decoded - sendAckNak(meshtastic_Routing_Error_NO_CHANNEL, getFrom(p), p->id, channels.getPrimaryIndex(), - routingModule->getHopLimitForResponse(p->hop_start, p->hop_limit)); + } else { + // Send a 'NO_CHANNEL' error on the primary channel if want_ack packet destined for us cannot be decoded + sendAckNak(meshtastic_Routing_Error_NO_CHANNEL, getFrom(p), p->id, channels.getPrimaryIndex(), + routingModule->getHopLimitForResponse(p->hop_start, p->hop_limit)); + } + } else if (p->next_hop == nodeDB->getLastByteOfNodeNum(getNodeNum()) && p->hop_limit > 0) { + // No wantAck, but we need to ACK with hop limit of 0 if we were the next hop to stop their retransmissions + sendAckNak(meshtastic_Routing_Error_NONE, getFrom(p), p->id, p->channel, 0); } + } else { + LOG_DEBUG("Another module replied to this message, no need for 2nd ack"); } if (p->which_payload_variant == meshtastic_MeshPacket_decoded_tag && c && c->error_reason == meshtastic_Routing_Error_PKI_UNKNOWN_PUBKEY) { From db2f79b6c467fc77ac7c024fb6838bb8d27c0324 Mon Sep 17 00:00:00 2001 From: WillyJL Date: Sat, 20 Sep 2025 14:04:27 +0200 Subject: [PATCH 05/14] Fix last build issues on develop (#8046) --- src/graphics/Screen.cpp | 1161 +++++++++++++++++++-------------------- 1 file changed, 579 insertions(+), 582 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 689c550d3..a440ecab9 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -954,708 +954,705 @@ void Screen::setFrames(FrameFocus focus) #if defined(DISPLAY_CLOCK_FRAME) if (!hiddenFrames.clock) { - if (!hiddenFrames.clock) { - fsi.positions.clock = numframes; + fsi.positions.clock = numframes; #if defined(M5STACK_UNITC6L) - normalFrames[numframes++] = graphics::ClockRenderer::drawAnalogClockFrame; + normalFrames[numframes++] = graphics::ClockRenderer::drawAnalogClockFrame; #else - normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame - : graphics::ClockRenderer::drawDigitalClockFrame; + normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame + : graphics::ClockRenderer::drawDigitalClockFrame; #endif - indicatorIcons.push_back(digital_icon_clock); + indicatorIcons.push_back(digital_icon_clock); + } #endif - // Declare this early so it’s available in FOCUS_PRESERVE block - bool willInsertTextMessage = shouldDrawMessage(&devicestate.rx_text_message); + // Declare this early so it’s available in FOCUS_PRESERVE block + bool willInsertTextMessage = shouldDrawMessage(&devicestate.rx_text_message); - if (!hiddenFrames.home) { - fsi.positions.home = numframes; - normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused; - indicatorIcons.push_back(icon_home); - } + if (!hiddenFrames.home) { + fsi.positions.home = numframes; + normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused; + indicatorIcons.push_back(icon_home); + } - fsi.positions.textMessage = numframes; - normalFrames[numframes++] = graphics::MessageRenderer::drawTextMessageFrame; - indicatorIcons.push_back(icon_mail); + fsi.positions.textMessage = numframes; + normalFrames[numframes++] = graphics::MessageRenderer::drawTextMessageFrame; + indicatorIcons.push_back(icon_mail); #ifndef USE_EINK - if (!hiddenFrames.nodelist) { - fsi.positions.nodelist = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen; - indicatorIcons.push_back(icon_nodes); - } + if (!hiddenFrames.nodelist) { + 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 - if (!hiddenFrames.nodelist_lastheard) { - fsi.positions.nodelist_lastheard = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen; - indicatorIcons.push_back(icon_nodes); - } - if (!hiddenFrames.nodelist_hopsignal) { - fsi.positions.nodelist_hopsignal = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen; - indicatorIcons.push_back(icon_signal); - } - if (!hiddenFrames.nodelist_distance) { - fsi.positions.nodelist_distance = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen; - indicatorIcons.push_back(icon_distance); - } + if (!hiddenFrames.nodelist_lastheard) { + fsi.positions.nodelist_lastheard = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen; + indicatorIcons.push_back(icon_nodes); + } + if (!hiddenFrames.nodelist_hopsignal) { + fsi.positions.nodelist_hopsignal = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen; + indicatorIcons.push_back(icon_signal); + } + if (!hiddenFrames.nodelist_distance) { + fsi.positions.nodelist_distance = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen; + indicatorIcons.push_back(icon_distance); + } #endif #if HAS_GPS - if (!hiddenFrames.nodelist_bearings) { - fsi.positions.nodelist_bearings = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses; - indicatorIcons.push_back(icon_list); - } - if (!hiddenFrames.gps) { - fsi.positions.gps = numframes; - normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen; - indicatorIcons.push_back(icon_compass); - } + if (!hiddenFrames.nodelist_bearings) { + fsi.positions.nodelist_bearings = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses; + indicatorIcons.push_back(icon_list); + } + if (!hiddenFrames.gps) { + fsi.positions.gps = numframes; + normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen; + indicatorIcons.push_back(icon_compass); + } #endif - if (RadioLibInterface::instance && !hiddenFrames.lora) { - fsi.positions.lora = numframes; - normalFrames[numframes++] = graphics::DebugRenderer::drawLoRaFocused; - indicatorIcons.push_back(icon_radio); - } - if (!hiddenFrames.system) { - fsi.positions.system = numframes; - normalFrames[numframes++] = graphics::DebugRenderer::drawSystemScreen; - indicatorIcons.push_back(icon_system); - } + if (RadioLibInterface::instance && !hiddenFrames.lora) { + fsi.positions.lora = numframes; + normalFrames[numframes++] = graphics::DebugRenderer::drawLoRaFocused; + indicatorIcons.push_back(icon_radio); + } + if (!hiddenFrames.system) { + fsi.positions.system = numframes; + normalFrames[numframes++] = graphics::DebugRenderer::drawSystemScreen; + indicatorIcons.push_back(icon_system); + } #if !defined(DISPLAY_CLOCK_FRAME) - if (!hiddenFrames.clock) { - fsi.positions.clock = numframes; - normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame - : graphics::ClockRenderer::drawDigitalClockFrame; - indicatorIcons.push_back(digital_icon_clock); - } + if (!hiddenFrames.clock) { + fsi.positions.clock = numframes; + normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame + : graphics::ClockRenderer::drawDigitalClockFrame; + indicatorIcons.push_back(digital_icon_clock); + } #endif - if (!hiddenFrames.chirpy) { - fsi.positions.chirpy = numframes; - normalFrames[numframes++] = graphics::DebugRenderer::drawChirpy; - indicatorIcons.push_back(small_chirpy); - } + if (!hiddenFrames.chirpy) { + fsi.positions.chirpy = numframes; + normalFrames[numframes++] = graphics::DebugRenderer::drawChirpy; + indicatorIcons.push_back(small_chirpy); + } #if HAS_WIFI && !defined(ARCH_PORTDUINO) - if (!hiddenFrames.wifi && isWifiAvailable()) { - fsi.positions.wifi = numframes; - normalFrames[numframes++] = graphics::DebugRenderer::drawDebugInfoWiFiTrampoline; - indicatorIcons.push_back(icon_wifi); - } + if (!hiddenFrames.wifi && isWifiAvailable()) { + fsi.positions.wifi = numframes; + normalFrames[numframes++] = graphics::DebugRenderer::drawDebugInfoWiFiTrampoline; + indicatorIcons.push_back(icon_wifi); + } #endif - // Beware of what changes you make in this code! - // We pass numframes 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()); + // Beware of what changes you make in this code! + // We pass numframes 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; + 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; + // 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++; - } + indicatorIcons.push_back(icon_module); + numframes++; + } + } + + LOG_DEBUG("Added modules. numframes: %d", numframes); + + // We don't show the node info of our node (if we have it yet - we should) + size_t numMeshNodes = nodeDB->getNumMeshNodes(); + if (numMeshNodes > 0) + numMeshNodes--; + + if (!hiddenFrames.show_favorites) { + // Temporary array to hold favorite node frames + std::vector favoriteFrames; + + for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { + const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); + if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) { + favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo); } - - LOG_DEBUG("Added modules. numframes: %d", numframes); - - // We don't show the node info of our node (if we have it yet - we should) - size_t numMeshNodes = nodeDB->getNumMeshNodes(); - if (numMeshNodes > 0) - numMeshNodes--; - - if (!hiddenFrames.show_favorites) { - // Temporary array to hold favorite node frames - std::vector favoriteFrames; - - for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { - const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); - if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) { - favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo); - } - } - - // Insert favorite frames *after* collecting them all - if (!favoriteFrames.empty()) { - fsi.positions.firstFavorite = numframes; - for (const auto &f : favoriteFrames) { - normalFrames[numframes++] = f; - indicatorIcons.push_back(icon_node); - } - fsi.positions.lastFavorite = numframes - 1; - } else { - fsi.positions.firstFavorite = 255; - fsi.positions.lastFavorite = 255; - } - } - - 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); - - ui->setFrames(normalFrames, numframes); - ui->disableAllIndicators(); - - // Add overlays: frame icons and alert banner) - static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, - NotificationRenderer::drawBannercallback}; - ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); - - prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list just changed) - - // Focus on a specific frame, in the frame set we just created - switch (focus) { - case FOCUS_DEFAULT: - ui->switchToFrame(fsi.positions.deviceFocused); - break; - case FOCUS_FAULT: - ui->switchToFrame(fsi.positions.fault); - break; - case FOCUS_TEXTMESSAGE: - hasUnreadMessage = false; // ✅ Clear when message is *viewed* - ui->switchToFrame(fsi.positions.textMessage); - break; - case FOCUS_MODULE: - // Whichever frame was marked by MeshModule::requestFocus(), if any - // 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_SYSTEM: - ui->switchToFrame(fsi.positions.system); - break; - - case FOCUS_PRESERVE: - // No more adjustment — force stay on same index - if (previousFrameCount > fsi.frameCount) { - ui->switchToFrame(originalPosition - 1); - } else if (previousFrameCount < fsi.frameCount) { - ui->switchToFrame(originalPosition + 1); - } else { - ui->switchToFrame(originalPosition); - } - break; - } - - // Store the info about this frameset, for future setFrames calls - this->framesetInfo = fsi; - - setFastFramerate(); // Draw ASAP } - void Screen::setFrameImmediateDraw(FrameCallback * drawFrames) - { - ui->disableAllIndicators(); - ui->setFrames(drawFrames, 1); - setFastFramerate(); + // Insert favorite frames *after* collecting them all + if (!favoriteFrames.empty()) { + fsi.positions.firstFavorite = numframes; + for (const auto &f : favoriteFrames) { + normalFrames[numframes++] = f; + indicatorIcons.push_back(icon_node); + } + fsi.positions.lastFavorite = numframes - 1; + } else { + fsi.positions.firstFavorite = 255; + fsi.positions.lastFavorite = 255; } + } - void Screen::toggleFrameVisibility(const std::string &frameName) - { + 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); + + ui->setFrames(normalFrames, numframes); + ui->disableAllIndicators(); + + // Add overlays: frame icons and alert banner) + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); + + prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list just changed) + + // Focus on a specific frame, in the frame set we just created + switch (focus) { + case FOCUS_DEFAULT: + ui->switchToFrame(fsi.positions.deviceFocused); + break; + case FOCUS_FAULT: + ui->switchToFrame(fsi.positions.fault); + break; + case FOCUS_TEXTMESSAGE: + hasUnreadMessage = false; // ✅ Clear when message is *viewed* + ui->switchToFrame(fsi.positions.textMessage); + break; + case FOCUS_MODULE: + // Whichever frame was marked by MeshModule::requestFocus(), if any + // 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_SYSTEM: + ui->switchToFrame(fsi.positions.system); + break; + + case FOCUS_PRESERVE: + // No more adjustment — force stay on same index + if (previousFrameCount > fsi.frameCount) { + ui->switchToFrame(originalPosition - 1); + } else if (previousFrameCount < fsi.frameCount) { + ui->switchToFrame(originalPosition + 1); + } else { + ui->switchToFrame(originalPosition); + } + break; + } + + // Store the info about this frameset, for future setFrames calls + this->framesetInfo = fsi; + + setFastFramerate(); // Draw ASAP +} + +void Screen::setFrameImmediateDraw(FrameCallback *drawFrames) +{ + ui->disableAllIndicators(); + ui->setFrames(drawFrames, 1); + setFastFramerate(); +} + +void Screen::toggleFrameVisibility(const std::string &frameName) +{ #ifndef USE_EINK - if (frameName == "nodelist") { - hiddenFrames.nodelist = !hiddenFrames.nodelist; - } + if (frameName == "nodelist") { + hiddenFrames.nodelist = !hiddenFrames.nodelist; + } #endif #ifdef USE_EINK - if (frameName == "nodelist_lastheard") { - hiddenFrames.nodelist_lastheard = !hiddenFrames.nodelist_lastheard; - } - if (frameName == "nodelist_hopsignal") { - hiddenFrames.nodelist_hopsignal = !hiddenFrames.nodelist_hopsignal; - } - if (frameName == "nodelist_distance") { - hiddenFrames.nodelist_distance = !hiddenFrames.nodelist_distance; - } + if (frameName == "nodelist_lastheard") { + hiddenFrames.nodelist_lastheard = !hiddenFrames.nodelist_lastheard; + } + if (frameName == "nodelist_hopsignal") { + hiddenFrames.nodelist_hopsignal = !hiddenFrames.nodelist_hopsignal; + } + if (frameName == "nodelist_distance") { + hiddenFrames.nodelist_distance = !hiddenFrames.nodelist_distance; + } #endif #if HAS_GPS - if (frameName == "nodelist_bearings") { - hiddenFrames.nodelist_bearings = !hiddenFrames.nodelist_bearings; - } - if (frameName == "gps") { - hiddenFrames.gps = !hiddenFrames.gps; - } + if (frameName == "nodelist_bearings") { + hiddenFrames.nodelist_bearings = !hiddenFrames.nodelist_bearings; + } + if (frameName == "gps") { + hiddenFrames.gps = !hiddenFrames.gps; + } #endif - if (frameName == "lora") { - hiddenFrames.lora = !hiddenFrames.lora; - } - if (frameName == "clock") { - hiddenFrames.clock = !hiddenFrames.clock; - } - if (frameName == "show_favorites") { - hiddenFrames.show_favorites = !hiddenFrames.show_favorites; - } - if (frameName == "chirpy") { - hiddenFrames.chirpy = !hiddenFrames.chirpy; - } - } + if (frameName == "lora") { + hiddenFrames.lora = !hiddenFrames.lora; + } + if (frameName == "clock") { + hiddenFrames.clock = !hiddenFrames.clock; + } + if (frameName == "show_favorites") { + hiddenFrames.show_favorites = !hiddenFrames.show_favorites; + } + if (frameName == "chirpy") { + hiddenFrames.chirpy = !hiddenFrames.chirpy; + } +} - bool Screen::isFrameHidden(const std::string &frameName) const - { +bool Screen::isFrameHidden(const std::string &frameName) const +{ #ifndef USE_EINK - if (frameName == "nodelist") - return hiddenFrames.nodelist; + if (frameName == "nodelist") + return hiddenFrames.nodelist; #endif #ifdef USE_EINK - if (frameName == "nodelist_lastheard") - return hiddenFrames.nodelist_lastheard; - if (frameName == "nodelist_hopsignal") - return hiddenFrames.nodelist_hopsignal; - if (frameName == "nodelist_distance") - return hiddenFrames.nodelist_distance; + if (frameName == "nodelist_lastheard") + return hiddenFrames.nodelist_lastheard; + if (frameName == "nodelist_hopsignal") + return hiddenFrames.nodelist_hopsignal; + if (frameName == "nodelist_distance") + return hiddenFrames.nodelist_distance; #endif #if HAS_GPS - if (frameName == "nodelist_bearings") - return hiddenFrames.nodelist_bearings; - if (frameName == "gps") - return hiddenFrames.gps; + if (frameName == "nodelist_bearings") + return hiddenFrames.nodelist_bearings; + if (frameName == "gps") + return hiddenFrames.gps; #endif - if (frameName == "lora") - return hiddenFrames.lora; - if (frameName == "clock") - return hiddenFrames.clock; - if (frameName == "show_favorites") - return hiddenFrames.show_favorites; - if (frameName == "chirpy") - return hiddenFrames.chirpy; + if (frameName == "lora") + return hiddenFrames.lora; + if (frameName == "clock") + return hiddenFrames.clock; + if (frameName == "show_favorites") + return hiddenFrames.show_favorites; + if (frameName == "chirpy") + return hiddenFrames.chirpy; - return false; - } + return false; +} - // Dismisses the currently displayed screen frame, if possible - // Relevant for text message, waypoint, others in future? - // Triggered with a CardKB keycombo - void Screen::hideCurrentFrame() - { - uint8_t currentFrame = ui->getUiState()->currentFrame; - bool dismissed = false; - if (currentFrame == framesetInfo.positions.textMessage && devicestate.has_rx_text_message) { - LOG_INFO("Hide Text Message"); - devicestate.has_rx_text_message = false; - memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message)); - } else if (currentFrame == framesetInfo.positions.waypoint && devicestate.has_rx_waypoint) { - LOG_DEBUG("Hide Waypoint"); - devicestate.has_rx_waypoint = false; - hiddenFrames.waypoint = true; - dismissed = true; - } else if (currentFrame == framesetInfo.positions.wifi) { - LOG_DEBUG("Hide WiFi Screen"); - hiddenFrames.wifi = true; - dismissed = true; - } else if (currentFrame == framesetInfo.positions.lora) { - LOG_INFO("Hide LoRa"); - hiddenFrames.lora = true; - dismissed = true; - } +// Dismisses the currently displayed screen frame, if possible +// Relevant for text message, waypoint, others in future? +// Triggered with a CardKB keycombo +void Screen::hideCurrentFrame() +{ + uint8_t currentFrame = ui->getUiState()->currentFrame; + bool dismissed = false; + if (currentFrame == framesetInfo.positions.textMessage && devicestate.has_rx_text_message) { + LOG_INFO("Hide Text Message"); + devicestate.has_rx_text_message = false; + memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message)); + } else if (currentFrame == framesetInfo.positions.waypoint && devicestate.has_rx_waypoint) { + LOG_DEBUG("Hide Waypoint"); + devicestate.has_rx_waypoint = false; + hiddenFrames.waypoint = true; + dismissed = true; + } else if (currentFrame == framesetInfo.positions.wifi) { + LOG_DEBUG("Hide WiFi Screen"); + hiddenFrames.wifi = true; + dismissed = true; + } else if (currentFrame == framesetInfo.positions.lora) { + LOG_INFO("Hide LoRa"); + hiddenFrames.lora = true; + dismissed = true; + } - if (dismissed) { - setFrames(FOCUS_DEFAULT); // You could also use FOCUS_PRESERVE - } - } + if (dismissed) { + setFrames(FOCUS_DEFAULT); // You could also use FOCUS_PRESERVE + } +} - void Screen::handleStartFirmwareUpdateScreen() - { - LOG_DEBUG("Show firmware screen"); - showingNormalScreen = false; - EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // E-Ink: Explicitly use fast-refresh for next frame +void Screen::handleStartFirmwareUpdateScreen() +{ + LOG_DEBUG("Show firmware screen"); + showingNormalScreen = false; + EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // E-Ink: Explicitly use fast-refresh for next frame - static FrameCallback frames[] = {graphics::NotificationRenderer::drawFrameFirmware}; - setFrameImmediateDraw(frames); - } + static FrameCallback frames[] = {graphics::NotificationRenderer::drawFrameFirmware}; + setFrameImmediateDraw(frames); +} - void Screen::blink() - { - setFastFramerate(); - uint8_t count = 10; - dispdev->setBrightness(254); - while (count > 0) { - dispdev->fillRect(0, 0, dispdev->getWidth(), dispdev->getHeight()); - dispdev->display(); - delay(50); - dispdev->clear(); - dispdev->display(); - delay(50); - count = count - 1; - } - // The dispdev->setBrightness does not work for t-deck display, it seems to run the setBrightness function in - // OLEDDisplay. - dispdev->setBrightness(brightness); - } +void Screen::blink() +{ + setFastFramerate(); + uint8_t count = 10; + dispdev->setBrightness(254); + while (count > 0) { + dispdev->fillRect(0, 0, dispdev->getWidth(), dispdev->getHeight()); + dispdev->display(); + delay(50); + dispdev->clear(); + dispdev->display(); + delay(50); + count = count - 1; + } + // The dispdev->setBrightness does not work for t-deck display, it seems to run the setBrightness function in + // OLEDDisplay. + dispdev->setBrightness(brightness); +} - void Screen::increaseBrightness() - { - brightness = ((brightness + 62) > 254) ? brightness : (brightness + 62); +void Screen::increaseBrightness() +{ + brightness = ((brightness + 62) > 254) ? brightness : (brightness + 62); #if defined(ST7789_CS) - // run the setDisplayBrightness function. This works on t-decks - static_cast(dispdev)->setDisplayBrightness(brightness); + // run the setDisplayBrightness function. This works on t-decks + static_cast(dispdev)->setDisplayBrightness(brightness); #endif - /* TO DO: add little popup in center of screen saying what brightness level it is set to*/ - } + /* TO DO: add little popup in center of screen saying what brightness level it is set to*/ +} - void Screen::decreaseBrightness() - { - brightness = (brightness < 70) ? brightness : (brightness - 62); +void Screen::decreaseBrightness() +{ + brightness = (brightness < 70) ? brightness : (brightness - 62); #if defined(ST7789_CS) - static_cast(dispdev)->setDisplayBrightness(brightness); + static_cast(dispdev)->setDisplayBrightness(brightness); #endif - /* TO DO: add little popup in center of screen saying what brightness level it is set to*/ - } + /* TO DO: add little popup in center of screen saying what brightness level it is set to*/ +} - void Screen::setFunctionSymbol(std::string sym) - { - if (std::find(functionSymbol.begin(), functionSymbol.end(), sym) == functionSymbol.end()) { - functionSymbol.push_back(sym); - functionSymbolString = ""; - for (auto symbol : functionSymbol) { - functionSymbolString = symbol + " " + functionSymbolString; - } - setFastFramerate(); - } +void Screen::setFunctionSymbol(std::string sym) +{ + if (std::find(functionSymbol.begin(), functionSymbol.end(), sym) == functionSymbol.end()) { + functionSymbol.push_back(sym); + functionSymbolString = ""; + for (auto symbol : functionSymbol) { + functionSymbolString = symbol + " " + functionSymbolString; } + setFastFramerate(); + } +} - void Screen::removeFunctionSymbol(std::string sym) - { - functionSymbol.erase(std::remove(functionSymbol.begin(), functionSymbol.end(), sym), functionSymbol.end()); - functionSymbolString = ""; - for (auto symbol : functionSymbol) { - functionSymbolString = symbol + " " + functionSymbolString; - } - setFastFramerate(); - } +void Screen::removeFunctionSymbol(std::string sym) +{ + functionSymbol.erase(std::remove(functionSymbol.begin(), functionSymbol.end(), sym), functionSymbol.end()); + functionSymbolString = ""; + for (auto symbol : functionSymbol) { + functionSymbolString = symbol + " " + functionSymbolString; + } + setFastFramerate(); +} - void Screen::handleOnPress() - { - // If screen was off, just wake it, otherwise advance to next frame - // If we are in a transition, the press must have bounced, drop it. - if (ui->getUiState()->frameState == FIXED) { - ui->nextFrame(); - lastScreenTransition = millis(); - setFastFramerate(); - } - } +void Screen::handleOnPress() +{ + // If screen was off, just wake it, otherwise advance to next frame + // If we are in a transition, the press must have bounced, drop it. + if (ui->getUiState()->frameState == FIXED) { + ui->nextFrame(); + lastScreenTransition = millis(); + setFastFramerate(); + } +} - void Screen::handleShowPrevFrame() - { - // If screen was off, just wake it, otherwise go back to previous frame - // If we are in a transition, the press must have bounced, drop it. - if (ui->getUiState()->frameState == FIXED) { - ui->previousFrame(); - lastScreenTransition = millis(); - setFastFramerate(); - } - } +void Screen::handleShowPrevFrame() +{ + // If screen was off, just wake it, otherwise go back to previous frame + // If we are in a transition, the press must have bounced, drop it. + if (ui->getUiState()->frameState == FIXED) { + ui->previousFrame(); + lastScreenTransition = millis(); + setFastFramerate(); + } +} - void Screen::handleShowNextFrame() - { - // If screen was off, just wake it, otherwise advance to next frame - // If we are in a transition, the press must have bounced, drop it. - if (ui->getUiState()->frameState == FIXED) { - ui->nextFrame(); - lastScreenTransition = millis(); - setFastFramerate(); - } - } +void Screen::handleShowNextFrame() +{ + // If screen was off, just wake it, otherwise advance to next frame + // If we are in a transition, the press must have bounced, drop it. + if (ui->getUiState()->frameState == FIXED) { + ui->nextFrame(); + lastScreenTransition = millis(); + setFastFramerate(); + } +} #ifndef SCREEN_TRANSITION_FRAMERATE #define SCREEN_TRANSITION_FRAMERATE 30 // fps #endif - void Screen::setFastFramerate() - { +void Screen::setFastFramerate() +{ #if defined(M5STACK_UNITC6L) - dispdev->clear(); - dispdev->display(); + dispdev->clear(); + dispdev->display(); #endif - // We are about to start a transition so speed up fps - targetFramerate = SCREEN_TRANSITION_FRAMERATE; + // We are about to start a transition so speed up fps + targetFramerate = SCREEN_TRANSITION_FRAMERATE; - ui->setTargetFPS(targetFramerate); - setInterval(0); // redraw ASAP - runASAP = true; + ui->setTargetFPS(targetFramerate); + setInterval(0); // redraw ASAP + runASAP = true; +} + +int Screen::handleStatusUpdate(const meshtastic::Status *arg) +{ + // LOG_DEBUG("Screen got status update %d", arg->getStatusType()); + switch (arg->getStatusType()) { + case STATUS_TYPE_NODE: + if (showingNormalScreen && nodeStatus->getLastNumTotal() != nodeStatus->getNumTotal()) { + setFrames(FOCUS_PRESERVE); // Regen the list of screen frames (returning to same frame, if possible) } + nodeDB->updateGUI = false; + break; + } - int Screen::handleStatusUpdate(const meshtastic::Status *arg) - { - // LOG_DEBUG("Screen got status update %d", arg->getStatusType()); - switch (arg->getStatusType()) { - case STATUS_TYPE_NODE: - if (showingNormalScreen && nodeStatus->getLastNumTotal() != nodeStatus->getNumTotal()) { - setFrames(FOCUS_PRESERVE); // Regen the list of screen frames (returning to same frame, if possible) + return 0; +} + +// Handles when message is received; will jump to text message frame. +int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) +{ + if (showingNormalScreen) { + if (packet->from == 0) { + // Outgoing message (likely sent from phone) + devicestate.has_rx_text_message = false; + memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message)); + hiddenFrames.textMessage = true; + hasUnreadMessage = false; // Clear unread state when user replies + + setFrames(FOCUS_PRESERVE); // Stay on same frame, silently update frame list + } else { + // Incoming message + devicestate.has_rx_text_message = true; // Needed to include the message frame + hasUnreadMessage = true; // Enables mail icon in the header + setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view + + // Only wake/force display if the configuration allows it + if (shouldWakeOnReceivedMessage()) { + setOn(true); // Wake up the screen first + forceDisplay(); // Forces screen redraw + } + // === Prepare banner content === + const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from); + const char *longName = (node && node->has_user) ? node->user.long_name : nullptr; + + const char *msgRaw = reinterpret_cast(packet->decoded.payload.bytes); + + char banner[256]; + + // Check for bell character in message to determine alert type + bool isAlert = false; + for (size_t i = 0; i < packet->decoded.payload.size && i < 100; i++) { + if (msgRaw[i] == '\x07') { + isAlert = true; + break; } - nodeDB->updateGUI = false; - break; } - return 0; - } - - // Handles when message is received; will jump to text message frame. - int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) - { - if (showingNormalScreen) { - if (packet->from == 0) { - // Outgoing message (likely sent from phone) - devicestate.has_rx_text_message = false; - memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message)); - hiddenFrames.textMessage = true; - hasUnreadMessage = false; // Clear unread state when user replies - - setFrames(FOCUS_PRESERVE); // Stay on same frame, silently update frame list + if (isAlert) { + if (longName && longName[0]) { + snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName); } else { - // Incoming message - devicestate.has_rx_text_message = true; // Needed to include the message frame - hasUnreadMessage = true; // Enables mail icon in the header - setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view - - // Only wake/force display if the configuration allows it - if (shouldWakeOnReceivedMessage()) { - setOn(true); // Wake up the screen first - forceDisplay(); // Forces screen redraw - } - // === Prepare banner content === - const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from); - const char *longName = (node && node->has_user) ? node->user.long_name : nullptr; - - const char *msgRaw = reinterpret_cast(packet->decoded.payload.bytes); - - char banner[256]; - - // Check for bell character in message to determine alert type - bool isAlert = false; - for (size_t i = 0; i < packet->decoded.payload.size && i < 100; i++) { - if (msgRaw[i] == '\x07') { - isAlert = true; - break; - } - } - - if (isAlert) { - if (longName && longName[0]) { - snprintf(banner, sizeof(banner), "Alert Received from\n%s", longName); - } else { - strcpy(banner, "Alert Received"); - } - } else { - if (longName && longName[0]) { + strcpy(banner, "Alert Received"); + } + } else { + if (longName && longName[0]) { #if defined(M5STACK_UNITC6L) - strcpy(banner, "New Message"); + strcpy(banner, "New Message"); #else snprintf(banner, sizeof(banner), "New Message from\n%s", longName); #endif - } else { - strcpy(banner, "New Message"); - } - } + } else { + strcpy(banner, "New Message"); + } + } #if defined(M5STACK_UNITC6L) - screen->setOn(true); - screen->showSimpleBanner(banner, 1500); - playLongBeep(); + screen->setOn(true); + screen->showSimpleBanner(banner, 1500); + playLongBeep(); #else screen->showSimpleBanner(banner, 3000); #endif - } - } - - return 0; } + } - // Triggered by MeshModules - int Screen::handleUIFrameEvent(const UIFrameEvent *event) - { - // Block UI frame events when virtual keyboard is active - if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { - return 0; - } + return 0; +} - if (showingNormalScreen) { - // Regenerate the frameset, potentially honoring a module's internal requestFocus() call - if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET) - setFrames(FOCUS_MODULE); +// Triggered by MeshModules +int Screen::handleUIFrameEvent(const UIFrameEvent *event) +{ + // Block UI frame events when virtual keyboard is active + if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { + return 0; + } - // Regenerate the frameset, while Attempt to maintain focus on the current frame - else if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET_BACKGROUND) - setFrames(FOCUS_PRESERVE); + if (showingNormalScreen) { + // Regenerate the frameset, potentially honoring a module's internal requestFocus() call + if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET) + setFrames(FOCUS_MODULE); - // Don't regenerate the frameset, just re-draw whatever is on screen ASAP - else if (event->action == UIFrameEvent::Action::REDRAW_ONLY) - setFastFramerate(); - } + // Regenerate the frameset, while Attempt to maintain focus on the current frame + else if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET_BACKGROUND) + setFrames(FOCUS_PRESERVE); - return 0; - } + // Don't regenerate the frameset, just re-draw whatever is on screen ASAP + else if (event->action == UIFrameEvent::Action::REDRAW_ONLY) + setFastFramerate(); + } - int Screen::handleInputEvent(const InputEvent *event) - { - if (!screenOn) - return 0; + return 0; +} - // Handle text input notifications specially - pass input to virtual keyboard - if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { - NotificationRenderer::inEvent = *event; - static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, - NotificationRenderer::drawBannercallback}; - ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); - setFastFramerate(); // Draw ASAP - ui->update(); - return 0; - } +int Screen::handleInputEvent(const InputEvent *event) +{ + if (!screenOn) + return 0; + + // Handle text input notifications specially - pass input to virtual keyboard + if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { + NotificationRenderer::inEvent = *event; + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); + setFastFramerate(); // Draw ASAP + ui->update(); + return 0; + } #ifdef USE_EINK // the screen is the last input handler, so if an event makes it here, we can assume it will prompt a screen draw. - EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please - EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update - handleSetOn(true); // Ensure power-on to receive deep-sleep screensaver (PowerFSM should handle?) - setFastFramerate(); // Draw ASAP + EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please + EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update + handleSetOn(true); // Ensure power-on to receive deep-sleep screensaver (PowerFSM should handle?) + setFastFramerate(); // Draw ASAP #endif - if (NotificationRenderer::isOverlayBannerShowing()) { - NotificationRenderer::inEvent = *event; - static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, - NotificationRenderer::drawBannercallback}; - ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); - setFastFramerate(); // Draw ASAP - ui->update(); + if (NotificationRenderer::isOverlayBannerShowing()) { + NotificationRenderer::inEvent = *event; + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); + setFastFramerate(); // Draw ASAP + ui->update(); - menuHandler::handleMenuSwitch(dispdev); - return 0; - } + menuHandler::handleMenuSwitch(dispdev); + return 0; + } - // Use left or right input from a keyboard to move between frames, - // so long as a mesh module isn't using these events for some other purpose - if (showingNormalScreen) { + // Use left or right input from a keyboard to move between frames, + // so long as a mesh module isn't using these events for some other purpose + if (showingNormalScreen) { - // Ask any MeshModules if they're handling keyboard input right now - bool inputIntercepted = false; - for (MeshModule *module : moduleFrames) { - if (module && module->interceptingKeyboardInput()) - inputIntercepted = true; - } + // Ask any MeshModules if they're handling keyboard input right now + bool inputIntercepted = false; + for (MeshModule *module : moduleFrames) { + if (module && module->interceptingKeyboardInput()) + inputIntercepted = true; + } - // If no modules are using the input, move between frames - if (!inputIntercepted) { - if (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_ALT_PRESS) { - showPrevFrame(); - } else if (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS) { - showNextFrame(); - } else if (event->inputEvent == INPUT_BROKER_SELECT) { - if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) { - menuHandler::homeBaseMenu(); - } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.system) { - menuHandler::systemBaseMenu(); + // If no modules are using the input, move between frames + if (!inputIntercepted) { + if (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_ALT_PRESS) { + showPrevFrame(); + } else if (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS) { + showNextFrame(); + } else if (event->inputEvent == INPUT_BROKER_SELECT) { + if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) { + menuHandler::homeBaseMenu(); + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.system) { + menuHandler::systemBaseMenu(); #if HAS_GPS - } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.gps && gps) { - menuHandler::positionBaseMenu(); + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.gps && gps) { + menuHandler::positionBaseMenu(); #endif - } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.clock) { - menuHandler::clockMenu(); - } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) { - menuHandler::loraMenu(); - } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) { - if (devicestate.rx_text_message.from) { - menuHandler::messageResponseMenu(); - } else { + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.clock) { + menuHandler::clockMenu(); + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) { + menuHandler::loraMenu(); + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) { + if (devicestate.rx_text_message.from) { + menuHandler::messageResponseMenu(); + } else { #if defined(M5STACK_UNITC6L) - menuHandler::textMessageMenu(); + menuHandler::textMessageMenu(); #else menuHandler::textMessageBaseMenu(); #endif - } - } else if (framesetInfo.positions.firstFavorite != 255 && - this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite && - this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) { - 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 (this->ui->getUiState()->currentFrame == framesetInfo.positions.wifi) { - menuHandler::wifiBaseMenu(); - } - } else if (event->inputEvent == INPUT_BROKER_BACK) { - showPrevFrame(); - } else if (event->inputEvent == INPUT_BROKER_CANCEL) { - setOn(false); } + } else if (framesetInfo.positions.firstFavorite != 255 && + this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite && + this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) { + 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 (this->ui->getUiState()->currentFrame == framesetInfo.positions.wifi) { + menuHandler::wifiBaseMenu(); } + } else if (event->inputEvent == INPUT_BROKER_BACK) { + showPrevFrame(); + } else if (event->inputEvent == INPUT_BROKER_CANCEL) { + setOn(false); } - - return 0; } + } - int Screen::handleAdminMessage(AdminModule_ObserverData * arg) - { - 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; + return 0; +} - // Default no-op, in case the admin message observable gets used by other classes in future - default: - break; - } - return 0; - } +int Screen::handleAdminMessage(AdminModule_ObserverData *arg) +{ + 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; - bool Screen::isOverlayBannerShowing() - { - return NotificationRenderer::isOverlayBannerShowing(); - } + // Default no-op, in case the admin message observable gets used by other classes in future + default: + break; + } + return 0; +} - } // namespace graphics +bool Screen::isOverlayBannerShowing() +{ + return NotificationRenderer::isOverlayBannerShowing(); +} + +} // namespace graphics #else graphics::Screen::Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY) {} #endif // HAS_SCREEN - bool shouldWakeOnReceivedMessage() - { - /* - The goal here is to determine when we do NOT wake up the screen on message received: - - Any ext. notifications are turned on - - If role is not CLIENT / CLIENT_MUTE / CLIENT_HIDDEN / CLIENT_BASE - - If the battery level is very low - */ - if (moduleConfig.external_notification.enabled) { - return false; - } - if (!IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_CLIENT, - meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE, meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN, - meshtastic_Config_DeviceConfig_Role_CLIENT_BASE)) { - return false; - } - if (powerStatus && powerStatus->getBatteryChargePercent() < 10) { - return false; - } - return true; +bool shouldWakeOnReceivedMessage() +{ + /* + The goal here is to determine when we do NOT wake up the screen on message received: + - Any ext. notifications are turned on + - If role is not CLIENT / CLIENT_MUTE / CLIENT_HIDDEN / CLIENT_BASE + - If the battery level is very low + */ + if (moduleConfig.external_notification.enabled) { + return false; } + if (!IS_ONE_OF(config.device.role, meshtastic_Config_DeviceConfig_Role_CLIENT, + meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE, meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN, + meshtastic_Config_DeviceConfig_Role_CLIENT_BASE)) { + return false; + } + if (powerStatus && powerStatus->getBatteryChargePercent() < 10) { + return false; + } + return true; +} From 6a3b2ceafe020e384e45463c99156a433bdc1686 Mon Sep 17 00:00:00 2001 From: Markus <974709+Links2004@users.noreply.github.com> Date: Sat, 20 Sep 2025 19:15:41 +0200 Subject: [PATCH 06/14] move HTTP contentTypes to Flash - saves 768 Bytes of RAM (#8055) --- src/mesh/http/ContentHandler.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/mesh/http/ContentHandler.cpp b/src/mesh/http/ContentHandler.cpp index fb66dae7c..f87c6e3b0 100644 --- a/src/mesh/http/ContentHandler.cpp +++ b/src/mesh/http/ContentHandler.cpp @@ -55,12 +55,12 @@ HTTPClient httpClient; // We need to specify some content-type mapping, so the resources get delivered with the // right content type and are displayed correctly in the browser -char contentTypes[][2][32] = {{".txt", "text/plain"}, {".html", "text/html"}, - {".js", "text/javascript"}, {".png", "image/png"}, - {".jpg", "image/jpg"}, {".gz", "application/gzip"}, - {".gif", "image/gif"}, {".json", "application/json"}, - {".css", "text/css"}, {".ico", "image/vnd.microsoft.icon"}, - {".svg", "image/svg+xml"}, {"", ""}}; +char const *contentTypes[][2] = {{".txt", "text/plain"}, {".html", "text/html"}, + {".js", "text/javascript"}, {".png", "image/png"}, + {".jpg", "image/jpg"}, {".gz", "application/gzip"}, + {".gif", "image/gif"}, {".json", "application/json"}, + {".css", "text/css"}, {".ico", "image/vnd.microsoft.icon"}, + {".svg", "image/svg+xml"}, {"", ""}}; // const char *certificate = NULL; // change this as needed, leave as is for no TLS check (yolo security) From 9b6cf53730e7d04da4689c82f874bc752570a8f8 Mon Sep 17 00:00:00 2001 From: Markus <974709+Links2004@users.noreply.github.com> Date: Sat, 20 Sep 2025 19:15:41 +0200 Subject: [PATCH 07/14] move HTTP contentTypes to Flash - saves 768 Bytes of RAM (#8055) --- src/mesh/http/ContentHandler.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/mesh/http/ContentHandler.cpp b/src/mesh/http/ContentHandler.cpp index fb66dae7c..f87c6e3b0 100644 --- a/src/mesh/http/ContentHandler.cpp +++ b/src/mesh/http/ContentHandler.cpp @@ -55,12 +55,12 @@ HTTPClient httpClient; // We need to specify some content-type mapping, so the resources get delivered with the // right content type and are displayed correctly in the browser -char contentTypes[][2][32] = {{".txt", "text/plain"}, {".html", "text/html"}, - {".js", "text/javascript"}, {".png", "image/png"}, - {".jpg", "image/jpg"}, {".gz", "application/gzip"}, - {".gif", "image/gif"}, {".json", "application/json"}, - {".css", "text/css"}, {".ico", "image/vnd.microsoft.icon"}, - {".svg", "image/svg+xml"}, {"", ""}}; +char const *contentTypes[][2] = {{".txt", "text/plain"}, {".html", "text/html"}, + {".js", "text/javascript"}, {".png", "image/png"}, + {".jpg", "image/jpg"}, {".gz", "application/gzip"}, + {".gif", "image/gif"}, {".json", "application/json"}, + {".css", "text/css"}, {".ico", "image/vnd.microsoft.icon"}, + {".svg", "image/svg+xml"}, {"", ""}}; // const char *certificate = NULL; // change this as needed, leave as is for no TLS check (yolo security) From 52527e281dbd1b15064aabf8383bc254fc8a88da Mon Sep 17 00:00:00 2001 From: GUVWAF <78759985+GUVWAF@users.noreply.github.com> Date: Sat, 20 Sep 2025 19:17:14 +0200 Subject: [PATCH 08/14] Use `lora.use_preset` config to get name (#8057) --- src/mesh/Channels.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mesh/Channels.cpp b/src/mesh/Channels.cpp index aec112a3e..4dcd94e3b 100644 --- a/src/mesh/Channels.cpp +++ b/src/mesh/Channels.cpp @@ -428,8 +428,8 @@ bool Channels::setDefaultPresetCryptoForHash(ChannelHash channelHash) // Iterate all known presets for (int preset = _meshtastic_Config_LoRaConfig_ModemPreset_MIN; preset <= _meshtastic_Config_LoRaConfig_ModemPreset_MAX; ++preset) { - const char *name = - DisplayFormatters::getModemPresetDisplayName((meshtastic_Config_LoRaConfig_ModemPreset)preset, false, false); + const char *name = DisplayFormatters::getModemPresetDisplayName((meshtastic_Config_LoRaConfig_ModemPreset)preset, false, + config.lora.use_preset); if (!name) continue; if (strcmp(name, "Invalid") == 0) From 34c2191f6323b0518ceeec9e53933ca0de57a8c0 Mon Sep 17 00:00:00 2001 From: GUVWAF <78759985+GUVWAF@users.noreply.github.com> Date: Sat, 20 Sep 2025 19:17:14 +0200 Subject: [PATCH 09/14] Use `lora.use_preset` config to get name (#8057) --- src/mesh/Channels.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mesh/Channels.cpp b/src/mesh/Channels.cpp index aec112a3e..4dcd94e3b 100644 --- a/src/mesh/Channels.cpp +++ b/src/mesh/Channels.cpp @@ -428,8 +428,8 @@ bool Channels::setDefaultPresetCryptoForHash(ChannelHash channelHash) // Iterate all known presets for (int preset = _meshtastic_Config_LoRaConfig_ModemPreset_MIN; preset <= _meshtastic_Config_LoRaConfig_ModemPreset_MAX; ++preset) { - const char *name = - DisplayFormatters::getModemPresetDisplayName((meshtastic_Config_LoRaConfig_ModemPreset)preset, false, false); + const char *name = DisplayFormatters::getModemPresetDisplayName((meshtastic_Config_LoRaConfig_ModemPreset)preset, false, + config.lora.use_preset); if (!name) continue; if (strcmp(name, "Invalid") == 0) From 040b3b8c7f9fb87e621fdcdd2cb7a931501fca37 Mon Sep 17 00:00:00 2001 From: Jason P Date: Sat, 20 Sep 2025 20:33:47 -0500 Subject: [PATCH 10/14] Resolve many warnings for BaseUI during builds (#8063) * Resolve many warnings for BaseUI during builds * Don't display "No GPS Lock" twice --- src/graphics/Screen.cpp | 2 +- src/graphics/draw/UIRenderer.cpp | 18 ++++--- src/graphics/images.h | 6 +-- src/graphics/img/icon_small.xbm | 88 +------------------------------- 4 files changed, 15 insertions(+), 99 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index a440ecab9..204fbd451 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1037,7 +1037,7 @@ void Screen::setFrames(FrameFocus focus) if (!hiddenFrames.chirpy) { fsi.positions.chirpy = numframes; normalFrames[numframes++] = graphics::DebugRenderer::drawChirpy; - indicatorIcons.push_back(small_chirpy); + indicatorIcons.push_back(chirpy_small); } #if HAS_WIFI && !defined(ARCH_PORTDUINO) diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 202a28835..988e7eb53 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -20,7 +20,9 @@ // External variables extern graphics::Screen *screen; +#if defined(M5STACK_UNITC6L) static uint32_t lastSwitchTime = 0; +#endif namespace graphics { NodeNum UIRenderer::currentFavoriteNodeNum = 0; @@ -126,8 +128,10 @@ void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, strcpy(displayLine, "No GPS present"); display->drawString(x, y, displayLine); } else if (!gps->getHasLock() && !config.position.fixed_position) { - strcpy(displayLine, "No GPS Lock"); - display->drawString(x, y, displayLine); + if (strcmp(mode, "line1") == 0) { + strcpy(displayLine, "No GPS Lock"); + display->drawString(x, y, displayLine); + } } else { geoCoord.updateCoords(int32_t(gps->getLatitude()), int32_t(gps->getLongitude()), int32_t(gps->getAltitude())); @@ -285,9 +289,9 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st meshtastic_NodeInfoLite *node = favoritedNodes[nodeIndex]; if (!node || node->num == nodeDB->getNodeNum() || !node->is_favorite) return; - uint32_t now = millis(); display->clear(); #if defined(M5STACK_UNITC6L) + uint32_t now = millis(); if (now - lastSwitchTime >= 10000) // 10000 ms = 10 秒 { display->display(); @@ -732,7 +736,6 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta int textWidth = 0; int nameX = 0; int yOffset = (isHighResolution) ? 0 : 5; - const char *longName = nullptr; std::string longNameStr; meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); @@ -1277,14 +1280,13 @@ 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 - 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) + // Only show bar briefly after switching frames + static uint32_t navBarLastShown = 0; + static bool cosmeticRefreshDone = false; static bool navBarPrevVisible = false; if (navBarVisible && !navBarPrevVisible) { diff --git a/src/graphics/images.h b/src/graphics/images.h index 72dda7886..7b80fc5ab 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -292,7 +292,7 @@ const uint8_t analog_icon_clock[] PROGMEM = {0b11111111, 0b01000010, 0b00100100, #else #define chirpy_width 38 #define chirpy_height 50 -static unsigned char chirpy[] = { +const uint8_t chirpy[] = { 0xfe, 0xff, 0xff, 0xff, 0xdf, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x80, 0xe3, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0x80, 0xe3, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x81, 0xff, 0xff, 0x7f, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xcf, 0x7f, @@ -308,7 +308,7 @@ static unsigned char chirpy[] = { #define chirpy_width_hirez 76 #define chirpy_height_hirez 100 -static unsigned char chirpy_hirez[] = { +const uint8_t chirpy_hirez[] = { 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, @@ -360,7 +360,7 @@ static unsigned char chirpy_hirez[] = { #define chirpy_small_image_width 8 #define chirpy_small_image_height 8 -static unsigned char small_chirpy[] = {0x7f, 0x41, 0x55, 0x55, 0x55, 0x55, 0x41, 0x7f}; +const uint8_t chirpy_small[] = {0x7f, 0x41, 0x55, 0x55, 0x55, 0x55, 0x41, 0x7f}; #include "img/icon.xbm" #endif diff --git a/src/graphics/img/icon_small.xbm b/src/graphics/img/icon_small.xbm index 97884edad..e320a1fea 100644 --- a/src/graphics/img/icon_small.xbm +++ b/src/graphics/img/icon_small.xbm @@ -27,90 +27,4 @@ static uint8_t icon_bits[] = { 0x00, 0x1c, 0x00, 0x00, 0x70, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; -#endif - -// Chirpy image definitions for M5STACK_UNITC6L compatibility -#define chirpy_width 38 -#define chirpy_height 50 -static unsigned char chirpy[] = { - 0xfe, 0xff, 0xff, 0xff, 0xdf, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x80, 0xe3, 0x01, - 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0x80, 0xe3, 0x01, 0x00, - 0x00, 0x00, 0xe0, 0x81, 0xff, 0xff, 0x7f, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xcf, 0x7f, - 0xfe, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, - 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, - 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, - 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0xcf, 0x7f, 0xfe, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0x81, 0xff, - 0xff, 0x7f, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0xc3, 0x00, 0xe0, 0x01, 0x00, 0xc3, - 0x00, 0xe0, 0x01, 0x80, 0xe1, 0x01, 0xe0, 0x01, 0x80, 0xe1, 0x01, 0xe0, 0x01, 0xc0, 0x30, 0x03, 0xe0, 0x01, 0xc0, 0x30, 0x03, - 0xe0, 0x01, 0x60, 0x18, 0x06, 0xe0, 0x01, 0x60, 0x18, 0x06, 0xe0, 0x01, 0x30, 0x0c, 0x0c, 0xe0, 0x01, 0x30, 0x0c, 0x0c, 0xe0, - 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01, - 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0xfe, 0xff, 0xff, 0xff, 0xdf}; - -#define chirpy_width_hirez 76 -#define chirpy_height_hirez 100 -static unsigned char chirpy_hirez[] = { - 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0x03, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, - 0xfc, 0x03, 0xe0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0x00, 0xfc, - 0x03, 0xe0, 0x1f, 0xff, 0xff, 0xff, 0xf8, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, - 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, - 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, - 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, - 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, - 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, - 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, - 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, - 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, - 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, - 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, - 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, - 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, - 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, - 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, - 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, - 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, - 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, - 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, - 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, - 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, - 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, - 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x0f, 0xfe, 0xff, 0x7f, 0xf0, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0x1f, - 0xff, 0xff, 0xff, 0xf8, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0x00, 0xfc, 0x03, 0xe0, 0xff, 0xff, - 0xff, 0xff, 0xff, 0x7f, 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x86, 0x61, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x86, 0x61, 0x00, - 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x86, 0x61, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x80, 0x87, 0xe1, 0x01, 0x00, - 0x00, 0xfc, 0x03, 0x00, 0x00, 0x80, 0x87, 0xe1, 0x01, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0, 0x83, 0xc1, 0x03, 0x00, 0x00, - 0xfc, 0x03, 0x00, 0x00, 0xc0, 0x83, 0xc1, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xe0, 0x01, 0x80, 0x07, 0x00, 0x00, 0xfc, - 0x03, 0x00, 0x00, 0xe0, 0x01, 0x80, 0x07, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x00, 0x0f, 0x00, 0x00, 0xfc, 0x03, - 0x00, 0x00, 0xf0, 0x00, 0x00, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x78, 0x00, 0x00, 0x1e, 0x00, 0x00, 0xfc, 0x03, 0x00, - 0x00, 0x78, 0x00, 0x00, 0x1e, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x3c, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, - 0x3c, 0x00, 0x00, 0x3c, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x1e, 0x00, 0x00, 0x78, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x1e, - 0x00, 0x00, 0x78, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0x00, 0x00, 0xf0, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0x00, - 0x00, 0xf0, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x80, 0x07, 0x00, 0x00, 0xe0, 0x01, 0x00, 0xfc, 0x03, 0x00, 0x80, 0x07, 0x00, 0x00, - 0xe0, 0x01, 0x00, 0xfc, 0x03, 0x00, 0xc0, 0x03, 0x00, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00, 0xc0, 0x03, 0x00, 0x00, 0xc0, - 0x03, 0x00, 0xfc, 0x03, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x80, 0x07, 0x00, 0xfc, 0x03, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x80, 0x07, - 0x00, 0xfc, 0x03, 0x00, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, - 0xfc, 0x03, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x1e, 0x00, 0xfc, 0x03, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x1e, 0x00, 0xfc, - 0x03, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x3c, 0x00, 0xfc, 0x03, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x00, 0x3c, 0x00, 0xfc, 0x03, - 0x00, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0xfc, 0x03, 0x00, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0xfc, 0x03, 0x00, - 0x0f, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x00, 0xfc, 0x03, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x00, 0xfc, 0x03, 0x80, 0x07, - 0x00, 0x00, 0x00, 0x00, 0xe0, 0x01, 0xfc, 0x03, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x01, 0xfc, 0x03, 0xc0, 0x03, 0x00, - 0x00, 0x00, 0x00, 0xc0, 0x03, 0xfc, 0x03, 0xc0, 0x03, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x03, 0xfc, 0x03, 0xe0, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x80, 0x07, 0xfc, 0x03, 0xe0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0xfc, 0x03, 0xf0, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x0f, 0xfc, 0x03, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0xfc, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3 -}; - -#define chirpy_small_image_width 8 -#define chirpy_small_image_height 8 -static unsigned char small_chirpy[] = {0x7f, 0x41, 0x55, 0x55, 0x55, 0x55, 0x41, 0x7f}; \ No newline at end of file +#endif \ No newline at end of file From 2010871e4ba1167ca0a16770245af99e837f25f3 Mon Sep 17 00:00:00 2001 From: Markus <974709+Links2004@users.noreply.github.com> Date: Sun, 21 Sep 2025 13:22:29 +0200 Subject: [PATCH 11/14] Fix Rotary Encoder Button (#8001) this fixes the Rotary Encoder Button, currenlty its not working at all. Currently the action `ROTARY_ACTION_PRESSED` is only triggerd with a IRQ on RISING, which results in nothing since the function detects the "not longer" pressed button --> no action. the `ROTARY_ACTION_PRESSED` implementation needs to be called on both edges (on press and release of the button) changing the interupt setting to `CHANGE` fixes the problem. --- src/input/RotaryEncoderInterruptBase.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/input/RotaryEncoderInterruptBase.cpp b/src/input/RotaryEncoderInterruptBase.cpp index 980057ce4..c315f23d9 100644 --- a/src/input/RotaryEncoderInterruptBase.cpp +++ b/src/input/RotaryEncoderInterruptBase.cpp @@ -27,7 +27,7 @@ void RotaryEncoderInterruptBase::init( if (!isRAK || pinPress != 0) { pinMode(pinPress, INPUT_PULLUP); - attachInterrupt(pinPress, onIntPress, RISING); + attachInterrupt(pinPress, onIntPress, CHANGE); } if (!isRAK || this->_pinA != 0) { pinMode(this->_pinA, INPUT_PULLUP); From 5701755608394c5f65c16967950e2e1b22a4f498 Mon Sep 17 00:00:00 2001 From: Tom <116762865+NomDeTom@users.noreply.github.com> Date: Sun, 21 Sep 2025 12:27:39 +0100 Subject: [PATCH 12/14] Add another seeed_xiao_nrf52840_kit build environment for I2C pinout (#8036) * Update platformio.ini * Remove some more extraneous lines --- variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini b/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini index 623eace71..4c68b40e8 100644 --- a/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini +++ b/variants/nrf52840/seeed_xiao_nrf52840_kit/platformio.ini @@ -16,3 +16,11 @@ lib_deps = debug_tool = jlink ; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) ;upload_protocol = jlink + +; Seeed Xiao BLE but with GPS undefined, and therefore i2c active +[env:seeed_xiao_nrf52840_kit_i2c] +extends = env:seeed_xiao_nrf52840_kit +board_level = extra +build_flags = ${env:seeed_xiao_nrf52840_kit.build_flags} + -DSEEED_XIAO_NRF52840_KIT +build_unflags = -DGPS_L76K From 11eb4a5b9011caabc76826cb85f28d947429d9c4 Mon Sep 17 00:00:00 2001 From: Quency-D <55523105+Quency-D@users.noreply.github.com> Date: Sun, 21 Sep 2025 20:03:44 +0800 Subject: [PATCH 13/14] Add heltec_v4 board. (#7845) * add heltec_v4 board. * Update variants/esp32s3/heltec_v4/platformio.ini Co-authored-by: Austin * Limit the maximum output power. * Trunk fixes Fixes formatting to match meshtastic trunk linter. * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Ben Meadors Co-authored-by: Austin Co-authored-by: Tom Fifield Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- boards/heltec_v4.json | 43 ++++++++++++++ src/mesh/SX126xInterface.cpp | 36 +++++++++++- src/mesh/SX126xInterface.h | 4 ++ src/platform/esp32/architecture.h | 2 + src/sleep.cpp | 6 ++ variants/esp32s3/heltec_v4/pins_arduino.h | 70 +++++++++++++++++++++++ variants/esp32s3/heltec_v4/platformio.ini | 10 ++++ variants/esp32s3/heltec_v4/variant.h | 58 +++++++++++++++++++ 8 files changed, 227 insertions(+), 2 deletions(-) create mode 100644 boards/heltec_v4.json create mode 100644 variants/esp32s3/heltec_v4/pins_arduino.h create mode 100644 variants/esp32s3/heltec_v4/platformio.ini create mode 100644 variants/esp32s3/heltec_v4/variant.h diff --git a/boards/heltec_v4.json b/boards/heltec_v4.json new file mode 100644 index 000000000..8eac3a9b2 --- /dev/null +++ b/boards/heltec_v4.json @@ -0,0 +1,43 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default_16MB.csv", + "memory_type": "qio_qspi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=0", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "psram_type": "qspi", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "heltec_v4" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "heltec_wifi_lora_32 v4 (16 MB FLASH, 2 MB PSRAM)", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 2097152, + "maximum_size": 16777216, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://heltec.org/", + "vendor": "heltec" +} diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index 49dc562d4..3fc2562b3 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -52,6 +52,16 @@ template bool SX126xInterface::init() pinMode(SX126X_POWER_EN, OUTPUT); #endif +#ifdef HELTEC_V4 + pinMode(LORA_PA_POWER, OUTPUT); + digitalWrite(LORA_PA_POWER, HIGH); + + pinMode(LORA_PA_EN, OUTPUT); + digitalWrite(LORA_PA_EN, LOW); + pinMode(LORA_PA_TX_EN, OUTPUT); + digitalWrite(LORA_PA_TX_EN, LOW); +#endif + #if ARCH_PORTDUINO tcxoVoltage = (float)portduino_config.dio3_tcxo_voltage / 1000; if (portduino_config.lora_sx126x_ant_sw_pin.pin != RADIOLIB_NC) { @@ -63,7 +73,7 @@ template bool SX126xInterface::init() LOG_DEBUG("SX126X_DIO3_TCXO_VOLTAGE not defined, not using DIO3 as TCXO reference voltage"); else LOG_DEBUG("SX126X_DIO3_TCXO_VOLTAGE defined, using DIO3 as TCXO reference voltage at %f V", tcxoVoltage); - + setTransmitEnable(false); // FIXME: May want to set depending on a definition, currently all SX126x variant files use the DC-DC regulator option bool useRegulatorLDO = false; // Seems to depend on the connection to pin 9/DCC_SW - if an inductor DCDC? @@ -259,6 +269,7 @@ template void SX126xInterface::addReceiveMetadata(meshtastic_Mes */ template void SX126xInterface::configHardwareForSend() { + setTransmitEnable(true); RadioLibInterface::configHardwareForSend(); } @@ -271,6 +282,7 @@ template void SX126xInterface::startReceive() sleep(); #else + setTransmitEnable(false); setStandby(); // We use a 16 bit preamble so this should save some power by letting radio sit in standby mostly. @@ -298,7 +310,7 @@ template bool SX126xInterface::isChannelActive() .irqFlags = RADIOLIB_IRQ_CAD_DEFAULT_FLAGS, .irqMask = RADIOLIB_IRQ_CAD_DEFAULT_MASK}}; int16_t result; - + setTransmitEnable(false); setStandby(); result = lora.scanChannel(cfg); if (result == RADIOLIB_LORA_DETECTED) @@ -337,6 +349,26 @@ template bool SX126xInterface::sleep() digitalWrite(SX126X_POWER_EN, LOW); #endif +#ifdef HELTEC_V4 + /* + * Do not switch the power on and off frequently. + * After turning off LORA_PA_EN, the power consumption has dropped to the uA level. + * // digitalWrite(LORA_PA_POWER, LOW); + */ + digitalWrite(LORA_PA_EN, LOW); + digitalWrite(LORA_PA_TX_EN, LOW); +#endif return true; } + +/** Some boards require GPIO control of tx vs rx paths */ +template void SX126xInterface::setTransmitEnable(bool txon) +{ +#ifdef HELTEC_V4 + digitalWrite(LORA_PA_POWER, HIGH); + digitalWrite(LORA_PA_EN, HIGH); + digitalWrite(LORA_PA_TX_EN, txon ? 1 : 0); +#endif +} + #endif \ No newline at end of file diff --git a/src/mesh/SX126xInterface.h b/src/mesh/SX126xInterface.h index 47b07c284..dc7024daa 100644 --- a/src/mesh/SX126xInterface.h +++ b/src/mesh/SX126xInterface.h @@ -71,5 +71,9 @@ template class SX126xInterface : public RadioLibInterface virtual void addReceiveMetadata(meshtastic_MeshPacket *mp) override; virtual void setStandby() override; + + private: + /** Some boards require GPIO control of tx vs rx paths */ + void setTransmitEnable(bool txon); }; #endif \ No newline at end of file diff --git a/src/platform/esp32/architecture.h b/src/platform/esp32/architecture.h index a87391739..6b658c2a3 100644 --- a/src/platform/esp32/architecture.h +++ b/src/platform/esp32/architecture.h @@ -197,6 +197,8 @@ #define HW_VENDOR meshtastic_HardwareModel_T_DECK_PRO #elif defined(T_LORA_PAGER) #define HW_VENDOR meshtastic_HardwareModel_T_LORA_PAGER +#elif defined(HELTEC_V4) +#define HW_VENDOR meshtastic_HardwareModel_HELTEC_V4 #elif defined(M5STACK_UNITC6L) #define HW_VENDOR meshtastic_HardwareModel_M5STACK_C6L #endif diff --git a/src/sleep.cpp b/src/sleep.cpp index 83597e349..d6eb865e5 100644 --- a/src/sleep.cpp +++ b/src/sleep.cpp @@ -545,6 +545,12 @@ void enableLoraInterrupt() gpio_pullup_en((gpio_num_t)LORA_CS); #endif +#ifdef HELTEC_V4 + gpio_pullup_en((gpio_num_t)LORA_PA_POWER); + gpio_pullup_en((gpio_num_t)LORA_PA_EN); + gpio_pulldown_en((gpio_num_t)LORA_PA_TX_EN); +#endif + LOG_INFO("setup LORA_DIO1 (GPIO%02d) with wakeup by gpio interrupt", LORA_DIO1); gpio_wakeup_enable((gpio_num_t)LORA_DIO1, GPIO_INTR_HIGH_LEVEL); diff --git a/variants/esp32s3/heltec_v4/pins_arduino.h b/variants/esp32s3/heltec_v4/pins_arduino.h new file mode 100644 index 000000000..45561b4b5 --- /dev/null +++ b/variants/esp32s3/heltec_v4/pins_arduino.h @@ -0,0 +1,70 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +static const uint8_t LED_BUILTIN = 35; +#define BUILTIN_LED LED_BUILTIN // backward compatibility +#define LED_BUILTIN LED_BUILTIN // allow testing #ifdef LED_BUILTIN + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +static const uint8_t SDA = 3; +static const uint8_t SCL = 4; + +static const uint8_t SS = 8; +static const uint8_t MOSI = 10; +static const uint8_t MISO = 11; +static const uint8_t SCK = 9; + +static const uint8_t A0 = 1; +static const uint8_t A1 = 2; +static const uint8_t A2 = 3; +static const uint8_t A3 = 4; +static const uint8_t A4 = 5; +static const uint8_t A5 = 6; +static const uint8_t A6 = 7; +static const uint8_t A7 = 8; +static const uint8_t A8 = 9; +static const uint8_t A9 = 10; +static const uint8_t A10 = 11; +static const uint8_t A11 = 12; +static const uint8_t A12 = 13; +static const uint8_t A13 = 14; +static const uint8_t A14 = 15; +static const uint8_t A15 = 16; +static const uint8_t A16 = 17; +static const uint8_t A17 = 18; +static const uint8_t A18 = 19; +static const uint8_t A19 = 20; + +static const uint8_t T1 = 1; +static const uint8_t T2 = 2; +static const uint8_t T3 = 3; +static const uint8_t T4 = 4; +static const uint8_t T5 = 5; +static const uint8_t T6 = 6; +static const uint8_t T7 = 7; +static const uint8_t T8 = 8; +static const uint8_t T9 = 9; +static const uint8_t T10 = 10; +static const uint8_t T11 = 11; +static const uint8_t T12 = 12; +static const uint8_t T13 = 13; +static const uint8_t T14 = 14; + +static const uint8_t Vext = 36; +static const uint8_t LED = 35; +static const uint8_t RST_OLED = 21; +static const uint8_t SCL_OLED = 18; +static const uint8_t SDA_OLED = 17; + +static const uint8_t RST_LoRa = 12; +static const uint8_t BUSY_LoRa = 13; +static const uint8_t DIO0 = 14; + +#endif /* Pins_Arduino_h */ \ No newline at end of file diff --git a/variants/esp32s3/heltec_v4/platformio.ini b/variants/esp32s3/heltec_v4/platformio.ini new file mode 100644 index 000000000..0177342a4 --- /dev/null +++ b/variants/esp32s3/heltec_v4/platformio.ini @@ -0,0 +1,10 @@ +[env:heltec-v4] +extends = esp32s3_base +board = heltec_v4 +board_check = true +build_flags = + ${esp32s3_base.build_flags} + -D HELTEC_V4 + -I variants/esp32s3/heltec_v4 + -D GPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. + -D SX126X_MAX_POWER=11 diff --git a/variants/esp32s3/heltec_v4/variant.h b/variants/esp32s3/heltec_v4/variant.h new file mode 100644 index 000000000..2b6b7af3d --- /dev/null +++ b/variants/esp32s3/heltec_v4/variant.h @@ -0,0 +1,58 @@ +#define LED_PIN 35 + +#define USE_SSD1306 // Heltec_v4 has an SSD1315 display (compatible with SSD1306 driver) + +#define RESET_OLED 21 +#define I2C_SDA 17 // I2C pins for this board +#define I2C_SCL 18 + +#define VEXT_ENABLE 36 // active low, powers the oled display and the lora antenna boost +#define BUTTON_PIN 0 + +#define ADC_CTRL 37 +#define ADC_CTRL_ENABLED HIGH +#define BATTERY_PIN 1 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage +#define ADC_CHANNEL ADC1_GPIO1_CHANNEL +#define ADC_ATTENUATION ADC_ATTEN_DB_2_5 // lower dB for high resistance voltage divider +#define ADC_MULTIPLIER 4.9 * 1.045 + +#define USE_SX1262 + +#define LORA_DIO0 -1 // a No connect on the SX1262 module +#define LORA_RESET 12 +#define LORA_DIO1 14 // SX1262 IRQ +#define LORA_DIO2 13 // SX1262 BUSY +#define LORA_DIO3 // Not connected on PCB, but internally on the TTGO SX1262, if DIO3 is high the TCXO is enabled + +#define LORA_SCK 9 +#define LORA_MISO 11 +#define LORA_MOSI 10 +#define LORA_CS 8 + +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET + +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +#define LORA_PA_POWER 7 // power en +#define LORA_PA_EN 2 +#define LORA_PA_TX_EN 46 // enable tx + +/* + * GPS pins + */ +#define GPS_L76K +#define PIN_GPS_RESET (42) // An output to reset L76K GPS. As per datasheet, low for > 100ms will reset the L76K +#define GPS_RESET_MODE LOW +#define PIN_GPS_EN (34) +#define GPS_EN_ACTIVE LOW +#define PERIPHERAL_WARMUP_MS 1000 // Make sure I2C QuickLink has stable power before continuing +#define PIN_GPS_STANDBY (40) // An output to wake GPS, low means allow sleep, high means force wake +#define PIN_GPS_PPS (41) +// Seems to be missing on this new board +#define GPS_TX_PIN (38) // This is for bits going TOWARDS the CPU +#define GPS_RX_PIN (39) // This is for bits going TOWARDS the GPS +#define GPS_THREAD_INTERVAL 50 \ No newline at end of file From b3df32c6c5016eae4298fce8b47a543f1201ef77 Mon Sep 17 00:00:00 2001 From: Jason P Date: Sun, 21 Sep 2025 14:04:17 -0500 Subject: [PATCH 14/14] Fix build errors (#8067) --- src/graphics/images.h | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/graphics/images.h b/src/graphics/images.h index 7b80fc5ab..b5010b116 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -287,9 +287,7 @@ const uint8_t digital_icon_clock[] PROGMEM = {0b00111100, 0b01000010, 0b10000101 #define analog_icon_clock_height 8 const uint8_t analog_icon_clock[] PROGMEM = {0b11111111, 0b01000010, 0b00100100, 0b00011000, 0b00100100, 0b01000010, 0b01000010, 0b11111111}; -#ifdef M5STACK_UNITC6L -#include "img/icon_small.xbm" -#else + #define chirpy_width 38 #define chirpy_height 50 const uint8_t chirpy[] = { @@ -362,6 +360,9 @@ const uint8_t chirpy_hirez[] = { #define chirpy_small_image_height 8 const uint8_t chirpy_small[] = {0x7f, 0x41, 0x55, 0x55, 0x55, 0x55, 0x41, 0x7f}; +#ifdef M5STACK_UNITC6L +#include "img/icon_small.xbm" +#else #include "img/icon.xbm" #endif static_assert(sizeof(icon_bits) >= 0, "Silence unused variable warning"); \ No newline at end of file