Notification and time

This commit is contained in:
Ben Meadors 2025-05-30 20:50:24 -05:00
parent 6627f2e873
commit 29402a5e7a
5 changed files with 244 additions and 209 deletions

View File

@ -31,6 +31,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "draw/DebugRenderer.h"
#include "draw/MessageRenderer.h"
#include "draw/NodeListRenderer.h"
#include "draw/NotificationRenderer.h"
#include "draw/UIRenderer.h"
#if !MESHTASTIC_EXCLUDE_GPS
#include "GPS.h"
@ -76,8 +77,6 @@ using graphics::NodeListRenderer::drawScaledXBitmap16x16;
#endif
#ifdef ARCH_ESP32
#include "esp_task_wdt.h"
#include "modules/StoreForwardModule.h"
#endif
#if ARCH_PORTDUINO
@ -141,61 +140,6 @@ static bool heartbeat = false;
#include "graphics/ScreenFonts.h"
#include <Throttle.h>
// Start Functions to write date/time to the screen
#include <string> // Only needed if you're using std::string elsewhere
bool isLeapYear(int year)
{
return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0));
}
const int daysInMonth[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
// Fills the buffer with a formatted date/time string and returns pixel width
int formatDateTime(char *buf, size_t bufSize, uint32_t rtc_sec, OLEDDisplay *display, bool includeTime)
{
int sec = rtc_sec % 60;
rtc_sec /= 60;
int min = rtc_sec % 60;
rtc_sec /= 60;
int hour = rtc_sec % 24;
rtc_sec /= 24;
int year = 1970;
while (true) {
int daysInYear = isLeapYear(year) ? 366 : 365;
if (rtc_sec >= (uint32_t)daysInYear) {
rtc_sec -= daysInYear;
year++;
} else {
break;
}
}
int month = 0;
while (month < 12) {
int dim = daysInMonth[month];
if (month == 1 && isLeapYear(year))
dim++;
if (rtc_sec >= (uint32_t)dim) {
rtc_sec -= dim;
month++;
} else {
break;
}
}
int day = rtc_sec + 1;
if (includeTime) {
snprintf(buf, bufSize, "%04d-%02d-%02d %02d:%02d:%02d", year, month + 1, day, hour, min, sec);
} else {
snprintf(buf, bufSize, "%04d-%02d-%02d", year, month + 1, day);
}
return display->getStringWidth(buf);
}
// Usage: int stringWidth = formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display);
// End Functions to write date/time to the screen
@ -367,51 +311,6 @@ void Screen::drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int1
display->setFont(FONT_MEDIUM);
display->drawString(x_offset + x, 26 + y, message);
}
// Used on boot when a certificate is being created
static void drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
display->setTextAlignment(TEXT_ALIGN_CENTER);
display->setFont(FONT_SMALL);
display->drawString(64 + x, y, "Creating SSL certificate");
#ifdef ARCH_ESP32
yield();
esp_task_wdt_reset();
#endif
display->setFont(FONT_SMALL);
if ((millis() / 1000) % 2) {
display->drawString(64 + x, FONT_HEIGHT_SMALL + y + 2, "Please wait . . .");
} else {
display->drawString(64 + x, FONT_HEIGHT_SMALL + y + 2, "Please wait . . ");
}
}
// Used when booting without a region set
static void drawWelcomeScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
display->setFont(FONT_SMALL);
display->setTextAlignment(TEXT_ALIGN_CENTER);
display->drawString(64 + x, y, "//\\ E S H T /\\ S T / C");
display->drawString(64 + x, y + FONT_HEIGHT_SMALL, getDeviceName());
display->setTextAlignment(TEXT_ALIGN_LEFT);
if ((millis() / 10000) % 2) {
display->drawString(x, y + FONT_HEIGHT_SMALL * 2 - 3, "Set the region using the");
display->drawString(x, y + FONT_HEIGHT_SMALL * 3 - 3, "Meshtastic Android, iOS,");
display->drawString(x, y + FONT_HEIGHT_SMALL * 4 - 3, "Web or CLI clients.");
} else {
display->drawString(x, y + FONT_HEIGHT_SMALL * 2 - 3, "Visit meshtastic.org");
display->drawString(x, y + FONT_HEIGHT_SMALL * 3 - 3, "for more information.");
display->drawString(x, y + FONT_HEIGHT_SMALL * 4 - 3, "");
}
#ifdef ARCH_ESP32
yield();
esp_task_wdt_reset();
#endif
}
// ==============================
// Overlay Alert Banner Renderer
// ==============================
@ -426,79 +325,6 @@ void Screen::showOverlayBanner(const String &message, uint32_t durationMs)
alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs;
}
// Draws the overlay banner on screen, if still within display duration
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state)
{
// Exit if no message is active or duration has passed
if (alertBannerMessage.length() == 0 || (alertBannerUntil != 0 && millis() > alertBannerUntil))
return;
// === Layout Configuration ===
constexpr uint16_t padding = 5; // Padding around text inside the box
constexpr uint8_t lineSpacing = 1; // Extra space between lines
// Search the mesage to determine if we need the bell added
bool needs_bell = (alertBannerMessage.indexOf("Alert Received") != -1);
// Setup font and alignment
display->setFont(FONT_SMALL);
display->setTextAlignment(TEXT_ALIGN_LEFT); // We will manually center per line
// === Split the message into lines (supports multi-line banners) ===
std::vector<String> lines;
int start = 0, newlineIdx;
while ((newlineIdx = alertBannerMessage.indexOf('\n', start)) != -1) {
lines.push_back(alertBannerMessage.substring(start, newlineIdx));
start = newlineIdx + 1;
}
lines.push_back(alertBannerMessage.substring(start));
// === Measure text dimensions ===
uint16_t minWidth = (SCREEN_WIDTH > 128) ? 106 : 78;
uint16_t maxWidth = 0;
std::vector<uint16_t> lineWidths;
for (const auto &line : lines) {
uint16_t w = display->getStringWidth(line.c_str(), line.length(), true);
lineWidths.push_back(w);
if (w > maxWidth)
maxWidth = w;
}
uint16_t boxWidth = padding * 2 + maxWidth;
if (needs_bell && boxWidth < minWidth)
boxWidth += (SCREEN_WIDTH > 128) ? 26 : 20;
uint16_t boxHeight = padding * 2 + lines.size() * FONT_HEIGHT_SMALL + (lines.size() - 1) * lineSpacing;
int16_t boxLeft = (display->width() / 2) - (boxWidth / 2);
int16_t boxTop = (display->height() / 2) - (boxHeight / 2);
// === Draw background box ===
display->setColor(BLACK);
display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); // Slightly oversized box
display->setColor(WHITE);
display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); // Border
// === Draw each line centered in the box ===
int16_t lineY = boxTop + padding;
for (size_t i = 0; i < lines.size(); ++i) {
int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2;
uint16_t line_width = display->getStringWidth(lines[i].c_str(), lines[i].length(), true);
if (needs_bell && i == 0) {
int bellY = lineY + (FONT_HEIGHT_SMALL - 8) / 2;
display->drawXbm(textX - 10, bellY, 8, 8, bell_alert);
display->drawXbm(textX + line_width + 2, bellY, 8, 8, bell_alert);
}
display->drawString(textX, lineY, lines[i]);
if (SCREEN_WIDTH > 128)
display->drawString(textX + 1, lineY, lines[i]); // Faux bold
lineY += FONT_HEIGHT_SMALL + lineSpacing;
}
}
// draw overlay in bottom right corner of screen to show when notifications are muted or modifier key is active
static void drawFunctionOverlay(OLEDDisplay *display, OLEDDisplayUiState *state)
{
@ -602,32 +428,6 @@ static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int
pi.drawFrame(display, state, x, y);
}
static void drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
display->setTextAlignment(TEXT_ALIGN_CENTER);
display->setFont(FONT_MEDIUM);
display->drawString(64 + x, y, "Updating");
display->setFont(FONT_SMALL);
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->drawStringMaxWidth(0 + x, 2 + y + FONT_HEIGHT_SMALL * 2, x + display->getWidth(),
"Please be patient and do not power off.");
}
/// Draw the last text message we received
static void drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_MEDIUM);
char tempBuf[24];
snprintf(tempBuf, sizeof(tempBuf), "Critical fault #%d", error_code);
display->drawString(0 + x, 0 + y, tempBuf);
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
display->drawString(0 + x, FONT_HEIGHT_MEDIUM + y, "For help, please visit \nmeshtastic.org");
}
// Ignore messages originating from phone (from the current node 0x0) unless range test or store and forward module are enabled
static bool shouldDrawMessage(const meshtastic_MeshPacket *packet)
{
@ -2107,7 +1907,7 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true);
char datetimeStr[25];
bool showTime = false; // set to true for full datetime
formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, showTime);
graphics::UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, showTime);
char fullLine[40];
snprintf(fullLine, sizeof(fullLine), " Date: %s", datetimeStr);
display->drawString(0, ((SCREEN_HEIGHT > 64) ? compactFifthLine : moreCompactFifthLine), fullLine);
@ -2926,7 +2726,7 @@ void Screen::setSSLFrames()
{
if (address_found.address) {
// LOG_DEBUG("Show SSL frames");
static FrameCallback sslFrames[] = {drawSSLScreen};
static FrameCallback sslFrames[] = {NotificationRenderer::drawSSLScreen};
ui->setFrames(sslFrames, 1);
ui->update();
}
@ -2938,7 +2738,7 @@ void Screen::setWelcomeFrames()
{
if (address_found.address) {
// LOG_DEBUG("Show Welcome frames");
static FrameCallback frames[] = {drawWelcomeScreen};
static FrameCallback frames[] = {NotificationRenderer::drawWelcomeScreen};
setFrameImmediateDraw(frames);
}
}
@ -3057,7 +2857,7 @@ void Screen::setFrames(FrameFocus focus)
// If we have a critical fault, show it first
fsi.positions.fault = numframes;
if (error_code) {
normalFrames[numframes++] = drawCriticalFaultFrame;
normalFrames[numframes++] = NotificationRenderer::drawCriticalFaultFrame;
indicatorIcons.push_back(icon_error);
focus = FOCUS_FAULT; // Change our "focus" parameter, to ensure we show the fault frame
}
@ -3145,7 +2945,7 @@ void Screen::setFrames(FrameFocus focus)
ui->disableAllIndicators();
// Add overlays: frame icons and alert banner)
static OverlayCallback overlays[] = {NavigationBar, drawAlertBannerOverlay};
static OverlayCallback overlays[] = {NavigationBar, NotificationRenderer::drawAlertBannerOverlay};
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list
@ -3231,7 +3031,7 @@ void Screen::handleStartFirmwareUpdateScreen()
showingNormalScreen = false;
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // E-Ink: Explicitly use fast-refresh for next frame
static FrameCallback frames[] = {drawFrameFirmware};
static FrameCallback frames[] = {NotificationRenderer::drawFrameFirmware};
setFrameImmediateDraw(frames);
}

View File

@ -0,0 +1,167 @@
#include "NotificationRenderer.h"
#include "DisplayFormatters.h"
#include "NodeDB.h"
#include "configuration.h"
#include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/images.h"
#include "main.h"
#include <algorithm>
#include <string>
#include <vector>
#ifdef ARCH_ESP32
#include "esp_task_wdt.h"
#endif
using namespace meshtastic;
// External references to global variables from Screen.cpp
extern String alertBannerMessage;
extern uint32_t alertBannerUntil;
extern std::vector<std::string> functionSymbol;
extern std::string functionSymbolString;
extern bool hasUnreadMessage;
// Used on boot when a certificate is being created
void NotificationRenderer::drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
display->setTextAlignment(TEXT_ALIGN_CENTER);
display->setFont(FONT_SMALL);
display->drawString(64 + x, y, "Creating SSL certificate");
#ifdef ARCH_ESP32
yield();
esp_task_wdt_reset();
#endif
display->setFont(FONT_SMALL);
if ((millis() / 1000) % 2) {
display->drawString(64 + x, FONT_HEIGHT_SMALL + y + 2, "Please wait . . .");
} else {
display->drawString(64 + x, FONT_HEIGHT_SMALL + y + 2, "Please wait . . ");
}
}
// Used when booting without a region set
void NotificationRenderer::drawWelcomeScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
display->setFont(FONT_SMALL);
display->setTextAlignment(TEXT_ALIGN_CENTER);
display->drawString(64 + x, y, "//\\ E S H T /\\ S T / C");
display->drawString(64 + x, y + FONT_HEIGHT_SMALL, getDeviceName());
display->setTextAlignment(TEXT_ALIGN_LEFT);
if ((millis() / 10000) % 2) {
display->drawString(x, y + FONT_HEIGHT_SMALL * 2 - 3, "Set the region using the");
display->drawString(x, y + FONT_HEIGHT_SMALL * 3 - 3, "Meshtastic Android, iOS,");
display->drawString(x, y + FONT_HEIGHT_SMALL * 4 - 3, "Web or CLI clients.");
} else {
display->drawString(x, y + FONT_HEIGHT_SMALL * 2 - 3, "Visit meshtastic.org");
display->drawString(x, y + FONT_HEIGHT_SMALL * 3 - 3, "for more information.");
display->drawString(x, y + FONT_HEIGHT_SMALL * 4 - 3, "");
}
#ifdef ARCH_ESP32
yield();
esp_task_wdt_reset();
#endif
}
void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state)
{
// Exit if no message is active or duration has passed
if (alertBannerMessage.length() == 0 || (alertBannerUntil != 0 && millis() > alertBannerUntil))
return;
// === Layout Configuration ===
constexpr uint16_t padding = 5; // Padding around text inside the box
constexpr uint8_t lineSpacing = 1; // Extra space between lines
// Search the mesage to determine if we need the bell added
bool needs_bell = (alertBannerMessage.indexOf("Alert Received") != -1);
// Setup font and alignment
display->setFont(FONT_SMALL);
display->setTextAlignment(TEXT_ALIGN_LEFT); // We will manually center per line
// === Split the message into lines (supports multi-line banners) ===
std::vector<String> lines;
int start = 0, newlineIdx;
while ((newlineIdx = alertBannerMessage.indexOf('\n', start)) != -1) {
lines.push_back(alertBannerMessage.substring(start, newlineIdx));
start = newlineIdx + 1;
}
lines.push_back(alertBannerMessage.substring(start));
// === Measure text dimensions ===
uint16_t minWidth = (SCREEN_WIDTH > 128) ? 106 : 78;
uint16_t maxWidth = 0;
std::vector<uint16_t> lineWidths;
for (const auto &line : lines) {
uint16_t w = display->getStringWidth(line.c_str(), line.length(), true);
lineWidths.push_back(w);
if (w > maxWidth)
maxWidth = w;
}
uint16_t boxWidth = padding * 2 + maxWidth;
if (needs_bell && boxWidth < minWidth)
boxWidth += (SCREEN_WIDTH > 128) ? 26 : 20;
uint16_t boxHeight = padding * 2 + lines.size() * FONT_HEIGHT_SMALL + (lines.size() - 1) * lineSpacing;
int16_t boxLeft = (display->width() / 2) - (boxWidth / 2);
int16_t boxTop = (display->height() / 2) - (boxHeight / 2);
// === Draw background box ===
display->setColor(BLACK);
display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); // Slightly oversized box
display->setColor(WHITE);
display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); // Border
// === Draw each line centered in the box ===
int16_t lineY = boxTop + padding;
for (size_t i = 0; i < lines.size(); ++i) {
int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2;
uint16_t line_width = display->getStringWidth(lines[i].c_str(), lines[i].length(), true);
if (needs_bell && i == 0) {
int bellY = lineY + (FONT_HEIGHT_SMALL - 8) / 2;
display->drawXbm(textX - 10, bellY, 8, 8, bell_alert);
display->drawXbm(textX + line_width + 2, bellY, 8, 8, bell_alert);
}
display->drawString(textX, lineY, lines[i]);
if (SCREEN_WIDTH > 128)
display->drawString(textX + 1, lineY, lines[i]); // Faux bold
lineY += FONT_HEIGHT_SMALL + lineSpacing;
}
}
/// Draw the last text message we received
void NotificationRenderer::drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_MEDIUM);
char tempBuf[24];
snprintf(tempBuf, sizeof(tempBuf), "Critical fault #%d", error_code);
display->drawString(0 + x, 0 + y, tempBuf);
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
display->drawString(0 + x, FONT_HEIGHT_MEDIUM + y, "For help, please visit \nmeshtastic.org");
}
void NotificationRenderer::drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
display->setTextAlignment(TEXT_ALIGN_CENTER);
display->setFont(FONT_MEDIUM);
display->drawString(64 + x, y, "Updating");
display->setFont(FONT_SMALL);
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->drawStringMaxWidth(0 + x, 2 + y + FONT_HEIGHT_SMALL * 2, x + display->getWidth(),
"Please be patient and do not power off.");
}

View File

@ -0,0 +1,14 @@
#pragma once
#include "OLEDDisplay.h"
#include "OLEDDisplayUi.h"
class NotificationRenderer
{
public:
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
static void drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
static void drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
static void drawWelcomeScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
static void drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
};

View File

@ -183,8 +183,62 @@ void drawGPScoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshta
}
}
} // namespace UIRenderer
// Start Functions to write date/time to the screen
// Helper function to check if a year is a leap year
bool isLeapYear(int year)
{
return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0));
}
// Array of days in each month (non-leap year)
const int daysInMonth[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
// Fills the buffer with a formatted date/time string and returns pixel width
int formatDateTime(char *buf, size_t bufSize, uint32_t rtc_sec, OLEDDisplay *display, bool includeTime)
{
int sec = rtc_sec % 60;
rtc_sec /= 60;
int min = rtc_sec % 60;
rtc_sec /= 60;
int hour = rtc_sec % 24;
rtc_sec /= 24;
int year = 1970;
while (true) {
int daysInYear = isLeapYear(year) ? 366 : 365;
if (rtc_sec >= (uint32_t)daysInYear) {
rtc_sec -= daysInYear;
year++;
} else {
break;
}
}
int month = 0;
while (month < 12) {
int dim = daysInMonth[month];
if (month == 1 && isLeapYear(year))
dim++;
if (rtc_sec >= (uint32_t)dim) {
rtc_sec -= dim;
month++;
} else {
break;
}
}
int day = rtc_sec + 1;
if (includeTime) {
snprintf(buf, bufSize, "%04d-%02d-%02d %02d:%02d:%02d", year, month + 1, day, hour, min, sec);
} else {
snprintf(buf, bufSize, "%04d-%02d-%02d", year, month + 1, day);
}
return display->getStringWidth(buf);
}
} // namespace UIRenderer
} // namespace graphics
#endif // !MESHTASTIC_EXCLUDE_GPS

View File

@ -55,7 +55,7 @@ void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string
// Time and date utilities
void getTimeAgoStr(uint32_t agoSecs, char *timeStr, uint8_t maxLength);
std::string drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds);
void formatDateTime(char *buffer, size_t bufferSize, uint32_t rtc_sec, OLEDDisplay *display, bool showTime);
int formatDateTime(char *buffer, size_t bufferSize, uint32_t rtc_sec, OLEDDisplay *display, bool showTime);
// Message filtering
bool shouldDrawMessage(const meshtastic_MeshPacket *packet);