diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 02d2b3d02..bf13fa0a2 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -51,6 +51,9 @@ extern NicheGraphics::BaseUIEInkDisplay *setupNicheGraphicsBaseUI(); #include "draw/UIRenderer.h" #include "graphics/TFTColorRegions.h" #include "modules/CannedMessageModule.h" +#if HAS_TELEMETRY && HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +#include "modules/Telemetry/EnvironmentTelemetry.h" +#endif #include "security/LockdownDisplay.h" #if !MESHTASTIC_EXCLUDE_GPS @@ -2333,6 +2336,13 @@ int Screen::handleInputEvent(const InputEvent *event) menuHandler::textMessageBaseMenu(); } } + } else if (this->ui->getUiState()->currentFrame < moduleFrames.size()) { +#if HAS_TELEMETRY && HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + const MeshModule *currentModule = moduleFrames.at(this->ui->getUiState()->currentFrame); + if (environmentTelemetryModule != nullptr && environmentTelemetryModule->ownsFrame(currentModule)) { + menuHandler::environmentTelemetryMenu(); + } +#endif } else if (framesetInfo.positions.firstFavorite != 255 && this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite && this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) { diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index a4d0b4b5e..36b2a00b9 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -26,6 +26,9 @@ #include "modules/CannedMessageModule.h" #include "modules/ExternalNotificationModule.h" #include "modules/KeyVerificationModule.h" +#if HAS_TELEMETRY && HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +#include "modules/Telemetry/EnvironmentTelemetry.h" +#endif #include "modules/TraceRouteModule.h" #include #include @@ -1471,6 +1474,76 @@ void menuHandler::positionBaseMenu() screen->showOverlayBanner(bannerOptions); } +void menuHandler::environmentTelemetryMenu() +{ +#if HAS_TELEMETRY && HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + enum optionsNumbers { Back, Source, enumEnd }; + + static const char *optionsArray[] = {"Back", "Source"}; + static int optionsEnumArray[] = {Back, Source}; + + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Environment"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.optionsCount = enumEnd; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Source) { + menuQueue = EnvironmentTelemetrySourceMenu; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); +#endif +} + +void menuHandler::environmentTelemetrySourceMenu() +{ +#if HAS_TELEMETRY && HAS_SENSOR && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR + enum optionsNumbers { Back, LocalSensor, Mesh, FavoritesOnly, enumEnd }; + static const char *optionsArray[enumEnd] = {"Back", "Local Sensor", "Mesh", "Favorite Nodes Only"}; + static int optionsEnumArray[enumEnd] = {Back, LocalSensor, Mesh, FavoritesOnly}; + + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Source"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.optionsCount = enumEnd; + + switch (EnvironmentTelemetryModule::getDisplaySource()) { + case EnvironmentTelemetryModule::DisplaySource::LocalSensor: + bannerOptions.InitialSelected = LocalSensor; + break; + case EnvironmentTelemetryModule::DisplaySource::FavoriteNodesOnly: + bannerOptions.InitialSelected = FavoritesOnly; + break; + case EnvironmentTelemetryModule::DisplaySource::Mesh: + default: + bannerOptions.InitialSelected = Mesh; + break; + } + + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Back) { + menuQueue = EnvironmentTelemetryMenu; + screen->runNow(); + return; + } + + if (selected == LocalSensor) { + EnvironmentTelemetryModule::setDisplaySource(EnvironmentTelemetryModule::DisplaySource::LocalSensor); + } else if (selected == Mesh) { + EnvironmentTelemetryModule::setDisplaySource(EnvironmentTelemetryModule::DisplaySource::Mesh); + } else if (selected == FavoritesOnly) { + EnvironmentTelemetryModule::setDisplaySource(EnvironmentTelemetryModule::DisplaySource::FavoriteNodesOnly); + } + + screen->runNow(); + }; + screen->showOverlayBanner(bannerOptions); +#endif +} + void menuHandler::nodeListMenu() { enum optionsNumbers { Back, NodePicker, TraceRoute, Verify, Reset, NodeNameLength, enumEnd }; @@ -2890,6 +2963,12 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display) case NumberTest: numberTest(); break; + case EnvironmentTelemetryMenu: + environmentTelemetryMenu(); + break; + case EnvironmentTelemetrySourceMenu: + environmentTelemetrySourceMenu(); + break; case WifiToggleMenu: wifiToggleMenu(); break; diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index b924c9197..9feae2081 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -38,6 +38,8 @@ class menuHandler RemoveFavorite, TestMenu, NumberTest, + EnvironmentTelemetryMenu, + EnvironmentTelemetrySourceMenu, WifiToggleMenu, BluetoothToggleMenu, ScreenOptionsMenu, @@ -104,6 +106,8 @@ class menuHandler static void traceRouteMenu(); static void testMenu(); static void numberTest(); + static void environmentTelemetryMenu(); + static void environmentTelemetrySourceMenu(); static void wifiBaseMenu(); static void wifiToggleMenu(); static void screenOptionsMenu(); diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index c2c819094..7ca82daee 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -142,7 +142,115 @@ extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const c #include "graphics/ScreenFonts.h" #include +EnvironmentTelemetryModule *environmentTelemetryModule = nullptr; + +namespace +{ +EnvironmentTelemetryModule::DisplaySource gDisplaySource = EnvironmentTelemetryModule::DisplaySource::Mesh; +} // namespace + static constexpr uint16_t TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY = 0x8002; +static constexpr uint32_t LOCAL_DISPLAY_REFRESH_INTERVAL_MS = 1000; + +EnvironmentTelemetryModule::DisplaySource EnvironmentTelemetryModule::getDisplaySource() +{ + return gDisplaySource; +} + +void EnvironmentTelemetryModule::setDisplaySource(DisplaySource source) +{ + gDisplaySource = source; + if (environmentTelemetryModule != nullptr) { + environmentTelemetryModule->lastLocalDisplayRefreshMs = 0; + environmentTelemetryModule->refreshDisplayedMeasurement(); + } +} + +void EnvironmentTelemetryModule::clearMeasurementPacket() +{ + if (lastMeasurementPacket != nullptr) { + packetPool.release(lastMeasurementPacket); + lastMeasurementPacket = nullptr; + } +} + +bool EnvironmentTelemetryModule::shouldDisplayRemoteNode(NodeNum nodeNum) const +{ + if (nodeNum == 0 || nodeNum == nodeDB->getNodeNum()) { + return false; + } + + switch (getDisplaySource()) { + case DisplaySource::LocalSensor: + return false; + case DisplaySource::Mesh: + return true; + case DisplaySource::FavoriteNodesOnly: + return nodeDB->isFavorite(nodeNum); + } + + return false; +} + +bool EnvironmentTelemetryModule::shouldKeepCurrentRemoteDisplay() const +{ + if (lastMeasurementPacket == nullptr) { + return false; + } + + return shouldDisplayRemoteNode(getFrom(lastMeasurementPacket)); +} + +bool EnvironmentTelemetryModule::shouldDisplayLocalMeasurement() const +{ + switch (getDisplaySource()) { + case DisplaySource::LocalSensor: + return true; + case DisplaySource::Mesh: + return !shouldKeepCurrentRemoteDisplay(); + case DisplaySource::FavoriteNodesOnly: + return false; + } + + return false; +} + +bool EnvironmentTelemetryModule::refreshLocalMeasurementPacket() +{ + meshtastic_Telemetry local = meshtastic_Telemetry_init_zero; + if (!getEnvironmentTelemetry(&local)) { + return false; + } + + meshtastic_MeshPacket *localPacket = allocDataProtobuf(local); + if (localPacket == nullptr) { + return false; + } + + clearMeasurementPacket(); + lastMeasurementPacket = packetPool.allocCopy(*localPacket); + packetPool.release(localPacket); + return lastMeasurementPacket != nullptr; +} + +void EnvironmentTelemetryModule::refreshDisplayedMeasurement() +{ + if (shouldDisplayLocalMeasurement()) { + if (lastMeasurementPacket != nullptr && + Throttle::isWithinTimespanMs(lastLocalDisplayRefreshMs, LOCAL_DISPLAY_REFRESH_INTERVAL_MS)) { + return; + } + lastLocalDisplayRefreshMs = millis(); + if (!refreshLocalMeasurementPacket()) { + clearMeasurementPacket(); + } + return; + } + + if (!shouldKeepCurrentRemoteDisplay()) { + clearMeasurementPacket(); + } +} void EnvironmentTelemetryModule::i2cScanFinished(ScanI2C *i2cScanner) { @@ -299,6 +407,7 @@ int32_t EnvironmentTelemetryModule::runOnce() result = rak9154Sensor.runOnce(); #endif #endif + refreshDisplayedMeasurement(); } // it's possible to have this module enabled, only for displaying values on the screen. // therefore, we should only enable the sensor loop if measurement is also enabled @@ -315,6 +424,7 @@ int32_t EnvironmentTelemetryModule::runOnce() result = delay; } } + refreshDisplayedMeasurement(); uint32_t lastTelemetry = transmitHistory ? transmitHistory->getLastSentToMeshMillis(TX_HISTORY_KEY_ENVIRONMENT_TELEMETRY) : 0; @@ -411,14 +521,17 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt } // === First line: Show sender name + time since received (left), and first metric (right) === - const char *sender = getSenderShortName(*lastMeasurementPacket); - uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket); - String agoStr = (agoSecs > 864000) ? "?" - : (agoSecs > 3600) ? String(agoSecs / 3600) + "h" - : (agoSecs > 60) ? String(agoSecs / 60) + "m" - : String(agoSecs) + "s"; - - String leftStr = String(sender) + " (" + agoStr + ")"; + const bool isLocalTelemetry = (getFrom(lastMeasurementPacket) == nodeDB->getNodeNum()); + const char *sender = isLocalTelemetry ? "Local Sensor" : getSenderShortName(*lastMeasurementPacket); + String leftStr = String(sender); + if (!isLocalTelemetry) { + uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket); + String agoStr = (agoSecs > 864000) ? "?" + : (agoSecs > 3600) ? String(agoSecs / 3600) + "h" + : (agoSecs > 60) ? String(agoSecs / 60) + "m" + : String(agoSecs) + "s"; + leftStr += " (" + agoStr + ")"; + } display->drawString(x, currentY, leftStr); // Left side: who and when // === Collect sensor readings as label strings (no icons) === @@ -463,7 +576,7 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt static uint32_t lastAlertTime = 0; uint32_t now = millis(); - bool isOwnTelemetry = lastMeasurementPacket->from == nodeDB->getNodeNum(); + bool isOwnTelemetry = isLocalTelemetry; bool isCooldownOver = (now - lastAlertTime > 60000); if (isOwnTelemetry && bannerMsg && isCooldownOver) { @@ -542,15 +655,17 @@ bool EnvironmentTelemetryModule::handleReceivedProtobuf(const meshtastic_MeshPac LOG_INFO("(Received from %s): radiation=%fµR/h", sender, t->variant.environment_metrics.radiation); #endif - // release previous packet before occupying a new spot - if (lastMeasurementPacket != nullptr) - packetPool.release(lastMeasurementPacket); - - lastMeasurementPacket = packetPool.allocCopy(mp); - // Cache the latest env metrics per node on NodeDB so the phone can // pull last-known values across reboots and replays. nodeDB->updateTelemetry(getFrom(&mp), *t, RX_SRC_RADIO); + + const NodeNum senderNode = getFrom(&mp); + if (shouldDisplayRemoteNode(senderNode)) { + clearMeasurementPacket(); + lastMeasurementPacket = packetPool.allocCopy(mp); + } else if (!shouldKeepCurrentRemoteDisplay()) { + refreshDisplayedMeasurement(); + } } return false; // Let others look at this message also if they want @@ -670,11 +785,6 @@ bool EnvironmentTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) p->priority = meshtastic_MeshPacket_Priority_RELIABLE; else p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; - // release previous packet before occupying a new spot - if (lastMeasurementPacket != nullptr) - packetPool.release(lastMeasurementPacket); - - lastMeasurementPacket = packetPool.allocCopy(*p); if (phoneOnly) { LOG_INFO("Send packet to phone"); service->sendToPhone(p); diff --git a/src/modules/Telemetry/EnvironmentTelemetry.h b/src/modules/Telemetry/EnvironmentTelemetry.h index 28e17a3a3..6d5678b35 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.h +++ b/src/modules/Telemetry/EnvironmentTelemetry.h @@ -17,6 +17,9 @@ #include #include +class EnvironmentTelemetryModule; +extern EnvironmentTelemetryModule *environmentTelemetryModule; + class EnvironmentTelemetryModule : private concurrency::OSThread, public ScanI2CConsumer, public BaseTelemetryModule, @@ -27,15 +30,26 @@ class EnvironmentTelemetryModule : private concurrency::OSThread, &EnvironmentTelemetryModule::handleStatusUpdate); public: + enum class DisplaySource : uint8_t { + LocalSensor = 0, + Mesh = 1, + FavoriteNodesOnly = 2, + }; + EnvironmentTelemetryModule() : concurrency::OSThread("EnvironmentTelemetry"), ScanI2CConsumer(), ProtobufModule("EnvironmentTelemetry", meshtastic_PortNum_TELEMETRY_APP, &meshtastic_Telemetry_msg) { + environmentTelemetryModule = this; + (void)getDisplaySource(); lastMeasurementPacket = nullptr; nodeStatusObserver.observe(&nodeStatus->onNewStatus); setIntervalFromNow(10 * 1000); } virtual bool wantUIFrame() override; + static DisplaySource getDisplaySource(); + static void setDisplaySource(DisplaySource source); + bool ownsFrame(const MeshModule *module) const { return module == this; } #if !HAS_SCREEN void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); #else @@ -65,8 +79,16 @@ class EnvironmentTelemetryModule : private concurrency::OSThread, void i2cScanFinished(ScanI2C *i2cScanner); private: + void clearMeasurementPacket(); + bool refreshLocalMeasurementPacket(); + void refreshDisplayedMeasurement(); + bool shouldDisplayLocalMeasurement() const; + bool shouldKeepCurrentRemoteDisplay() const; + bool shouldDisplayRemoteNode(NodeNum nodeNum) const; + bool firstTime = 1; meshtastic_MeshPacket *lastMeasurementPacket; + uint32_t lastLocalDisplayRefreshMs = 0; uint32_t sendToPhoneIntervalMs = SECONDS_IN_MINUTE * 1000; // Send to phone every minute uint32_t lastSentToPhone = 0; };