From a12b9922ed370766bf6538df45dbe27e0e935415 Mon Sep 17 00:00:00 2001 From: todd-herbert Date: Wed, 22 May 2024 09:00:04 +1200 Subject: [PATCH] Screen changes from time deltas to timestamps after 15 minutes (#3935) * E-Ink displays sometimes use timestamp instead of delta * Allow users to disable E-Ink screensaver * Clarify variable's purpose * Operator order oopsie * Picky print problem * Implement for all display, not just E-Ink * Align "unknown age" behavior with existing code * One more use of timestamp, if screen is wide enough --- src/PowerFSM.cpp | 18 +++-- src/graphics/Screen.cpp | 142 +++++++++++++++++++++++++++++++++++----- 2 files changed, 138 insertions(+), 22 deletions(-) diff --git a/src/PowerFSM.cpp b/src/PowerFSM.cpp index 4f42b36b5..a7bc18f1a 100644 --- a/src/PowerFSM.cpp +++ b/src/PowerFSM.cpp @@ -348,12 +348,18 @@ void PowerFSM_setup() powerFSM.add_transition(&stateDARK, &stateDARK, EVENT_CONTACT_FROM_PHONE, NULL, "Contact from phone"); - powerFSM.add_timed_transition(&stateON, &stateDARK, - Default::getConfiguredOrDefaultMs(config.display.screen_on_secs, default_screen_on_secs), NULL, - "Screen-on timeout"); - powerFSM.add_timed_transition(&statePOWER, &stateDARK, - Default::getConfiguredOrDefaultMs(config.display.screen_on_secs, default_screen_on_secs), NULL, - "Screen-on timeout"); +#ifdef USE_EINK + // Allow E-Ink devices to suppress the screensaver, if screen timeout set to 0 + if (config.display.screen_on_secs > 0) +#endif + { + powerFSM.add_timed_transition(&stateON, &stateDARK, + Default::getConfiguredOrDefaultMs(config.display.screen_on_secs, default_screen_on_secs), + NULL, "Screen-on timeout"); + powerFSM.add_timed_transition(&statePOWER, &stateDARK, + Default::getConfiguredOrDefaultMs(config.display.screen_on_secs, default_screen_on_secs), + NULL, "Screen-on timeout"); + } // We never enter light-sleep or NB states on NRF52 (because the CPU uses so little power normally) #ifdef ARCH_ESP32 diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 0899335e6..bbf2c5e7c 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -419,6 +419,76 @@ static bool shouldDrawMessage(const meshtastic_MeshPacket *packet) return packet->from != 0 && !moduleConfig.store_forward.enabled; } +// Get an absolute time from "seconds ago" info. Returns false if no valid timestamp possible +bool deltaToTimestamp(uint32_t secondsAgo, uint8_t *hours, uint8_t *minutes, int32_t *daysAgo) +{ + // Cache the result - avoid frequent recalculation + static uint8_t hoursCached = 0, minutesCached = 0; + static uint32_t daysAgoCached = 0; + static uint32_t secondsAgoCached = 0; + static bool validCached = false; + + // Abort: if timezone not set + if (strlen(config.device.tzdef) == 0) { + validCached = false; + return validCached; + } + + // Abort: if invalid pointers passed + if (hours == nullptr || minutes == nullptr || daysAgo == nullptr) { + validCached = false; + return validCached; + } + + // Abort: if time seems invalid.. (> 6 months ago, probably seen before RTC set) + if (secondsAgo > SEC_PER_DAY * 30UL * 6) { + validCached = false; + return validCached; + } + + // If repeated request, don't bother recalculating + if (secondsAgo - secondsAgoCached < 60 && secondsAgoCached != 0) { + if (validCached) { + *hours = hoursCached; + *minutes = minutesCached; + *daysAgo = daysAgoCached; + } + return validCached; + } + + // Get local time + uint32_t secondsRTC = getValidTime(RTCQuality::RTCQualityDevice, true); // Get local time + + // Abort: if RTC not set + if (!secondsRTC) { + validCached = false; + return validCached; + } + + // Get absolute time when last seen + uint32_t secondsSeenAt = secondsRTC - secondsAgo; + + // Calculate daysAgo + *daysAgo = (secondsRTC / SEC_PER_DAY) - (secondsSeenAt / SEC_PER_DAY); // How many "midnights" have passed + + // Get seconds since midnight + uint32_t hms = (secondsRTC - secondsAgo) % SEC_PER_DAY; + hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; + + // Tear apart hms into hours and minutes + *hours = hms / SEC_PER_HOUR; + *minutes = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + + // Cache the result + daysAgoCached = *daysAgo; + hoursCached = *hours; + minutesCached = *minutes; + secondsAgoCached = secondsAgo; + + validCached = true; + return validCached; +} + /// Draw the last text message we received static void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { @@ -440,18 +510,36 @@ static void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state display->setColor(BLACK); } + // For time delta uint32_t seconds = sinceReceived(&mp); uint32_t minutes = seconds / 60; uint32_t hours = minutes / 60; uint32_t days = hours / 24; - if (config.display.heading_bold) { - display->drawStringf(1 + x, 0 + y, tempBuf, "%s ago from %s", - screen->drawTimeDelta(days, hours, minutes, seconds).c_str(), - (node && node->has_user) ? node->user.short_name : "???"); + // For timestamp + uint8_t timestampHours, timestampMinutes; + int32_t daysAgo; + bool useTimestamp = deltaToTimestamp(seconds, ×tampHours, ×tampMinutes, &daysAgo); + + // If bold, draw twice, shifting right by one pixel + for (uint8_t xOff = 0; xOff <= (config.display.heading_bold ? 1 : 0); xOff++) { + // Show a timestamp if received today, but longer than 15 minutes ago + if (useTimestamp && minutes >= 15 && daysAgo == 0) { + display->drawStringf(xOff + x, 0 + y, tempBuf, "At %02hu:%02hu from %s", timestampHours, timestampMinutes, + (node && node->has_user) ? node->user.short_name : "???"); + } + // Timestamp yesterday (if display is wide enough) + else if (useTimestamp && daysAgo == 1 && display->width() >= 200) { + display->drawStringf(xOff + x, 0 + y, tempBuf, "Yesterday %02hu:%02hu from %s", timestampHours, timestampMinutes, + (node && node->has_user) ? node->user.short_name : "???"); + } + // Otherwise, show a time delta + else { + display->drawStringf(xOff + x, 0 + y, tempBuf, "%s ago from %s", + screen->drawTimeDelta(days, hours, minutes, seconds).c_str(), + (node && node->has_user) ? node->user.short_name : "???"); + } } - display->drawStringf(0 + x, 0 + y, tempBuf, "%s ago from %s", screen->drawTimeDelta(days, hours, minutes, seconds).c_str(), - (node && node->has_user) ? node->user.short_name : "???"); display->setColor(WHITE); snprintf(tempBuf, sizeof(tempBuf), "%s", mp.decoded.payload.bytes); @@ -879,19 +967,32 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ uint32_t agoSecs = sinceLastSeen(node); static char lastStr[20]; + + // Use an absolute timestamp in some cases. + // Particularly useful with E-Ink displays. Static UI, fewer refreshes. + uint8_t timestampHours, timestampMinutes; + int32_t daysAgo; + bool useTimestamp = deltaToTimestamp(agoSecs, ×tampHours, ×tampMinutes, &daysAgo); + if (agoSecs < 120) // last 2 mins? snprintf(lastStr, sizeof(lastStr), "%u seconds ago", agoSecs); + // -- if suitable for timestamp -- + else if (useTimestamp && agoSecs < 15 * SECONDS_IN_MINUTE) // Last 15 minutes + snprintf(lastStr, sizeof(lastStr), "%u minutes ago", agoSecs / SECONDS_IN_MINUTE); + else if (useTimestamp && daysAgo == 0) // Today + snprintf(lastStr, sizeof(lastStr), "Last seen: %02u:%02u", (unsigned int)timestampHours, (unsigned int)timestampMinutes); + else if (useTimestamp && daysAgo == 1) // Yesterday + snprintf(lastStr, sizeof(lastStr), "Seen yesterday"); + else if (useTimestamp && daysAgo > 1) // Last six months (capped by deltaToTimestamp method) + snprintf(lastStr, sizeof(lastStr), "%li days ago", (long)daysAgo); + // -- if using time delta instead -- else if (agoSecs < 120 * 60) // last 2 hrs snprintf(lastStr, sizeof(lastStr), "%u minutes ago", agoSecs / 60); - else { - // Only show hours ago if it's been less than 6 months. Otherwise, we may have bad - // data. - if ((agoSecs / 60 / 60) < (hours_in_month * 6)) { - snprintf(lastStr, sizeof(lastStr), "%u hours ago", agoSecs / 60 / 60); - } else { - snprintf(lastStr, sizeof(lastStr), "unknown age"); - } - } + // Only show hours ago if it's been less than 6 months. Otherwise, we may have bad data. + else if ((agoSecs / 60 / 60) < (hours_in_month * 6)) + snprintf(lastStr, sizeof(lastStr), "%u hours ago", agoSecs / 60 / 60); + else + snprintf(lastStr, sizeof(lastStr), "unknown age"); static char distStr[20]; if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { @@ -900,7 +1001,7 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ strncpy(distStr, "? km", sizeof(distStr)); } meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - const char *fields[] = {username, distStr, signalStr, lastStr, NULL}; + const char *fields[] = {username, lastStr, signalStr, distStr, NULL}; int16_t compassX = 0, compassY = 0; // coordinates for the center of the compass/circle @@ -1448,6 +1549,15 @@ void Screen::setFrames() LOG_DEBUG("showing standard frames\n"); showingNormalScreen = true; +#ifdef USE_EINK + // If user has disabled the screensaver, warn them after boot + static bool warnedScreensaverDisabled = false; + if (config.display.screen_on_secs == 0 && !warnedScreensaverDisabled) { + screen->print("Screensaver disabled\n"); + warnedScreensaverDisabled = true; + } +#endif + moduleFrames = MeshModule::GetMeshModulesWithUIFrames(); LOG_DEBUG("Showing %d module frames\n", moduleFrames.size()); #ifdef DEBUG_PORT