mirror of
https://github.com/meshtastic/firmware.git
synced 2026-01-12 04:47:23 +00:00
I thought git would be smart enough to understand all the whitespace changes but even with all the flags I know to make it ignore theses it still blows up if there are identical changes on both sides.
I have a solution but it require creating a new commit at the merge base for each conflicting PR and merging it into develop.
I don't think blowing up all PRs is worth for now, maybe if we can coordinate this for V3 let's say.
This reverts commit 0d11331d18.
525 lines
20 KiB
C++
525 lines
20 KiB
C++
#include "configuration.h"
|
||
#if HAS_SCREEN
|
||
#include "MeshService.h"
|
||
#include "RTC.h"
|
||
#include "draw/NodeListRenderer.h"
|
||
#include "graphics/ScreenFonts.h"
|
||
#include "graphics/SharedUIDisplay.h"
|
||
#include "graphics/draw/UIRenderer.h"
|
||
#include "main.h"
|
||
#include "meshtastic/config.pb.h"
|
||
#include "modules/ExternalNotificationModule.h"
|
||
#include "power.h"
|
||
#include <OLEDDisplay.h>
|
||
#include <graphics/images.h>
|
||
|
||
namespace graphics
|
||
{
|
||
|
||
ScreenResolution determineScreenResolution(int16_t screenheight, int16_t screenwidth)
|
||
{
|
||
|
||
#ifdef FORCE_LOW_RES
|
||
return ScreenResolution::Low;
|
||
#else
|
||
// Unit C6L and other ultra low res screens
|
||
if (screenwidth <= 64 || screenheight <= 48) {
|
||
return ScreenResolution::UltraLow;
|
||
}
|
||
|
||
// Standard OLED screens
|
||
if (screenwidth > 128 && screenheight <= 64) {
|
||
return ScreenResolution::Low;
|
||
}
|
||
|
||
// High Resolutions screens like T114, TDeck, TLora Pager, etc
|
||
if (screenwidth > 128) {
|
||
return ScreenResolution::High;
|
||
}
|
||
|
||
// Default to low resolution
|
||
return ScreenResolution::Low;
|
||
#endif
|
||
}
|
||
|
||
void decomposeTime(uint32_t rtc_sec, int &hour, int &minute, int &second)
|
||
{
|
||
hour = 0;
|
||
minute = 0;
|
||
second = 0;
|
||
if (rtc_sec == 0)
|
||
return;
|
||
uint32_t hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
|
||
hour = hms / SEC_PER_HOUR;
|
||
minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||
second = hms % SEC_PER_MIN;
|
||
}
|
||
|
||
// === Shared External State ===
|
||
bool hasUnreadMessage = false;
|
||
ScreenResolution currentResolution = ScreenResolution::Low;
|
||
|
||
// === Internal State ===
|
||
bool isBoltVisibleShared = true;
|
||
uint32_t lastBlinkShared = 0;
|
||
bool isMailIconVisible = true;
|
||
uint32_t lastMailBlink = 0;
|
||
|
||
// *********************************
|
||
// * Rounded Header when inverted *
|
||
// *********************************
|
||
void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r)
|
||
{
|
||
// Draw the center and side rectangles
|
||
display->fillRect(x + r, y, w - 2 * r, h); // center bar
|
||
display->fillRect(x, y + r, r, h - 2 * r); // left edge
|
||
display->fillRect(x + w - r, y + r, r, h - 2 * r); // right edge
|
||
|
||
// Draw the rounded corners using filled circles
|
||
display->fillCircle(x + r + 1, y + r, r); // top-left
|
||
display->fillCircle(x + w - r - 1, y + r, r); // top-right
|
||
display->fillCircle(x + r + 1, y + h - r - 1, r); // bottom-left
|
||
display->fillCircle(x + w - r - 1, y + h - r - 1, r); // bottom-right
|
||
}
|
||
|
||
// *************************
|
||
// * Common Header Drawing *
|
||
// *************************
|
||
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, bool show_date)
|
||
{
|
||
constexpr int HEADER_OFFSET_Y = 1;
|
||
y += HEADER_OFFSET_Y;
|
||
|
||
display->setFont(FONT_SMALL);
|
||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||
|
||
const int xOffset = 4;
|
||
const int highlightHeight = FONT_HEIGHT_SMALL - 1;
|
||
const bool isInverted = (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED);
|
||
const bool isBold = config.display.heading_bold;
|
||
|
||
const int screenW = display->getWidth();
|
||
const int screenH = display->getHeight();
|
||
|
||
if (!force_no_invert) {
|
||
// === Inverted Header Background ===
|
||
if (isInverted) {
|
||
display->setColor(BLACK);
|
||
display->fillRect(0, 0, screenW, highlightHeight + 2);
|
||
display->setColor(WHITE);
|
||
drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2);
|
||
display->setColor(BLACK);
|
||
} else {
|
||
display->setColor(BLACK);
|
||
display->fillRect(0, 0, screenW, highlightHeight + 2);
|
||
display->setColor(WHITE);
|
||
if (currentResolution == ScreenResolution::High) {
|
||
display->drawLine(0, 20, screenW, 20);
|
||
} else {
|
||
display->drawLine(0, 14, screenW, 14);
|
||
}
|
||
}
|
||
|
||
// === Screen Title ===
|
||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||
display->drawString(SCREEN_WIDTH / 2, y, titleStr);
|
||
if (config.display.heading_bold) {
|
||
display->drawString((SCREEN_WIDTH / 2) + 1, y, titleStr);
|
||
}
|
||
}
|
||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||
|
||
// === Battery State ===
|
||
int chargePercent = powerStatus->getBatteryChargePercent();
|
||
bool isCharging = powerStatus->getIsCharging();
|
||
bool usbPowered = powerStatus->getHasUSB();
|
||
|
||
if (chargePercent >= 100) {
|
||
isCharging = false;
|
||
}
|
||
if (chargePercent == 101) {
|
||
usbPowered = true; // Forcing this flag on for the express purpose that some devices have no concept of having a USB cable
|
||
// plugged in
|
||
}
|
||
|
||
uint32_t now = millis();
|
||
|
||
#ifndef USE_EINK
|
||
if (isCharging && now - lastBlinkShared > 500) {
|
||
isBoltVisibleShared = !isBoltVisibleShared;
|
||
lastBlinkShared = now;
|
||
}
|
||
#endif
|
||
|
||
bool useHorizontalBattery = (currentResolution == ScreenResolution::High && screenW >= screenH);
|
||
const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2;
|
||
|
||
int batteryX = 1;
|
||
int batteryY = HEADER_OFFSET_Y + 1;
|
||
#if !defined(M5STACK_UNITC6L)
|
||
// === Battery Icons ===
|
||
if (usbPowered && !isCharging) { // This is a basic check to determine USB Powered is flagged but not charging
|
||
batteryX += 1;
|
||
batteryY += 2;
|
||
if (currentResolution == ScreenResolution::High) {
|
||
display->drawXbm(batteryX, batteryY, 19, 12, imgUSB_HighResolution);
|
||
batteryX += 20; // Icon + 1 pixel
|
||
} else {
|
||
display->drawXbm(batteryX, batteryY, 10, 8, imgUSB);
|
||
batteryX += 11; // Icon + 1 pixel
|
||
}
|
||
} else {
|
||
if (useHorizontalBattery) {
|
||
batteryX += 1;
|
||
batteryY += 2;
|
||
display->drawXbm(batteryX, batteryY, 9, 13, batteryBitmap_h_bottom);
|
||
display->drawXbm(batteryX + 9, batteryY, 9, 13, batteryBitmap_h_top);
|
||
if (isCharging && isBoltVisibleShared)
|
||
display->drawXbm(batteryX + 4, batteryY, 9, 13, lightning_bolt_h);
|
||
else {
|
||
display->drawLine(batteryX + 5, batteryY, batteryX + 10, batteryY);
|
||
display->drawLine(batteryX + 5, batteryY + 12, batteryX + 10, batteryY + 12);
|
||
int fillWidth = 14 * chargePercent / 100;
|
||
display->fillRect(batteryX + 1, batteryY + 1, fillWidth, 11);
|
||
}
|
||
batteryX += 18; // Icon + 2 pixels
|
||
} else {
|
||
#ifdef USE_EINK
|
||
batteryY += 2;
|
||
#endif
|
||
display->drawXbm(batteryX, batteryY, 7, 11, batteryBitmap_v);
|
||
if (isCharging && isBoltVisibleShared)
|
||
display->drawXbm(batteryX + 1, batteryY + 3, 5, 5, lightning_bolt_v);
|
||
else {
|
||
display->drawXbm(batteryX - 1, batteryY + 4, 8, 3, batteryBitmap_sidegaps_v);
|
||
int fillHeight = 8 * chargePercent / 100;
|
||
int fillY = batteryY - fillHeight;
|
||
display->fillRect(batteryX + 1, fillY + 10, 5, fillHeight);
|
||
}
|
||
batteryX += 9; // Icon + 2 pixels
|
||
}
|
||
}
|
||
|
||
if (chargePercent != 101) {
|
||
// === Battery % Display ===
|
||
char chargeStr[4];
|
||
snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent);
|
||
int chargeNumWidth = display->getStringWidth(chargeStr);
|
||
display->drawString(batteryX, textY, chargeStr);
|
||
display->drawString(batteryX + chargeNumWidth - 1, textY, "%");
|
||
if (isBold) {
|
||
display->drawString(batteryX + 1, textY, chargeStr);
|
||
display->drawString(batteryX + chargeNumWidth, textY, "%");
|
||
}
|
||
}
|
||
|
||
// === Time and Right-aligned Icons ===
|
||
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true);
|
||
char timeStr[10] = "--:--"; // Fallback display
|
||
int timeStrWidth = display->getStringWidth("12:34"); // Default alignment
|
||
int timeX = screenW - xOffset - timeStrWidth + 4;
|
||
|
||
if (rtc_sec > 0) {
|
||
// === Build Time String ===
|
||
long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
|
||
int hour, minute, second;
|
||
graphics::decomposeTime(rtc_sec, hour, minute, second);
|
||
snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute);
|
||
|
||
// === Build Date String ===
|
||
char datetimeStr[25];
|
||
UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, false);
|
||
char dateLine[40];
|
||
|
||
if (currentResolution == ScreenResolution::High) {
|
||
snprintf(dateLine, sizeof(dateLine), "%s", datetimeStr);
|
||
} else {
|
||
if (hasUnreadMessage) {
|
||
snprintf(dateLine, sizeof(dateLine), "%s", &datetimeStr[5]);
|
||
} else {
|
||
snprintf(dateLine, sizeof(dateLine), "%s", &datetimeStr[2]);
|
||
}
|
||
}
|
||
|
||
if (config.display.use_12h_clock) {
|
||
bool isPM = hour >= 12;
|
||
hour %= 12;
|
||
if (hour == 0)
|
||
hour = 12;
|
||
snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "p" : "a");
|
||
}
|
||
|
||
if (show_date) {
|
||
timeStrWidth = display->getStringWidth(dateLine);
|
||
} else {
|
||
timeStrWidth = display->getStringWidth(timeStr);
|
||
}
|
||
timeX = screenW - xOffset - timeStrWidth + 3;
|
||
|
||
// === Show Mail or Mute Icon to the Left of Time ===
|
||
int iconRightEdge = timeX - 2;
|
||
|
||
bool showMail = false;
|
||
|
||
#ifndef USE_EINK
|
||
if (hasUnreadMessage) {
|
||
if (now - lastMailBlink > 500) {
|
||
isMailIconVisible = !isMailIconVisible;
|
||
lastMailBlink = now;
|
||
}
|
||
showMail = isMailIconVisible;
|
||
}
|
||
#else
|
||
if (hasUnreadMessage) {
|
||
showMail = true;
|
||
}
|
||
#endif
|
||
|
||
if (showMail) {
|
||
if (useHorizontalBattery) {
|
||
int iconW = 16, iconH = 12;
|
||
int iconX = iconRightEdge - iconW;
|
||
int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1;
|
||
if (isInverted && !force_no_invert) {
|
||
display->setColor(WHITE);
|
||
display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2);
|
||
display->setColor(BLACK);
|
||
} else {
|
||
display->setColor(BLACK);
|
||
display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2);
|
||
display->setColor(WHITE);
|
||
}
|
||
display->drawRect(iconX, iconY, iconW + 1, iconH);
|
||
display->drawLine(iconX, iconY, iconX + iconW / 2, iconY + iconH - 4);
|
||
display->drawLine(iconX + iconW, iconY, iconX + iconW / 2, iconY + iconH - 4);
|
||
} else {
|
||
int iconX = iconRightEdge - (mail_width - 2);
|
||
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
|
||
if (isInverted && !force_no_invert) {
|
||
display->setColor(WHITE);
|
||
display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2);
|
||
display->setColor(BLACK);
|
||
} else {
|
||
display->setColor(BLACK);
|
||
display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2);
|
||
display->setColor(WHITE);
|
||
}
|
||
display->drawXbm(iconX, iconY, mail_width, mail_height, mail);
|
||
}
|
||
} else if (externalNotificationModule->getMute()) {
|
||
if (currentResolution == ScreenResolution::High) {
|
||
int iconX = iconRightEdge - mute_symbol_big_width;
|
||
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
|
||
|
||
if (isInverted && !force_no_invert) {
|
||
display->setColor(WHITE);
|
||
display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2);
|
||
display->setColor(BLACK);
|
||
} else {
|
||
display->setColor(BLACK);
|
||
display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2);
|
||
display->setColor(WHITE);
|
||
}
|
||
display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big);
|
||
} else {
|
||
int iconX = iconRightEdge - mute_symbol_width;
|
||
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
|
||
|
||
if (isInverted && !force_no_invert) {
|
||
display->setColor(WHITE);
|
||
display->fillRect(iconX - 1, iconY - 1, mute_symbol_width + 2, mute_symbol_height + 2);
|
||
display->setColor(BLACK);
|
||
} else {
|
||
display->setColor(BLACK);
|
||
display->fillRect(iconX - 1, iconY - 1, mute_symbol_width + 2, mute_symbol_height + 2);
|
||
display->setColor(WHITE);
|
||
}
|
||
display->drawXbm(iconX, iconY, mute_symbol_width, mute_symbol_height, mute_symbol);
|
||
}
|
||
}
|
||
|
||
if (show_date) {
|
||
// === Draw Date ===
|
||
display->drawString(timeX, textY, dateLine);
|
||
if (isBold)
|
||
display->drawString(timeX - 1, textY, dateLine);
|
||
} else {
|
||
// === Draw Time ===
|
||
display->drawString(timeX, textY, timeStr);
|
||
if (isBold)
|
||
display->drawString(timeX - 1, textY, timeStr);
|
||
}
|
||
|
||
} else {
|
||
// === No Time Available: Mail/Mute Icon Moves to Far Right ===
|
||
int iconRightEdge = screenW - xOffset;
|
||
|
||
bool showMail = false;
|
||
|
||
#ifndef USE_EINK
|
||
if (hasUnreadMessage) {
|
||
if (now - lastMailBlink > 500) {
|
||
isMailIconVisible = !isMailIconVisible;
|
||
lastMailBlink = now;
|
||
}
|
||
showMail = isMailIconVisible;
|
||
}
|
||
#else
|
||
if (hasUnreadMessage) {
|
||
showMail = true;
|
||
}
|
||
#endif
|
||
|
||
if (showMail) {
|
||
if (useHorizontalBattery) {
|
||
int iconW = 16, iconH = 12;
|
||
int iconX = iconRightEdge - iconW;
|
||
int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1;
|
||
display->drawRect(iconX, iconY, iconW + 1, iconH);
|
||
display->drawLine(iconX, iconY, iconX + iconW / 2, iconY + iconH - 4);
|
||
display->drawLine(iconX + iconW, iconY, iconX + iconW / 2, iconY + iconH - 4);
|
||
} else {
|
||
int iconX = iconRightEdge - mail_width;
|
||
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
|
||
display->drawXbm(iconX, iconY, mail_width, mail_height, mail);
|
||
}
|
||
} else if (externalNotificationModule->getMute()) {
|
||
if (currentResolution == ScreenResolution::High) {
|
||
int iconX = iconRightEdge - mute_symbol_big_width;
|
||
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
|
||
display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big);
|
||
} else {
|
||
int iconX = iconRightEdge - mute_symbol_width;
|
||
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
|
||
display->drawXbm(iconX, iconY, mute_symbol_width, mute_symbol_height, mute_symbol);
|
||
}
|
||
}
|
||
}
|
||
#endif
|
||
display->setColor(WHITE); // Reset for other UI
|
||
}
|
||
|
||
const int *getTextPositions(OLEDDisplay *display)
|
||
{
|
||
static int textPositions[7]; // Static array that persists beyond function scope
|
||
|
||
if (currentResolution == ScreenResolution::High) {
|
||
textPositions[0] = textZeroLine;
|
||
textPositions[1] = textFirstLine_medium;
|
||
textPositions[2] = textSecondLine_medium;
|
||
textPositions[3] = textThirdLine_medium;
|
||
textPositions[4] = textFourthLine_medium;
|
||
textPositions[5] = textFifthLine_medium;
|
||
textPositions[6] = textSixthLine_medium;
|
||
} else {
|
||
textPositions[0] = textZeroLine;
|
||
textPositions[1] = textFirstLine;
|
||
textPositions[2] = textSecondLine;
|
||
textPositions[3] = textThirdLine;
|
||
textPositions[4] = textFourthLine;
|
||
textPositions[5] = textFifthLine;
|
||
textPositions[6] = textSixthLine;
|
||
}
|
||
return textPositions;
|
||
}
|
||
|
||
// *************************
|
||
// * Common Footer Drawing *
|
||
// *************************
|
||
void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y)
|
||
{
|
||
bool drawConnectionState = false;
|
||
if (service->api_state == service->STATE_BLE || service->api_state == service->STATE_WIFI ||
|
||
service->api_state == service->STATE_SERIAL || service->api_state == service->STATE_PACKET ||
|
||
service->api_state == service->STATE_HTTP || service->api_state == service->STATE_ETH) {
|
||
drawConnectionState = true;
|
||
}
|
||
|
||
if (drawConnectionState) {
|
||
const int scale = (currentResolution == ScreenResolution::High) ? 2 : 1;
|
||
display->setColor(BLACK);
|
||
display->fillRect(0, SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale), (connection_icon_width * scale),
|
||
(connection_icon_height * scale) + (2 * scale));
|
||
display->setColor(WHITE);
|
||
if (currentResolution == ScreenResolution::High) {
|
||
const int bytesPerRow = (connection_icon_width + 7) / 8;
|
||
int iconX = 0;
|
||
int iconY = SCREEN_HEIGHT - (connection_icon_height * 2);
|
||
|
||
for (int yy = 0; yy < connection_icon_height; ++yy) {
|
||
const uint8_t *rowPtr = connection_icon + yy * bytesPerRow;
|
||
for (int xx = 0; xx < connection_icon_width; ++xx) {
|
||
const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3));
|
||
const uint8_t bitMask = 1U << (xx & 7); // XBM is LSB-first
|
||
if (byteVal & bitMask) {
|
||
display->fillRect(iconX + xx * scale, iconY + yy * scale, scale, scale);
|
||
}
|
||
}
|
||
}
|
||
|
||
} else {
|
||
display->drawXbm(0, SCREEN_HEIGHT - connection_icon_height, connection_icon_width, connection_icon_height,
|
||
connection_icon);
|
||
}
|
||
}
|
||
}
|
||
|
||
bool isAllowedPunctuation(char c)
|
||
{
|
||
const std::string allowed = ".,!?;:-_()[]{}'\"@#$/\\&+=%~^ ";
|
||
return allowed.find(c) != std::string::npos;
|
||
}
|
||
|
||
static void replaceAll(std::string &s, const std::string &from, const std::string &to)
|
||
{
|
||
if (from.empty())
|
||
return;
|
||
size_t pos = 0;
|
||
while ((pos = s.find(from, pos)) != std::string::npos) {
|
||
s.replace(pos, from.size(), to);
|
||
pos += to.size();
|
||
}
|
||
}
|
||
|
||
std::string sanitizeString(const std::string &input)
|
||
{
|
||
std::string output;
|
||
bool inReplacement = false;
|
||
|
||
// Make a mutable copy so we can normalize UTF-8 “smart punctuation” into ASCII first.
|
||
std::string s = input;
|
||
|
||
// Curly single quotes: ‘ ’
|
||
replaceAll(s, "\xE2\x80\x98", "'"); // U+2018
|
||
replaceAll(s, "\xE2\x80\x99", "'"); // U+2019
|
||
|
||
// Curly double quotes: “ ”
|
||
replaceAll(s, "\xE2\x80\x9C", "\""); // U+201C
|
||
replaceAll(s, "\xE2\x80\x9D", "\""); // U+201D
|
||
|
||
// En dash / Em dash: – —
|
||
replaceAll(s, "\xE2\x80\x93", "-"); // U+2013
|
||
replaceAll(s, "\xE2\x80\x94", "-"); // U+2014
|
||
|
||
// Non-breaking space
|
||
replaceAll(s, "\xC2\xA0", " "); // U+00A0
|
||
|
||
// Now do your original sanitize pass over the normalized string.
|
||
for (unsigned char uc : s) {
|
||
char c = static_cast<char>(uc);
|
||
if (std::isalnum(uc) || isAllowedPunctuation(c)) {
|
||
output += c;
|
||
inReplacement = false;
|
||
} else {
|
||
if (!inReplacement) {
|
||
output += static_cast<char>(0xBF); // ISO-8859-1 for inverted question mark
|
||
inReplacement = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
return output;
|
||
}
|
||
|
||
} // namespace graphics
|
||
#endif |