mirror of
https://github.com/meshtastic/firmware.git
synced 2025-06-07 21:52:05 +00:00
1898 lines
69 KiB
C++
1898 lines
69 KiB
C++
/*
|
||
BaseUI
|
||
|
||
Developed and Maintained By:
|
||
- Ronald Garcia (HarukiToreda) – Lead development and implementation.
|
||
- JasonP (Xaositek) – Screen layout and icon design, UI improvements and testing.
|
||
- TonyG (Tropho) – Project management, structural planning, and testing
|
||
|
||
This program is free software: you can redistribute it and/or modify
|
||
it under the terms of the GNU General Public License as published by
|
||
the Free Software Foundation, either version 3 of the License, or
|
||
(at your option) any later version.
|
||
|
||
This program is distributed in the hope that it will be useful,
|
||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
GNU General Public License for more details.
|
||
|
||
You should have received a copy of the GNU General Public License
|
||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||
|
||
*/
|
||
#include "Screen.h"
|
||
#include "PowerMon.h"
|
||
#include "Throttle.h"
|
||
#include "configuration.h"
|
||
#if HAS_SCREEN
|
||
#include <OLEDDisplay.h>
|
||
|
||
#include "DisplayFormatters.h"
|
||
#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"
|
||
#endif
|
||
#include "FSCommon.h"
|
||
#include "MeshService.h"
|
||
#include "NodeDB.h"
|
||
#include "RadioLibInterface.h"
|
||
#include "error.h"
|
||
#include "gps/GeoCoord.h"
|
||
#include "gps/RTC.h"
|
||
#include "graphics/ScreenFonts.h"
|
||
#include "graphics/SharedUIDisplay.h"
|
||
#include "graphics/emotes.h"
|
||
#include "graphics/images.h"
|
||
#include "input/ButtonThread.h"
|
||
#include "input/ScanAndSelect.h"
|
||
#include "input/TouchScreenImpl1.h"
|
||
#include "main.h"
|
||
#include "mesh-pb-constants.h"
|
||
#include "mesh/Channels.h"
|
||
#include "mesh/generated/meshtastic/deviceonly.pb.h"
|
||
#include "meshUtils.h"
|
||
#include "modules/AdminModule.h"
|
||
#include "modules/ExternalNotificationModule.h"
|
||
#include "modules/TextMessageModule.h"
|
||
#include "modules/WaypointModule.h"
|
||
#include "sleep.h"
|
||
#include "target_specific.h"
|
||
|
||
using graphics::Emote;
|
||
using graphics::emotes;
|
||
using graphics::numEmotes;
|
||
|
||
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
|
||
#include "mesh/wifi/WiFiAPClient.h"
|
||
#endif
|
||
|
||
#ifdef ARCH_ESP32
|
||
#endif
|
||
|
||
#if ARCH_PORTDUINO
|
||
#include "modules/StoreForwardModule.h"
|
||
#include "platform/portduino/PortduinoGlue.h"
|
||
#endif
|
||
|
||
using namespace meshtastic; /** @todo remove */
|
||
|
||
namespace graphics
|
||
{
|
||
|
||
// This means the *visible* area (sh1106 can address 132, but shows 128 for example)
|
||
#define IDLE_FRAMERATE 1 // in fps
|
||
|
||
// DEBUG
|
||
#define NUM_EXTRA_FRAMES 3 // text message and debug frame
|
||
// if defined a pixel will blink to show redraws
|
||
// #define SHOW_REDRAWS
|
||
|
||
// A text message frame + debug frame + all the node infos
|
||
FrameCallback *normalFrames;
|
||
static uint32_t targetFramerate = IDLE_FRAMERATE;
|
||
// Global variables for alert banner - explicitly define with extern "C" linkage to prevent optimization
|
||
|
||
uint32_t logo_timeout = 5000; // 4 seconds for EACH logo
|
||
|
||
// This image definition is here instead of images.h because it's modified dynamically by the drawBattery function
|
||
uint8_t imgBattery[16] = {0xFF, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0xE7, 0x3C};
|
||
|
||
// Threshold values for the GPS lock accuracy bar display
|
||
uint32_t dopThresholds[5] = {2000, 1000, 500, 200, 100};
|
||
|
||
// At some point, we're going to ask all of the modules if they would like to display a screen frame
|
||
// we'll need to hold onto pointers for the modules that can draw a frame.
|
||
std::vector<MeshModule *> moduleFrames;
|
||
|
||
// Global variables for screen function overlay symbols
|
||
std::vector<std::string> functionSymbol;
|
||
std::string functionSymbolString;
|
||
|
||
#if HAS_GPS
|
||
// GeoCoord object for the screen
|
||
GeoCoord geoCoord;
|
||
#endif
|
||
|
||
#ifdef SHOW_REDRAWS
|
||
static bool heartbeat = false;
|
||
#endif
|
||
|
||
#include "graphics/ScreenFonts.h"
|
||
#include <Throttle.h>
|
||
|
||
// Usage: int stringWidth = formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display);
|
||
// End Functions to write date/time to the screen
|
||
|
||
extern bool hasUnreadMessage;
|
||
|
||
// ==============================
|
||
// Overlay Alert Banner Renderer
|
||
// ==============================
|
||
// Displays a temporary centered banner message (e.g., warning, status, etc.)
|
||
// The banner appears in the center of the screen and disappears after the specified duration
|
||
|
||
// Called to trigger a banner with custom message and duration
|
||
void Screen::showOverlayBanner(const char *message, uint32_t durationMs, uint8_t options, std::function<void(int)> bannerCallback)
|
||
{
|
||
// Store the message and set the expiration timestamp
|
||
strncpy(NotificationRenderer::alertBannerMessage, message, 255);
|
||
NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination
|
||
NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs;
|
||
NotificationRenderer::alertBannerOptions = options;
|
||
NotificationRenderer::alertBannerCallback = bannerCallback;
|
||
NotificationRenderer::curSelected = 0;
|
||
}
|
||
|
||
static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||
{
|
||
uint8_t module_frame;
|
||
// there's a little but in the UI transition code
|
||
// where it invokes the function at the correct offset
|
||
// in the array of "drawScreen" functions; however,
|
||
// the passed-state doesn't quite reflect the "current"
|
||
// screen, so we have to detect it.
|
||
if (state->frameState == IN_TRANSITION && state->transitionFrameRelationship == TransitionRelationship_INCOMING) {
|
||
// if we're transitioning from the end of the frame list back around to the first
|
||
// frame, then we want this to be `0`
|
||
module_frame = state->transitionFrameTarget;
|
||
} else {
|
||
// otherwise, just display the module frame that's aligned with the current frame
|
||
module_frame = state->currentFrame;
|
||
// LOG_DEBUG("Screen is not in transition. Frame: %d", module_frame);
|
||
}
|
||
// LOG_DEBUG("Draw Module Frame %d", module_frame);
|
||
MeshModule &pi = *moduleFrames.at(module_frame);
|
||
pi.drawFrame(display, state, x, y);
|
||
}
|
||
|
||
// 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)
|
||
{
|
||
return packet->from != 0 && !moduleConfig.store_forward.enabled;
|
||
}
|
||
|
||
#if defined(DISPLAY_CLOCK_FRAME)
|
||
|
||
void Screen::drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode, float scale)
|
||
{
|
||
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
|
||
uint16_t segmentHeight = SEGMENT_HEIGHT * scale;
|
||
|
||
if (digitalMode) {
|
||
uint16_t radius = (segmentWidth + (segmentHeight * 2) + 4) / 2;
|
||
uint16_t centerX = (x + segmentHeight + 2) + (radius / 2);
|
||
uint16_t centerY = (y + segmentHeight + 2) + (radius / 2);
|
||
|
||
display->drawCircle(centerX, centerY, radius);
|
||
display->drawCircle(centerX, centerY, radius + 1);
|
||
display->drawLine(centerX, centerY, centerX, centerY - radius + 3);
|
||
display->drawLine(centerX, centerY, centerX + radius - 3, centerY);
|
||
} else {
|
||
uint16_t segmentOneX = x + segmentHeight + 2;
|
||
uint16_t segmentOneY = y;
|
||
|
||
uint16_t segmentTwoX = segmentOneX + segmentWidth + 2;
|
||
uint16_t segmentTwoY = segmentOneY + segmentHeight + 2;
|
||
|
||
uint16_t segmentThreeX = segmentOneX;
|
||
uint16_t segmentThreeY = segmentTwoY + segmentWidth + 2;
|
||
|
||
uint16_t segmentFourX = x;
|
||
uint16_t segmentFourY = y + segmentHeight + 2;
|
||
|
||
drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight);
|
||
drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight);
|
||
drawHorizontalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight);
|
||
drawVerticalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight);
|
||
}
|
||
}
|
||
|
||
// Draw a digital clock
|
||
void Screen::drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||
{
|
||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||
|
||
UIRenderer::drawBattery(display, x, y + 7, imgBattery, powerStatus);
|
||
|
||
if (powerStatus->getHasBattery()) {
|
||
char batteryPercent[8];
|
||
snprintf(batteryPercent, sizeof(batteryPercent), "%d%%", powerStatus->getBatteryChargePercent());
|
||
|
||
display->setFont(FONT_SMALL);
|
||
|
||
display->drawString(x + 20, y + 2, batteryPercent);
|
||
}
|
||
|
||
if (nimbleBluetooth && nimbleBluetooth->isConnected()) {
|
||
drawBluetoothConnectedIcon(display, display->getWidth() - 18, y + 2);
|
||
}
|
||
|
||
drawWatchFaceToggleButton(display, display->getWidth() - 36, display->getHeight() - 36, screen->digitalWatchFace, 1);
|
||
|
||
display->setColor(OLEDDISPLAY_COLOR::WHITE);
|
||
|
||
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
|
||
if (rtc_sec > 0) {
|
||
long hms = rtc_sec % SEC_PER_DAY;
|
||
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
|
||
|
||
int hour = hms / SEC_PER_HOUR;
|
||
int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||
int second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
|
||
|
||
hour = hour > 12 ? hour - 12 : hour;
|
||
|
||
if (hour == 0) {
|
||
hour = 12;
|
||
}
|
||
|
||
// Format time string
|
||
char timeString[16];
|
||
snprintf(timeString, sizeof(timeString), "%d:%02d", hour, minute);
|
||
|
||
// Format seconds string
|
||
char secondString[8];
|
||
snprintf(secondString, sizeof(secondString), "%02d", second);
|
||
|
||
float scale = 1.5;
|
||
|
||
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
|
||
uint16_t segmentHeight = SEGMENT_HEIGHT * scale;
|
||
|
||
// calculate hours:minutes string width
|
||
uint16_t timeStringWidth = strlen(timeString) * 5;
|
||
|
||
for (uint8_t i = 0; i < strlen(timeString); i++) {
|
||
char character = timeString[i];
|
||
|
||
if (character == ':') {
|
||
timeStringWidth += segmentHeight;
|
||
} else {
|
||
timeStringWidth += segmentWidth + (segmentHeight * 2) + 4;
|
||
}
|
||
}
|
||
|
||
// calculate seconds string width
|
||
uint16_t secondStringWidth = (strlen(secondString) * 12) + 4;
|
||
|
||
// sum these to get total string width
|
||
uint16_t totalWidth = timeStringWidth + secondStringWidth;
|
||
|
||
uint16_t hourMinuteTextX = (display->getWidth() / 2) - (totalWidth / 2);
|
||
|
||
uint16_t startingHourMinuteTextX = hourMinuteTextX;
|
||
|
||
uint16_t hourMinuteTextY = (display->getHeight() / 2) - (((segmentWidth * 2) + (segmentHeight * 3) + 8) / 2);
|
||
|
||
// iterate over characters in hours:minutes string and draw segmented characters
|
||
for (uint8_t i = 0; i < strlen(timeString); i++) {
|
||
char character = timeString[i];
|
||
|
||
if (character == ':') {
|
||
drawSegmentedDisplayColon(display, hourMinuteTextX, hourMinuteTextY, scale);
|
||
|
||
hourMinuteTextX += segmentHeight + 6;
|
||
} else {
|
||
drawSegmentedDisplayCharacter(display, hourMinuteTextX, hourMinuteTextY, character - '0', scale);
|
||
|
||
hourMinuteTextX += segmentWidth + (segmentHeight * 2) + 4;
|
||
}
|
||
|
||
hourMinuteTextX += 5;
|
||
}
|
||
|
||
// draw seconds string
|
||
display->setFont(FONT_MEDIUM);
|
||
display->drawString(startingHourMinuteTextX + timeStringWidth + 4,
|
||
(display->getHeight() - hourMinuteTextY) - FONT_HEIGHT_MEDIUM + 6, secondString);
|
||
}
|
||
}
|
||
|
||
void Screen::drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale)
|
||
{
|
||
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
|
||
uint16_t segmentHeight = SEGMENT_HEIGHT * scale;
|
||
|
||
uint16_t cellHeight = (segmentWidth * 2) + (segmentHeight * 3) + 8;
|
||
|
||
uint16_t topAndBottomX = x + (4 * scale);
|
||
|
||
uint16_t quarterCellHeight = cellHeight / 4;
|
||
|
||
uint16_t topY = y + quarterCellHeight;
|
||
uint16_t bottomY = y + (quarterCellHeight * 3);
|
||
|
||
display->fillRect(topAndBottomX, topY, segmentHeight, segmentHeight);
|
||
display->fillRect(topAndBottomX, bottomY, segmentHeight, segmentHeight);
|
||
}
|
||
|
||
void Screen::drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t number, float scale)
|
||
{
|
||
// the numbers 0-9, each expressed as an array of seven boolean (0|1) values encoding the on/off state of
|
||
// segment {innerIndex + 1}
|
||
// e.g., to display the numeral '0', segments 1-6 are on, and segment 7 is off.
|
||
uint8_t numbers[10][7] = {
|
||
{1, 1, 1, 1, 1, 1, 0}, // 0 Display segment key
|
||
{0, 1, 1, 0, 0, 0, 0}, // 1 1
|
||
{1, 1, 0, 1, 1, 0, 1}, // 2 ___
|
||
{1, 1, 1, 1, 0, 0, 1}, // 3 6 | | 2
|
||
{0, 1, 1, 0, 0, 1, 1}, // 4 |_7̲_|
|
||
{1, 0, 1, 1, 0, 1, 1}, // 5 5 | | 3
|
||
{1, 0, 1, 1, 1, 1, 1}, // 6 |___|
|
||
{1, 1, 1, 0, 0, 1, 0}, // 7
|
||
{1, 1, 1, 1, 1, 1, 1}, // 8 4
|
||
{1, 1, 1, 1, 0, 1, 1}, // 9
|
||
};
|
||
|
||
// the width and height of each segment's central rectangle:
|
||
// _____________________
|
||
// ⋰| (only this part, |⋱
|
||
// ⋰ | not including | ⋱
|
||
// ⋱ | the triangles | ⋰
|
||
// ⋱| on the ends) |⋰
|
||
// ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
|
||
|
||
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
|
||
uint16_t segmentHeight = SEGMENT_HEIGHT * scale;
|
||
|
||
// segment x and y coordinates
|
||
uint16_t segmentOneX = x + segmentHeight + 2;
|
||
uint16_t segmentOneY = y;
|
||
|
||
uint16_t segmentTwoX = segmentOneX + segmentWidth + 2;
|
||
uint16_t segmentTwoY = segmentOneY + segmentHeight + 2;
|
||
|
||
uint16_t segmentThreeX = segmentTwoX;
|
||
uint16_t segmentThreeY = segmentTwoY + segmentWidth + 2 + segmentHeight + 2;
|
||
|
||
uint16_t segmentFourX = segmentOneX;
|
||
uint16_t segmentFourY = segmentThreeY + segmentWidth + 2;
|
||
|
||
uint16_t segmentFiveX = x;
|
||
uint16_t segmentFiveY = segmentThreeY;
|
||
|
||
uint16_t segmentSixX = x;
|
||
uint16_t segmentSixY = segmentTwoY;
|
||
|
||
uint16_t segmentSevenX = segmentOneX;
|
||
uint16_t segmentSevenY = segmentTwoY + segmentWidth + 2;
|
||
|
||
if (numbers[number][0]) {
|
||
drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight);
|
||
}
|
||
|
||
if (numbers[number][1]) {
|
||
drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight);
|
||
}
|
||
|
||
if (numbers[number][2]) {
|
||
drawVerticalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight);
|
||
}
|
||
|
||
if (numbers[number][3]) {
|
||
drawHorizontalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight);
|
||
}
|
||
|
||
if (numbers[number][4]) {
|
||
drawVerticalSegment(display, segmentFiveX, segmentFiveY, segmentWidth, segmentHeight);
|
||
}
|
||
|
||
if (numbers[number][5]) {
|
||
drawVerticalSegment(display, segmentSixX, segmentSixY, segmentWidth, segmentHeight);
|
||
}
|
||
|
||
if (numbers[number][6]) {
|
||
drawHorizontalSegment(display, segmentSevenX, segmentSevenY, segmentWidth, segmentHeight);
|
||
}
|
||
}
|
||
|
||
void Screen::drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int height)
|
||
{
|
||
int halfHeight = height / 2;
|
||
|
||
// draw central rectangle
|
||
display->fillRect(x, y, width, height);
|
||
|
||
// draw end triangles
|
||
display->fillTriangle(x, y, x, y + height - 1, x - halfHeight, y + halfHeight);
|
||
|
||
display->fillTriangle(x + width, y, x + width + halfHeight, y + halfHeight, x + width, y + height - 1);
|
||
}
|
||
|
||
void Screen::drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int height)
|
||
{
|
||
int halfHeight = height / 2;
|
||
|
||
// draw central rectangle
|
||
display->fillRect(x, y, height, width);
|
||
|
||
// draw end triangles
|
||
display->fillTriangle(x + halfHeight, y - halfHeight, x + height - 1, y, x, y);
|
||
|
||
display->fillTriangle(x, y + width, x + height - 1, y + width, x + halfHeight, y + width + halfHeight);
|
||
}
|
||
|
||
void Screen::drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y)
|
||
{
|
||
display->drawFastImage(x, y, 18, 14, bluetoothConnectedIcon);
|
||
}
|
||
|
||
// Draw an analog clock
|
||
void Screen::drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||
{
|
||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||
|
||
UIRenderer::drawBattery(display, x, y + 7, imgBattery, powerStatus);
|
||
|
||
if (powerStatus->getHasBattery()) {
|
||
char batteryPercent[8];
|
||
snprintf(batteryPercent, sizeof(batteryPercent), "%d%%", powerStatus->getBatteryChargePercent());
|
||
|
||
display->setFont(FONT_SMALL);
|
||
|
||
display->drawString(x + 20, y + 2, batteryPercent);
|
||
}
|
||
|
||
if (nimbleBluetooth && nimbleBluetooth->isConnected()) {
|
||
drawBluetoothConnectedIcon(display, display->getWidth() - 18, y + 2);
|
||
}
|
||
|
||
drawWatchFaceToggleButton(display, display->getWidth() - 36, display->getHeight() - 36, screen->digitalWatchFace, 1);
|
||
|
||
// clock face center coordinates
|
||
int16_t centerX = display->getWidth() / 2;
|
||
int16_t centerY = display->getHeight() / 2;
|
||
|
||
// clock face radius
|
||
int16_t radius = (display->getWidth() / 2) * 0.8;
|
||
|
||
// noon (0 deg) coordinates (outermost circle)
|
||
int16_t noonX = centerX;
|
||
int16_t noonY = centerY - radius;
|
||
|
||
// second hand radius and y coordinate (outermost circle)
|
||
int16_t secondHandNoonY = noonY + 1;
|
||
|
||
// tick mark outer y coordinate; (first nested circle)
|
||
int16_t tickMarkOuterNoonY = secondHandNoonY;
|
||
|
||
// seconds tick mark inner y coordinate; (second nested circle)
|
||
double secondsTickMarkInnerNoonY = (double)noonY + 8;
|
||
|
||
// hours tick mark inner y coordinate; (third nested circle)
|
||
double hoursTickMarkInnerNoonY = (double)noonY + 16;
|
||
|
||
// minute hand y coordinate
|
||
int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4;
|
||
|
||
// hour string y coordinate
|
||
int16_t hourStringNoonY = minuteHandNoonY + 18;
|
||
|
||
// hour hand radius and y coordinate
|
||
int16_t hourHandRadius = radius * 0.55;
|
||
int16_t hourHandNoonY = centerY - hourHandRadius;
|
||
|
||
display->setColor(OLEDDISPLAY_COLOR::WHITE);
|
||
display->drawCircle(centerX, centerY, radius);
|
||
|
||
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
|
||
if (rtc_sec > 0) {
|
||
long hms = rtc_sec % SEC_PER_DAY;
|
||
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
|
||
|
||
// Tear apart hms into h:m:s
|
||
int hour = hms / SEC_PER_HOUR;
|
||
int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||
int second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
|
||
|
||
hour = hour > 12 ? hour - 12 : hour;
|
||
|
||
int16_t degreesPerHour = 30;
|
||
int16_t degreesPerMinuteOrSecond = 6;
|
||
|
||
double hourBaseAngle = hour * degreesPerHour;
|
||
double hourAngleOffset = ((double)minute / 60) * degreesPerHour;
|
||
double hourAngle = radians(hourBaseAngle + hourAngleOffset);
|
||
|
||
double minuteBaseAngle = minute * degreesPerMinuteOrSecond;
|
||
double minuteAngleOffset = ((double)second / 60) * degreesPerMinuteOrSecond;
|
||
double minuteAngle = radians(minuteBaseAngle + minuteAngleOffset);
|
||
|
||
double secondAngle = radians(second * degreesPerMinuteOrSecond);
|
||
|
||
double hourX = sin(-hourAngle) * (hourHandNoonY - centerY) + noonX;
|
||
double hourY = cos(-hourAngle) * (hourHandNoonY - centerY) + centerY;
|
||
|
||
double minuteX = sin(-minuteAngle) * (minuteHandNoonY - centerY) + noonX;
|
||
double minuteY = cos(-minuteAngle) * (minuteHandNoonY - centerY) + centerY;
|
||
|
||
double secondX = sin(-secondAngle) * (secondHandNoonY - centerY) + noonX;
|
||
double secondY = cos(-secondAngle) * (secondHandNoonY - centerY) + centerY;
|
||
|
||
display->setFont(FONT_MEDIUM);
|
||
|
||
// draw minute and hour tick marks and hour numbers
|
||
for (uint16_t angle = 0; angle < 360; angle += 6) {
|
||
double angleInRadians = radians(angle);
|
||
|
||
double sineAngleInRadians = sin(-angleInRadians);
|
||
double cosineAngleInRadians = cos(-angleInRadians);
|
||
|
||
double endX = sineAngleInRadians * (tickMarkOuterNoonY - centerY) + noonX;
|
||
double endY = cosineAngleInRadians * (tickMarkOuterNoonY - centerY) + centerY;
|
||
|
||
if (angle % degreesPerHour == 0) {
|
||
double startX = sineAngleInRadians * (hoursTickMarkInnerNoonY - centerY) + noonX;
|
||
double startY = cosineAngleInRadians * (hoursTickMarkInnerNoonY - centerY) + centerY;
|
||
|
||
// draw hour tick mark
|
||
display->drawLine(startX, startY, endX, endY);
|
||
|
||
static char buffer[2];
|
||
|
||
uint8_t hourInt = (angle / 30);
|
||
|
||
if (hourInt == 0) {
|
||
hourInt = 12;
|
||
}
|
||
|
||
// hour number x offset needs to be adjusted for some cases
|
||
int8_t hourStringXOffset;
|
||
int8_t hourStringYOffset = 13;
|
||
|
||
switch (hourInt) {
|
||
case 3:
|
||
hourStringXOffset = 5;
|
||
break;
|
||
case 9:
|
||
hourStringXOffset = 7;
|
||
break;
|
||
case 10:
|
||
case 11:
|
||
hourStringXOffset = 8;
|
||
break;
|
||
case 12:
|
||
hourStringXOffset = 13;
|
||
break;
|
||
default:
|
||
hourStringXOffset = 6;
|
||
break;
|
||
}
|
||
|
||
double hourStringX = (sineAngleInRadians * (hourStringNoonY - centerY) + noonX) - hourStringXOffset;
|
||
double hourStringY = (cosineAngleInRadians * (hourStringNoonY - centerY) + centerY) - hourStringYOffset;
|
||
|
||
// draw hour number
|
||
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
|
||
}
|
||
|
||
if (angle % degreesPerMinuteOrSecond == 0) {
|
||
double startX = sineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + noonX;
|
||
double startY = cosineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + centerY;
|
||
|
||
// draw minute tick mark
|
||
display->drawLine(startX, startY, endX, endY);
|
||
}
|
||
}
|
||
|
||
// draw hour hand
|
||
display->drawLine(centerX, centerY, hourX, hourY);
|
||
|
||
// draw minute hand
|
||
display->drawLine(centerX, centerY, minuteX, minuteY);
|
||
|
||
// draw second hand
|
||
display->drawLine(centerX, centerY, secondX, secondY);
|
||
}
|
||
}
|
||
|
||
#endif
|
||
|
||
// 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;
|
||
}
|
||
|
||
/**
|
||
* Given a recent lat/lon return a guess of the heading the user is walking on.
|
||
*
|
||
* We keep a series of "after you've gone 10 meters, what is your heading since
|
||
* the last reference point?"
|
||
*/
|
||
float Screen::estimatedHeading(double lat, double lon)
|
||
{
|
||
static double oldLat, oldLon;
|
||
static float b;
|
||
|
||
if (oldLat == 0) {
|
||
// just prepare for next time
|
||
oldLat = lat;
|
||
oldLon = lon;
|
||
|
||
return b;
|
||
}
|
||
|
||
float d = GeoCoord::latLongToMeter(oldLat, oldLon, lat, lon);
|
||
if (d < 10) // haven't moved enough, just keep current bearing
|
||
return b;
|
||
|
||
b = GeoCoord::bearing(oldLat, oldLon, lat, lon);
|
||
oldLat = lat;
|
||
oldLon = lon;
|
||
|
||
return b;
|
||
}
|
||
|
||
/// We will skip one node - the one for us, so we just blindly loop over all
|
||
/// nodes
|
||
static int8_t prevFrame = -1;
|
||
|
||
// Get a string representation of the time passed since something happened
|
||
void Screen::getTimeAgoStr(uint32_t agoSecs, char *timeStr, uint8_t maxLength)
|
||
{
|
||
// 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(timeStr, maxLength, "%u seconds ago", agoSecs);
|
||
// -- if suitable for timestamp --
|
||
else if (useTimestamp && agoSecs < 15 * SECONDS_IN_MINUTE) // Last 15 minutes
|
||
snprintf(timeStr, maxLength, "%u minutes ago", agoSecs / SECONDS_IN_MINUTE);
|
||
else if (useTimestamp && daysAgo == 0) // Today
|
||
snprintf(timeStr, maxLength, "Last seen: %02u:%02u", (unsigned int)timestampHours, (unsigned int)timestampMinutes);
|
||
else if (useTimestamp && daysAgo == 1) // Yesterday
|
||
snprintf(timeStr, maxLength, "Seen yesterday");
|
||
else if (useTimestamp && daysAgo > 1) // Last six months (capped by deltaToTimestamp method)
|
||
snprintf(timeStr, maxLength, "%li days ago", (long)daysAgo);
|
||
// -- if using time delta instead --
|
||
else if (agoSecs < 120 * 60) // last 2 hrs
|
||
snprintf(timeStr, maxLength, "%u minutes ago", agoSecs / 60);
|
||
// 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(timeStr, maxLength, "%u hours ago", agoSecs / 60 / 60);
|
||
else
|
||
snprintf(timeStr, maxLength, "unknown age");
|
||
}
|
||
|
||
// Combined dynamic node list frame cycling through LastHeard, HopSignal, and Distance modes
|
||
// Uses a single frame and changes data every few seconds (E-Ink variant is separate)
|
||
|
||
#if defined(ESP_PLATFORM) && defined(USE_ST7789)
|
||
SPIClass SPI1(HSPI);
|
||
#endif
|
||
|
||
Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_OledType screenType, OLEDDISPLAY_GEOMETRY geometry)
|
||
: concurrency::OSThread("Screen"), address_found(address), model(screenType), geometry(geometry), cmdQueue(32)
|
||
{
|
||
graphics::normalFrames = new FrameCallback[MAX_NUM_NODES + NUM_EXTRA_FRAMES];
|
||
#if defined(USE_SH1106) || defined(USE_SH1107) || defined(USE_SH1107_128_64)
|
||
dispdev = new SH1106Wire(address.address, -1, -1, geometry,
|
||
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
||
#elif defined(USE_ST7789)
|
||
#ifdef ESP_PLATFORM
|
||
dispdev = new ST7789Spi(&SPI1, ST7789_RESET, ST7789_RS, ST7789_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT, ST7789_SDA,
|
||
ST7789_MISO, ST7789_SCK);
|
||
#else
|
||
dispdev = new ST7789Spi(&SPI1, ST7789_RESET, ST7789_RS, ST7789_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT);
|
||
static_cast<ST7789Spi *>(dispdev)->setRGB(COLOR565(255, 255, 128));
|
||
#endif
|
||
#elif defined(USE_SSD1306)
|
||
dispdev = new SSD1306Wire(address.address, -1, -1, geometry,
|
||
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
||
#elif defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7789_CS) || \
|
||
defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS)
|
||
dispdev = new TFTDisplay(address.address, -1, -1, geometry,
|
||
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
||
#elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY)
|
||
dispdev = new EInkDisplay(address.address, -1, -1, geometry,
|
||
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
||
#elif defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY)
|
||
dispdev = new EInkDynamicDisplay(address.address, -1, -1, geometry,
|
||
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
||
#elif defined(USE_ST7567)
|
||
dispdev = new ST7567Wire(address.address, -1, -1, geometry,
|
||
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
||
#elif ARCH_PORTDUINO
|
||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||
if (settingsMap[displayPanel] != no_screen) {
|
||
LOG_DEBUG("Make TFTDisplay!");
|
||
dispdev = new TFTDisplay(address.address, -1, -1, geometry,
|
||
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
||
} else {
|
||
dispdev = new AutoOLEDWire(address.address, -1, -1, geometry,
|
||
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
||
isAUTOOled = true;
|
||
}
|
||
}
|
||
#else
|
||
dispdev = new AutoOLEDWire(address.address, -1, -1, geometry,
|
||
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
|
||
isAUTOOled = true;
|
||
#endif
|
||
|
||
ui = new OLEDDisplayUi(dispdev);
|
||
cmdQueue.setReader(this);
|
||
}
|
||
|
||
Screen::~Screen()
|
||
{
|
||
delete[] graphics::normalFrames;
|
||
}
|
||
|
||
/**
|
||
* Prepare the display for the unit going to the lowest power mode possible. Most screens will just
|
||
* poweroff, but eink screens will show a "I'm sleeping" graphic, possibly with a QR code
|
||
*/
|
||
void Screen::doDeepSleep()
|
||
{
|
||
#ifdef USE_EINK
|
||
setOn(false, graphics::UIRenderer::drawDeepSleepFrame);
|
||
#ifdef PIN_EINK_EN
|
||
digitalWrite(PIN_EINK_EN, LOW); // power off backlight
|
||
#endif
|
||
#else
|
||
// Without E-Ink display:
|
||
setOn(false);
|
||
#endif
|
||
}
|
||
|
||
void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
|
||
{
|
||
if (!useDisplay)
|
||
return;
|
||
|
||
if (on != screenOn) {
|
||
if (on) {
|
||
LOG_INFO("Turn on screen");
|
||
buttonThread->setScreenFlag(true);
|
||
powerMon->setState(meshtastic_PowerMon_State_Screen_On);
|
||
#ifdef T_WATCH_S3
|
||
PMU->enablePowerOutput(XPOWERS_ALDO2);
|
||
#endif
|
||
#ifdef HELTEC_TRACKER_V1_X
|
||
uint8_t tft_vext_enabled = digitalRead(VEXT_ENABLE);
|
||
#endif
|
||
#if !ARCH_PORTDUINO
|
||
dispdev->displayOn();
|
||
#endif
|
||
|
||
#if defined(ST7789_CS) && \
|
||
!defined(M5STACK) // set display brightness when turning on screens. Just moved function from TFTDisplay to here.
|
||
static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness);
|
||
#endif
|
||
|
||
dispdev->displayOn();
|
||
#ifdef HELTEC_TRACKER_V1_X
|
||
// If the TFT VEXT power is not enabled, initialize the UI.
|
||
if (!tft_vext_enabled) {
|
||
ui->init();
|
||
}
|
||
#endif
|
||
#ifdef USE_ST7789
|
||
pinMode(VTFT_CTRL, OUTPUT);
|
||
digitalWrite(VTFT_CTRL, LOW);
|
||
ui->init();
|
||
#ifdef ESP_PLATFORM
|
||
analogWrite(VTFT_LEDA, BRIGHTNESS_DEFAULT);
|
||
#else
|
||
pinMode(VTFT_LEDA, OUTPUT);
|
||
digitalWrite(VTFT_LEDA, TFT_BACKLIGHT_ON);
|
||
#endif
|
||
#endif
|
||
enabled = true;
|
||
setInterval(0); // Draw ASAP
|
||
runASAP = true;
|
||
} else {
|
||
powerMon->clearState(meshtastic_PowerMon_State_Screen_On);
|
||
#ifdef USE_EINK
|
||
// eInkScreensaver parameter is usually NULL (default argument), default frame used instead
|
||
setScreensaverFrames(einkScreensaver);
|
||
#endif
|
||
LOG_INFO("Turn off screen");
|
||
buttonThread->setScreenFlag(false);
|
||
#ifdef ELECROW_ThinkNode_M1
|
||
if (digitalRead(PIN_EINK_EN) == HIGH) {
|
||
digitalWrite(PIN_EINK_EN, LOW);
|
||
}
|
||
#endif
|
||
dispdev->displayOff();
|
||
#ifdef USE_ST7789
|
||
SPI1.end();
|
||
#if defined(ARCH_ESP32)
|
||
pinMode(VTFT_LEDA, ANALOG);
|
||
pinMode(VTFT_CTRL, ANALOG);
|
||
pinMode(ST7789_RESET, ANALOG);
|
||
pinMode(ST7789_RS, ANALOG);
|
||
pinMode(ST7789_NSS, ANALOG);
|
||
#else
|
||
nrf_gpio_cfg_default(VTFT_LEDA);
|
||
nrf_gpio_cfg_default(VTFT_CTRL);
|
||
nrf_gpio_cfg_default(ST7789_RESET);
|
||
nrf_gpio_cfg_default(ST7789_RS);
|
||
nrf_gpio_cfg_default(ST7789_NSS);
|
||
#endif
|
||
#endif
|
||
|
||
#ifdef T_WATCH_S3
|
||
PMU->disablePowerOutput(XPOWERS_ALDO2);
|
||
#endif
|
||
enabled = false;
|
||
}
|
||
screenOn = on;
|
||
}
|
||
}
|
||
|
||
void Screen::setup()
|
||
{
|
||
// === Enable display rendering ===
|
||
useDisplay = true;
|
||
|
||
// === Detect OLED subtype (if supported by board variant) ===
|
||
#ifdef AutoOLEDWire_h
|
||
if (isAUTOOled)
|
||
static_cast<AutoOLEDWire *>(dispdev)->setDetected(model);
|
||
#endif
|
||
|
||
#ifdef USE_SH1107_128_64
|
||
static_cast<SH1106Wire *>(dispdev)->setSubtype(7);
|
||
#endif
|
||
|
||
#if defined(USE_ST7789) && defined(TFT_MESH)
|
||
// Apply custom RGB color (e.g. Heltec T114/T190)
|
||
static_cast<ST7789Spi *>(dispdev)->setRGB(TFT_MESH);
|
||
#endif
|
||
|
||
// === Initialize display and UI system ===
|
||
ui->init();
|
||
displayWidth = dispdev->width();
|
||
displayHeight = dispdev->height();
|
||
|
||
ui->setTimePerTransition(0); // Disable animation delays
|
||
ui->setIndicatorPosition(BOTTOM); // Not used (indicators disabled below)
|
||
ui->setIndicatorDirection(LEFT_RIGHT); // Not used (indicators disabled below)
|
||
ui->setFrameAnimation(SLIDE_LEFT); // Used only when indicators are active
|
||
ui->disableAllIndicators(); // Disable page indicator dots
|
||
ui->getUiState()->userData = this; // Allow static callbacks to access Screen instance
|
||
|
||
// === Set custom overlay callbacks ===
|
||
static OverlayCallback overlays[] = {
|
||
graphics::UIRenderer::drawFunctionOverlay, // For mute/buzzer modifiers etc.
|
||
graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame
|
||
};
|
||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||
|
||
// === Enable UTF-8 to display mapping ===
|
||
dispdev->setFontTableLookupFunction(customFontTableLookup);
|
||
|
||
#ifdef USERPREFS_OEM_TEXT
|
||
logo_timeout *= 2; // Give more time for branded boot logos
|
||
#endif
|
||
|
||
// === Configure alert frames (e.g., "Resuming..." or region name) ===
|
||
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip slow refresh
|
||
alertFrames[0] = [this](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) {
|
||
#ifdef ARCH_ESP32
|
||
if (wakeCause == ESP_SLEEP_WAKEUP_TIMER || wakeCause == ESP_SLEEP_WAKEUP_EXT1)
|
||
graphics::UIRenderer::drawFrameText(display, state, x, y, "Resuming...");
|
||
else
|
||
#endif
|
||
{
|
||
const char *region = myRegion ? myRegion->name : nullptr;
|
||
graphics::UIRenderer::drawIconScreen(region, display, state, x, y);
|
||
}
|
||
};
|
||
ui->setFrames(alertFrames, 1);
|
||
ui->disableAutoTransition(); // Require manual navigation between frames
|
||
|
||
// === Log buffer for on-screen logs (3 lines max) ===
|
||
dispdev->setLogBuffer(3, 32);
|
||
|
||
// === Optional screen mirroring or flipping (e.g. for T-Beam orientation) ===
|
||
#ifdef SCREEN_MIRROR
|
||
dispdev->mirrorScreen();
|
||
#else
|
||
if (!config.display.flip_screen) {
|
||
#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \
|
||
defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS)
|
||
static_cast<TFTDisplay *>(dispdev)->flipScreenVertically();
|
||
#elif defined(USE_ST7789)
|
||
static_cast<ST7789Spi *>(dispdev)->flipScreenVertically();
|
||
#else
|
||
dispdev->flipScreenVertically();
|
||
#endif
|
||
}
|
||
#endif
|
||
|
||
// === Generate device ID from MAC address ===
|
||
uint8_t dmac[6];
|
||
getMacAddr(dmac);
|
||
snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]);
|
||
|
||
#if ARCH_PORTDUINO
|
||
handleSetOn(false); // Ensure proper init for Arduino targets
|
||
#endif
|
||
|
||
// === Turn on display and trigger first draw ===
|
||
handleSetOn(true);
|
||
ui->update();
|
||
#ifndef USE_EINK
|
||
ui->update(); // Some SSD1306 clones drop the first draw, so run twice
|
||
#endif
|
||
serialSinceMsec = millis();
|
||
|
||
#if ARCH_PORTDUINO
|
||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
||
if (settingsMap[touchscreenModule]) {
|
||
touchScreenImpl1 =
|
||
new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast<TFTDisplay *>(dispdev)->getTouch);
|
||
touchScreenImpl1->init();
|
||
}
|
||
}
|
||
#elif HAS_TOUCHSCREEN
|
||
touchScreenImpl1 =
|
||
new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast<TFTDisplay *>(dispdev)->getTouch);
|
||
touchScreenImpl1->init();
|
||
#endif
|
||
|
||
// === Subscribe to device status updates ===
|
||
powerStatusObserver.observe(&powerStatus->onNewStatus);
|
||
gpsStatusObserver.observe(&gpsStatus->onNewStatus);
|
||
nodeStatusObserver.observe(&nodeStatus->onNewStatus);
|
||
|
||
#if !MESHTASTIC_EXCLUDE_ADMIN
|
||
adminMessageObserver.observe(adminModule);
|
||
#endif
|
||
if (textMessageModule)
|
||
textMessageObserver.observe(textMessageModule);
|
||
if (inputBroker)
|
||
inputObserver.observe(inputBroker);
|
||
|
||
// === Notify modules that support UI events ===
|
||
MeshModule::observeUIEvents(&uiFrameEventObserver);
|
||
}
|
||
|
||
void Screen::forceDisplay(bool forceUiUpdate)
|
||
{
|
||
// Nasty hack to force epaper updates for 'key' frames. FIXME, cleanup.
|
||
#ifdef USE_EINK
|
||
// If requested, make sure queued commands are run, and UI has rendered a new frame
|
||
if (forceUiUpdate) {
|
||
// Force a display refresh, in addition to the UI update
|
||
// Changing the GPS status bar icon apparently doesn't register as a change in image
|
||
// (False negative of the image hashing algorithm used to skip identical frames)
|
||
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST);
|
||
|
||
// No delay between UI frame rendering
|
||
setFastFramerate();
|
||
|
||
// Make sure all CMDs have run first
|
||
while (!cmdQueue.isEmpty())
|
||
runOnce();
|
||
|
||
// Ensure at least one frame has drawn
|
||
uint64_t startUpdate;
|
||
do {
|
||
startUpdate = millis(); // Handle impossibly unlikely corner case of a millis() overflow..
|
||
delay(10);
|
||
ui->update();
|
||
} while (ui->getUiState()->lastUpdate < startUpdate);
|
||
|
||
// Return to normal frame rate
|
||
targetFramerate = IDLE_FRAMERATE;
|
||
ui->setTargetFPS(targetFramerate);
|
||
}
|
||
|
||
// Tell EInk class to update the display
|
||
static_cast<EInkDisplay *>(dispdev)->forceDisplay();
|
||
#endif
|
||
}
|
||
|
||
static uint32_t lastScreenTransition;
|
||
|
||
int32_t Screen::runOnce()
|
||
{
|
||
// If we don't have a screen, don't ever spend any CPU for us.
|
||
if (!useDisplay) {
|
||
enabled = false;
|
||
return RUN_SAME;
|
||
}
|
||
|
||
if (displayHeight == 0) {
|
||
displayHeight = dispdev->getHeight();
|
||
}
|
||
|
||
// Show boot screen for first logo_timeout seconds, then switch to normal operation.
|
||
// serialSinceMsec adjusts for additional serial wait time during nRF52 bootup
|
||
static bool showingBootScreen = true;
|
||
if (showingBootScreen && (millis() > (logo_timeout + serialSinceMsec))) {
|
||
LOG_INFO("Done with boot screen");
|
||
stopBootScreen();
|
||
showingBootScreen = false;
|
||
}
|
||
|
||
#ifdef USERPREFS_OEM_TEXT
|
||
static bool showingOEMBootScreen = true;
|
||
if (showingOEMBootScreen && (millis() > ((logo_timeout / 2) + serialSinceMsec))) {
|
||
LOG_INFO("Switch to OEM screen...");
|
||
// Change frames.
|
||
static FrameCallback bootOEMFrames[] = {graphics::UIRenderer::drawOEMBootScreen};
|
||
static const int bootOEMFrameCount = sizeof(bootOEMFrames) / sizeof(bootOEMFrames[0]);
|
||
ui->setFrames(bootOEMFrames, bootOEMFrameCount);
|
||
ui->update();
|
||
#ifndef USE_EINK
|
||
ui->update();
|
||
#endif
|
||
showingOEMBootScreen = false;
|
||
}
|
||
#endif
|
||
|
||
#ifndef DISABLE_WELCOME_UNSET
|
||
if (!NotificationRenderer::isOverlayBannerShowing() && config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
|
||
showOverlayBanner(
|
||
"Set the LoRa "
|
||
"region\nUS\nEU_433\nEU_868\nCN\nJP\nANZ\nKR\nTW\nRU\nIN\nNZ_865\nTH\nLORA_24\nUA_433\nUA_868\nMY_433\nMY_919\nSG_"
|
||
"923\nPH_433\nPH_868\nPH_915",
|
||
0, 21, [](int selected) -> void {
|
||
LOG_WARN("Chose %d", selected);
|
||
config.lora.region = _meshtastic_Config_LoRaConfig_RegionCode(selected + 1);
|
||
if (!owner.is_licensed) {
|
||
bool keygenSuccess = false;
|
||
if (config.security.private_key.size == 32) {
|
||
if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) {
|
||
keygenSuccess = true;
|
||
}
|
||
} else {
|
||
LOG_INFO("Generate new PKI keys");
|
||
crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes);
|
||
keygenSuccess = true;
|
||
}
|
||
if (keygenSuccess) {
|
||
config.security.public_key.size = 32;
|
||
config.security.private_key.size = 32;
|
||
owner.public_key.size = 32;
|
||
memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32);
|
||
}
|
||
}
|
||
config.lora.tx_enabled = true;
|
||
initRegion();
|
||
if (myRegion->dutyCycle < 100) {
|
||
config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit
|
||
}
|
||
service->reloadConfig(SEGMENT_CONFIG);
|
||
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
||
});
|
||
}
|
||
#endif
|
||
if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) {
|
||
showOverlayBanner("Rebooting...", 0);
|
||
}
|
||
|
||
// Process incoming commands.
|
||
for (;;) {
|
||
ScreenCmd cmd;
|
||
if (!cmdQueue.dequeue(&cmd, 0)) {
|
||
break;
|
||
}
|
||
switch (cmd.cmd) {
|
||
case Cmd::SET_ON:
|
||
handleSetOn(true);
|
||
break;
|
||
case Cmd::SET_OFF:
|
||
handleSetOn(false);
|
||
break;
|
||
case Cmd::ON_PRESS:
|
||
handleOnPress();
|
||
break;
|
||
case Cmd::SHOW_PREV_FRAME:
|
||
handleShowPrevFrame();
|
||
break;
|
||
case Cmd::SHOW_NEXT_FRAME:
|
||
handleShowNextFrame();
|
||
break;
|
||
case Cmd::START_ALERT_FRAME: {
|
||
showingBootScreen = false; // this should avoid the edge case where an alert triggers before the boot screen goes away
|
||
showingNormalScreen = false;
|
||
alertFrames[0] = alertFrame;
|
||
#ifdef USE_EINK
|
||
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?)
|
||
#endif
|
||
setFrameImmediateDraw(alertFrames);
|
||
break;
|
||
}
|
||
case Cmd::START_FIRMWARE_UPDATE_SCREEN:
|
||
handleStartFirmwareUpdateScreen();
|
||
break;
|
||
case Cmd::STOP_ALERT_FRAME:
|
||
case Cmd::STOP_BOOT_SCREEN:
|
||
EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame
|
||
setFrames();
|
||
break;
|
||
case Cmd::PRINT:
|
||
handlePrint(cmd.print_text);
|
||
free(cmd.print_text);
|
||
break;
|
||
default:
|
||
LOG_ERROR("Invalid screen cmd");
|
||
}
|
||
}
|
||
|
||
if (!screenOn) { // If we didn't just wake and the screen is still off, then
|
||
// stop updating until it is on again
|
||
enabled = false;
|
||
return 0;
|
||
}
|
||
|
||
// this must be before the frameState == FIXED check, because we always
|
||
// want to draw at least one FIXED frame before doing forceDisplay
|
||
ui->update();
|
||
|
||
// Switch to a low framerate (to save CPU) when we are not in transition
|
||
// but we should only call setTargetFPS when framestate changes, because
|
||
// otherwise that breaks animations.
|
||
|
||
if (targetFramerate != IDLE_FRAMERATE && ui->getUiState()->frameState == FIXED) {
|
||
// oldFrameState = ui->getUiState()->frameState;
|
||
targetFramerate = IDLE_FRAMERATE;
|
||
|
||
ui->setTargetFPS(targetFramerate);
|
||
forceDisplay();
|
||
}
|
||
|
||
// While showing the bootscreen or Bluetooth pair screen all of our
|
||
// standard screen switching is stopped.
|
||
if (showingNormalScreen) {
|
||
// standard screen loop handling here
|
||
if (config.display.auto_screen_carousel_secs > 0 &&
|
||
!Throttle::isWithinTimespanMs(lastScreenTransition, config.display.auto_screen_carousel_secs * 1000)) {
|
||
|
||
// If an E-Ink display struggles with fast refresh, force carousel to use full refresh instead
|
||
// Carousel is potentially a major source of E-Ink display wear
|
||
#if !defined(EINK_BACKGROUND_USES_FAST)
|
||
EINK_ADD_FRAMEFLAG(dispdev, COSMETIC);
|
||
#endif
|
||
|
||
LOG_DEBUG("LastScreenTransition exceeded %ums transition to next frame", (millis() - lastScreenTransition));
|
||
handleOnPress();
|
||
}
|
||
}
|
||
|
||
// LOG_DEBUG("want fps %d, fixed=%d", targetFramerate,
|
||
// ui->getUiState()->frameState); If we are scrolling we need to be called
|
||
// soon, otherwise just 1 fps (to save CPU) We also ask to be called twice
|
||
// as fast as we really need so that any rounding errors still result with
|
||
// the correct framerate
|
||
return (1000 / targetFramerate);
|
||
}
|
||
|
||
/* show a message that the SSL cert is being built
|
||
* it is expected that this will be used during the boot phase */
|
||
void Screen::setSSLFrames()
|
||
{
|
||
if (address_found.address) {
|
||
// LOG_DEBUG("Show SSL frames");
|
||
static FrameCallback sslFrames[] = {NotificationRenderer::drawSSLScreen};
|
||
ui->setFrames(sslFrames, 1);
|
||
ui->update();
|
||
}
|
||
}
|
||
|
||
#ifdef USE_EINK
|
||
/// Determine which screensaver frame to use, then set the FrameCallback
|
||
void Screen::setScreensaverFrames(FrameCallback einkScreensaver)
|
||
{
|
||
// Retain specified frame / overlay callback beyond scope of this method
|
||
static FrameCallback screensaverFrame;
|
||
static OverlayCallback screensaverOverlay;
|
||
|
||
#if defined(HAS_EINK_ASYNCFULL) && defined(USE_EINK_DYNAMICDISPLAY)
|
||
// Join (await) a currently running async refresh, then run the post-update code.
|
||
// Avoid skipping of screensaver frame. Would otherwise be handled by NotifiedWorkerThread.
|
||
EINK_JOIN_ASYNCREFRESH(dispdev);
|
||
#endif
|
||
|
||
// If: one-off screensaver frame passed as argument. Handles doDeepSleep()
|
||
if (einkScreensaver != NULL) {
|
||
screensaverFrame = einkScreensaver;
|
||
ui->setFrames(&screensaverFrame, 1);
|
||
}
|
||
|
||
// Else, display the usual "overlay" screensaver
|
||
else {
|
||
screensaverOverlay = graphics::UIRenderer::drawScreensaverOverlay;
|
||
ui->setOverlays(&screensaverOverlay, 1);
|
||
}
|
||
|
||
// Request new frame, ASAP
|
||
setFastFramerate();
|
||
uint64_t startUpdate;
|
||
do {
|
||
startUpdate = millis(); // Handle impossibly unlikely corner case of a millis() overflow..
|
||
delay(1);
|
||
ui->update();
|
||
} while (ui->getUiState()->lastUpdate < startUpdate);
|
||
|
||
// Old EInkDisplay class
|
||
#if !defined(USE_EINK_DYNAMICDISPLAY)
|
||
static_cast<EInkDisplay *>(dispdev)->forceDisplay(0); // Screen::forceDisplay(), but override rate-limit
|
||
#endif
|
||
|
||
// Prepare now for next frame, shown when display wakes
|
||
ui->setOverlays(NULL, 0); // Clear overlay
|
||
setFrames(FOCUS_PRESERVE); // Return to normal display updates, showing same frame as before screensaver, ideally
|
||
|
||
// Pick a refresh method, for when display wakes
|
||
#ifdef EINK_HASQUIRK_GHOSTING
|
||
EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // Really ugly to see ghosting from "screen paused"
|
||
#else
|
||
EINK_ADD_FRAMEFLAG(dispdev, RESPONSIVE); // Really nice to wake screen with a fast-refresh
|
||
#endif
|
||
}
|
||
#endif
|
||
|
||
// Regenerate the normal set of frames, focusing a specific frame if requested
|
||
// Called when a frame should be added / removed, or custom frames should be cleared
|
||
void Screen::setFrames(FrameFocus focus)
|
||
{
|
||
uint8_t originalPosition = ui->getUiState()->currentFrame;
|
||
FramesetInfo fsi; // Location of specific frames, for applying focus parameter
|
||
|
||
LOG_DEBUG("Show standard frames");
|
||
showingNormalScreen = true;
|
||
|
||
indicatorIcons.clear();
|
||
#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("Show %d module frames", moduleFrames.size());
|
||
#ifdef DEBUG_PORT
|
||
int totalFrameCount = MAX_NUM_NODES + NUM_EXTRA_FRAMES + moduleFrames.size();
|
||
LOG_DEBUG("Total frame count: %d", totalFrameCount);
|
||
#endif
|
||
|
||
// 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--;
|
||
|
||
size_t numframes = 0;
|
||
|
||
// put all of the module frames first.
|
||
// this is a little bit of a dirty hack; since we're going to call
|
||
// the same drawModuleFrame handler here for all of these module frames
|
||
// and then we'll just assume that the state->currentFrame value
|
||
// is the same offset into the moduleFrames vector
|
||
// so that we can invoke the module's callback
|
||
for (auto i = moduleFrames.begin(); i != moduleFrames.end(); ++i) {
|
||
// Draw the module frame, using the hack described above
|
||
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->isRequestingFocus())
|
||
fsi.positions.focusedModule = numframes;
|
||
if (m == waypointModule)
|
||
fsi.positions.waypoint = numframes;
|
||
|
||
indicatorIcons.push_back(icon_module);
|
||
numframes++;
|
||
}
|
||
|
||
LOG_DEBUG("Added modules. numframes: %d", numframes);
|
||
|
||
// If we have a critical fault, show it first
|
||
fsi.positions.fault = numframes;
|
||
if (error_code) {
|
||
normalFrames[numframes++] = NotificationRenderer::drawCriticalFaultFrame;
|
||
indicatorIcons.push_back(icon_error);
|
||
focus = FOCUS_FAULT; // Change our "focus" parameter, to ensure we show the fault frame
|
||
}
|
||
|
||
#if defined(DISPLAY_CLOCK_FRAME)
|
||
normalFrames[numframes++] = screen->digitalWatchFace ? &Screen::drawDigitalClockFrame : &Screen::drawAnalogClockFrame;
|
||
indicatorIcons.push_back(icon_compass);
|
||
#endif
|
||
|
||
// Declare this early so it’s available in FOCUS_PRESERVE block
|
||
bool willInsertTextMessage = shouldDrawMessage(&devicestate.rx_text_message);
|
||
|
||
if (willInsertTextMessage) {
|
||
fsi.positions.textMessage = numframes;
|
||
normalFrames[numframes++] = graphics::MessageRenderer::drawTextMessageFrame;
|
||
indicatorIcons.push_back(icon_mail);
|
||
}
|
||
|
||
normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused;
|
||
indicatorIcons.push_back(icon_home);
|
||
|
||
#ifndef USE_EINK
|
||
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen;
|
||
indicatorIcons.push_back(icon_nodes);
|
||
#endif
|
||
|
||
// Show detailed node views only on E-Ink builds
|
||
#ifdef USE_EINK
|
||
normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen;
|
||
indicatorIcons.push_back(icon_nodes);
|
||
|
||
normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen;
|
||
indicatorIcons.push_back(icon_signal);
|
||
|
||
normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen;
|
||
indicatorIcons.push_back(icon_distance);
|
||
#endif
|
||
|
||
normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses;
|
||
indicatorIcons.push_back(icon_list);
|
||
|
||
fsi.positions.gps = numframes;
|
||
normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen;
|
||
indicatorIcons.push_back(icon_compass);
|
||
|
||
normalFrames[numframes++] = graphics::DebugRenderer::drawLoRaFocused;
|
||
indicatorIcons.push_back(icon_radio);
|
||
|
||
if (!dismissedFrames.memory) {
|
||
fsi.positions.memory = numframes;
|
||
normalFrames[numframes++] = graphics::DebugRenderer::drawMemoryUsage;
|
||
indicatorIcons.push_back(icon_memory);
|
||
}
|
||
|
||
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) {
|
||
normalFrames[numframes++] = graphics::UIRenderer::drawNodeInfo;
|
||
indicatorIcons.push_back(icon_node);
|
||
}
|
||
}
|
||
|
||
// then the debug info
|
||
|
||
// Since frames are basic function pointers, we have to use a helper to
|
||
// call a method on debugInfo object.
|
||
// fsi.positions.log = numframes;
|
||
// normalFrames[numframes++] = graphics::DebugRenderer::drawDebugInfoTrampoline;
|
||
|
||
// call a method on debugInfoScreen object (for more details)
|
||
// fsi.positions.settings = numframes;
|
||
// normalFrames[numframes++] = graphics::DebugRenderer::drawDebugInfoSettingsTrampoline;
|
||
|
||
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
|
||
if (!dismissedFrames.wifi && isWifiAvailable()) {
|
||
fsi.positions.wifi = numframes;
|
||
normalFrames[numframes++] = graphics::DebugRenderer::drawDebugInfoWiFiTrampoline;
|
||
indicatorIcons.push_back(icon_wifi);
|
||
}
|
||
#endif
|
||
|
||
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::drawAlertBannerOverlay};
|
||
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_PRESERVE:
|
||
// No more adjustment — force stay on same index
|
||
if (originalPosition < fsi.frameCount)
|
||
ui->switchToFrame(originalPosition);
|
||
else
|
||
ui->switchToFrame(fsi.frameCount - 1);
|
||
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();
|
||
}
|
||
|
||
// Dismisses the currently displayed screen frame, if possible
|
||
// Relevant for text message, waypoint, others in future?
|
||
// Triggered with a CardKB keycombo
|
||
void Screen::dismissCurrentFrame()
|
||
{
|
||
uint8_t currentFrame = ui->getUiState()->currentFrame;
|
||
bool dismissed = false;
|
||
|
||
if (currentFrame == framesetInfo.positions.textMessage && devicestate.has_rx_text_message) {
|
||
LOG_INFO("Dismiss Text Message");
|
||
devicestate.has_rx_text_message = false;
|
||
memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message));
|
||
dismissedFrames.textMessage = true;
|
||
dismissed = true;
|
||
} else if (currentFrame == framesetInfo.positions.waypoint && devicestate.has_rx_waypoint) {
|
||
LOG_DEBUG("Dismiss Waypoint");
|
||
devicestate.has_rx_waypoint = false;
|
||
dismissedFrames.waypoint = true;
|
||
dismissed = true;
|
||
} else if (currentFrame == framesetInfo.positions.wifi) {
|
||
LOG_DEBUG("Dismiss WiFi Screen");
|
||
dismissedFrames.wifi = true;
|
||
dismissed = true;
|
||
} else if (currentFrame == framesetInfo.positions.memory) {
|
||
LOG_INFO("Dismiss Memory");
|
||
dismissedFrames.memory = true;
|
||
dismissed = true;
|
||
}
|
||
|
||
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
|
||
|
||
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::increaseBrightness()
|
||
{
|
||
brightness = ((brightness + 62) > 254) ? brightness : (brightness + 62);
|
||
|
||
#if defined(ST7789_CS)
|
||
// run the setDisplayBrightness function. This works on t-decks
|
||
static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness);
|
||
#endif
|
||
|
||
/* 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);
|
||
|
||
#if defined(ST7789_CS)
|
||
static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness);
|
||
#endif
|
||
|
||
/* 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::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::handlePrint(const char *text)
|
||
{
|
||
// the string passed into us probably has a newline, but that would confuse the logging system
|
||
// so strip it
|
||
LOG_DEBUG("Screen: %.*s", strlen(text) - 1, text);
|
||
if (!useDisplay || !showingNormalScreen)
|
||
return;
|
||
|
||
dispdev->print(text);
|
||
}
|
||
|
||
void Screen::handleOnPress()
|
||
{
|
||
// If Canned Messages is using the "Scan and Select" input, dismiss the canned message frame when user button is pressed
|
||
// Minimize impact as a courtesy, as "scan and select" may be used as default config for some boards
|
||
if (scanAndSelectInput != nullptr && scanAndSelectInput->dismissCannedMessageFrame())
|
||
return;
|
||
|
||
// 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::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()
|
||
{
|
||
// We are about to start a transition so speed up fps
|
||
targetFramerate = SCREEN_TRANSITION_FRAMERATE;
|
||
|
||
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;
|
||
}
|
||
|
||
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));
|
||
dismissedFrames.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
|
||
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<const char *>(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\nfrom %s", longName);
|
||
} else {
|
||
strcpy(banner, "Alert Received");
|
||
}
|
||
} else {
|
||
if (longName && longName[0]) {
|
||
snprintf(banner, sizeof(banner), "New Message\nfrom %s", longName);
|
||
} else {
|
||
strcpy(banner, "New Message");
|
||
}
|
||
}
|
||
|
||
screen->showOverlayBanner(banner, 3000);
|
||
}
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
// Triggered by MeshModules
|
||
int Screen::handleUIFrameEvent(const UIFrameEvent *event)
|
||
{
|
||
if (showingNormalScreen) {
|
||
// Regenerate the frameset, potentially honoring a module's internal requestFocus() call
|
||
if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET)
|
||
setFrames(FOCUS_MODULE);
|
||
|
||
// Regenerate the frameset, while Attempt to maintain focus on the current frame
|
||
else if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET_BACKGROUND)
|
||
setFrames(FOCUS_PRESERVE);
|
||
|
||
// Don't regenerate the frameset, just re-draw whatever is on screen ASAP
|
||
else if (event->action == UIFrameEvent::Action::REDRAW_ONLY)
|
||
setFastFramerate();
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
int Screen::handleInputEvent(const InputEvent *event)
|
||
{
|
||
|
||
if (NotificationRenderer::isOverlayBannerShowing()) {
|
||
NotificationRenderer::inEvent = event->inputEvent;
|
||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar,
|
||
NotificationRenderer::drawAlertBannerOverlay};
|
||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||
setFastFramerate(); // Draw ASAP
|
||
ui->update();
|
||
return 0;
|
||
}
|
||
#if defined(DISPLAY_CLOCK_FRAME)
|
||
// For the T-Watch, intercept touches to the 'toggle digital/analog watch face' button
|
||
uint8_t watchFaceFrame = error_code ? 1 : 0;
|
||
|
||
if (this->ui->getUiState()->currentFrame == watchFaceFrame && event->touchX >= 204 && event->touchX <= 240 &&
|
||
event->touchY >= 204 && event->touchY <= 240) {
|
||
screen->digitalWatchFace = !screen->digitalWatchFace;
|
||
|
||
setFrames();
|
||
|
||
return 0;
|
||
}
|
||
#endif
|
||
|
||
// 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->interceptingKeyboardInput())
|
||
inputIntercepted = true;
|
||
}
|
||
|
||
// Only allow BUTTON_PRESSED and BUTTON_LONG_PRESSED to trigger frame changes if no module is handling input
|
||
if (!inputIntercepted) {
|
||
if (event->inputEvent == INPUT_BROKER_MSG_BUTTON_PRESSED) {
|
||
showNextFrame();
|
||
return 0;
|
||
} else if (event->inputEvent == INPUT_BROKER_MSG_BUTTON_LONG_PRESSED) {
|
||
// Optional: Define alternate screen action or no-op
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
// If no modules are using the input, move between frames
|
||
if (!inputIntercepted) {
|
||
if (event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) {
|
||
showPrevFrame();
|
||
} else if (event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT)) {
|
||
showNextFrame();
|
||
} else if (event->inputEvent ==
|
||
static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT) ||
|
||
event->inputEvent == INPUT_BROKER_MSG_BUTTON_DOUBLE_PRESSED) {
|
||
#if HAS_TFT
|
||
if (this->ui->getUiState()->currentFrame == framesetInfo.positions.memory) {
|
||
showOverlayBanner("Switch to MUI?\nYES\nNO", 30000, 2, [](int selected) -> void {
|
||
if (selected == 0) {
|
||
config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR;
|
||
config.bluetooth.enabled = false;
|
||
service->reloadConfig(SEGMENT_CONFIG);
|
||
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
||
}
|
||
});
|
||
}
|
||
#endif
|
||
#if HAS_GPS
|
||
if (this->ui->getUiState()->currentFrame == framesetInfo.positions.gps) {
|
||
showOverlayBanner("Toggle GPS\nENABLED\nDISABLED", 30000, 2, [](int selected) -> void {
|
||
if (selected == 0) {
|
||
config.position.gps_enabled = true;
|
||
gps->enable();
|
||
} else if (selected == 1) {
|
||
config.position.gps_enabled = false;
|
||
gps->disable();
|
||
}
|
||
});
|
||
}
|
||
#endif
|
||
}
|
||
}
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
int Screen::handleAdminMessage(const meshtastic_AdminMessage *arg)
|
||
{
|
||
switch (arg->which_payload_variant) {
|
||
// Node removed manually (i.e. via app)
|
||
case meshtastic_AdminMessage_remove_by_nodenum_tag:
|
||
setFrames(FOCUS_PRESERVE);
|
||
break;
|
||
|
||
// Default no-op, in case the admin message observable gets used by other classes in future
|
||
default:
|
||
break;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
bool Screen::isOverlayBannerShowing()
|
||
{
|
||
return NotificationRenderer::isOverlayBannerShowing();
|
||
}
|
||
|
||
} // namespace graphics
|
||
#else
|
||
graphics::Screen::Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY) {}
|
||
#endif // HAS_SCREEN
|