mirror of
https://github.com/meshtastic/firmware.git
synced 2026-01-05 17:40:51 +00:00
Unify the native display config between legacy display and MUI (#6838)
* Add missed include * Another Warning fix * Add another HAS_SCREEN * Namespace fixes * Removed depricated destination types and re-factored destination screen * Get rid of Arduino Strings * Clean up after Copilot * SixthLine Def, Screen Rename Added Sixth Line Definition Screen Rename, and Automatic Line Adjustment * Consistency is hard - fixed "Sixth" * System Frame Updates Adjusted line construction to ensure we fit maximum content per screen. * Fix up notifications * Add a couple more ifdef HAS_SCREEN lines * Add screen->isOverlayBannerShowing() * Don't forget the invert! * Adjust Nodelist Center Divider Adjust Nodelist Center Divider * Fix variable casting * Fix entryText variable as empty before update to fix validation * Altitude is int32_t * Update PowerTelemetry to have correct data type * Fix cppcheck warnings (#6945) * Fix cppcheck warnings * Adjust logic in Power.cpp for power sensor --------- Co-authored-by: Jason P <applewiz@mac.com> * More pixel wrangling so things line up NodeList edition * Adjust NodeList alignments and plumb some background padding for a possible title fix * Better alignment for banner notifications * Move title into drawCommonHeader; initial screen tested * Fonts make spacing items difficult * Improved beeping booping and other buzzer based feedback (#6947) * Improved beeping booping and other buzzer based feedback * audible button feedback (#6949) * Refactor --------- Co-authored-by: todd-herbert <herbert.todd@gmail.com> * Sandpapered the corners of the notification popup * Finalize drawCommonHeader migration * Update Title of Favorite Node Screens * Update node metric alignment on LoRa screen * Update the border for popups to separate it from background * Update PaxcounterModule.cpp with CommonHeader * Update WiFi screen with CommonHeader and related data reflow * It was not, in fact, pointing up * Fix build on wismeshtap * T-deck trackball debounce * Fix uptime on Device Focused page to actually detail * Update Sys screen for new uptime, add label to Freq/Chan on LoRa * Don't display DOP any longer, make Uptime consistent * Revert Uptime change on Favorites, Apply to Device Focused * Label the satelite number to avoid confusion * Boop boop boop boop * Correct GPS positioning and string consistency across strings for GPS * Fix GPS text alignment * Enable canned messages by default * Don't wake screen on new nodes * Cannedmessage list emote support added * Fn+e emote picker for freetext screen * Actually block CannedInput actions while display is shown * Add selection menu to bannerOverlay * Off by one * Move to unified text layouts and spacing * Still my Fav without an "e" * Fully remove EVENT_NODEDB_UPDATED * Simply LoRa screen * Make some char pointers const to fix compilation on native targets * Update drawCompassNorth to include radius * Fix warning * button thread cleanup * Pull OneButton handling from PowerFSM and add MUI switch (#6973) * Trunk * Onebutton Menu Support * Add temporary clock icon * Add gps location to fsi * Banner message state reset * Cast to char to satisfy compiler * Better fast handling of input during banner * Fix warning * Derp * oops * Update ref * Wire buzzer_mode * remove legacy string->print() * Only init screen if one found * Unsigned Char * More buttonThread cleaning * screen.cpp button handling cleanup * The Great Event Rename of 2025 * Fix the Radiomaster * Missed trackball type change * Remove unused function * Make ButtonThread an InputBroker * Coffee hadn't kicked in yet * Add clock icon for Navigation Bar * Restore clock screen definition code - whoops * ExternalNotifications now observe inputBroker * Clock rework (#6992) * Move Clock bits into ClockRenderer space * Rework clock into all device navigation * T-Watch Actually Builds Different * Compile fix --------- Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> * Add AM/PM to Digital Clock * Flip Seconds and AM/PM on Clock Display * Tik-tok pixels are hard * Fix builds on Thinknode M1 * Check for GPS and don't crash * Don't endif til the end * Rework the OneButton thread to be much less of a mess. (#6997) * Rework the OneButton thread to be much less of a mess. And break lots of targets temporarily * Update src/input/ButtonThread.h Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix GPS toggle * Send the shutdown event, not just the kbchar * Honor the back button in a notificaiton popup * Draw the right size box for popup with options * Try to un-break all the things --------- Co-authored-by: Ben Meadors <benmmeadors@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * 24-hour Clock Should have leading zero, but not 12-hour * Fixup some compile errors * Add intRoutine to ButtonThread init, to get more responsive user button back * Add Timezone picker * Fix Warning * Optionally set the initial selection for the chooser popup * Make back buttons work in canned messages * Drop the wrapper classes * LonPressTime now configurable * Clock Frame can not longer be blank; just add valid time * Back buttons everywhere! * Key Verification confirm banner * Make Elecrow M* top button a back button * Add settings saves * EInk responsiveness fixes * Linux Input Fixes * Add Native Trackball/Joystick support, and move UserButton to Input * No Flight Stick Mode * Send input event * Add Channel Utilization to Device Focused frame * Don't shift screens when we draw new ones * Add showOverlayBanner arguments to no-op * trunk * Default Native trackball to NC * Fix crash in simulator mode * Add longLong button press * Get the args right * Adjust Bluetooth Pairing Screen to account for bottom navigation. * Trackball everywhere, and unPhone buttons * Remap visionmaster secondary button to TB_UP * Kill ScanAndSelect * trunk * No longer need the canned messages input filter * All Canned All the time * Fix stm32 compile error regarding inputBroker * Unify tft lineheights (#7033) * Create variable line heights based upon SCREEN_HEIGHT * Refactor textPositions into method -> getTextPositions * Update SharedUIDisplay.h --------- Co-authored-by: Jason P <applewiz@mac.com> * Adjust top distance for larger displays * Adjust icon sizes for larger displays * Fix Paxcounter compile errors after code updates * Pixel wrangling to make larger screens fit better * Alert frame has precedence over banner -- for now * Unify on ALT_BUTTON * Align AM/PM to the digit, not the segment on larger displays * Move some global pin defines into configuration.h * Scaffolding for BMM150 9-axis gyro * Alt button behavior * Don't add the blank GPS frames without HAS_GPS * EVENT_NODEDB_UPDATED has been retired * Clean out LOG_WARN messages from debugging * Add dismiss message function * Minor buttonThread cleanup * Add BMM150 support * Clean up last warning from dev * Simplify bmm150 init return logic * Add option to reply to messages * Add minimal menu upon selecting home screen * Move Messages to slot 2, rename GPS to Position, move variables nearer functional usage in Screen.cpp * Properly dismiss message * T-Deck Trackball press is not user button * Add select on favorite frame to launch cannedMessage DM * Minor wording change * Less capital letters * Fix empty message check, time isn't reliable * drop dead code * Make UIRenderer a static class instead of namespace * Fix the select on favorite * Check if message is empty early and then 'return' * Add kb_found, and show the option to launch freetype if appropriate * Ignore impossible touchscreen touches * Auto scroll fix * Move linebreak after "from" for banners to maximize screen usage. * Center "No messages to show" on Message frame * Start consolidating buzzer behavior * Fixed signed / unsigned warning * Cast second parameter of max() to make some targets happy * Cast kbchar to (char) to make arduino string happy * Shorten the notice of "No messages" * Add buzzer mode chooser * Add regionPicker to Lora icon * Reduce line spacing and reorder Position screen to resolve overlapping issues * Update message titles, fix GPS icons, add Back options * Leftover boops * Remove chirp * Make the region selection dismissable when a region is already set * Add read-aloud functionality on messages w/ esp8266sam * "Last Heard" is a better label * tweak the beep * 5 options * properly tear down freetext upon cancel * de-convelute canned messages just a bit * Correct height of Mail icon in navigation bar * Remove unused warning * Consolidate time methods into TimeFormatters * Oops * Change LoRa Picker Cancel to Back * Tweak selection characters on Banner * Message render not scrolling on 5th line * More fixes for message scrolling * Remove the safety next on text overflow - we found that root cause * Add pin definitions to fix compilation for obscure target * Don't let the touchscreen send unitialized kbchar values * Make virtual KB just a bit quicker * No more double tap, swipe! * Left is left, and Right is right * Update horizontal lightning bolt design * Move from solid to dashed separator for Message Frame * Single emote feature fix * Manually sort overlapping elements for now * Freetext and clearer choices * Fix ESP32 InkHUD builds on the unify-tft branch (#7087) * Remove BaseUI branding * Capitalization is fun * Revert Meshtastic Boot Frame Changes * Add ANZ_433 LoRa region to picker * Update settings.json --------- Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> Co-authored-by: Jason P <applewiz@mac.com> Co-authored-by: todd-herbert <herbert.todd@gmail.com> Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
473
src/graphics/draw/ClockRenderer.cpp
Normal file
473
src/graphics/draw/ClockRenderer.cpp
Normal file
@@ -0,0 +1,473 @@
|
||||
#include "configuration.h"
|
||||
#if HAS_SCREEN
|
||||
#include "ClockRenderer.h"
|
||||
#include "NodeDB.h"
|
||||
#include "UIRenderer.h"
|
||||
#include "configuration.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 "main.h"
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_BLUETOOTH
|
||||
#include "nimble/NimbleBluetooth.h"
|
||||
#endif
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
|
||||
namespace ClockRenderer
|
||||
{
|
||||
|
||||
void 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 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]) {
|
||||
graphics::ClockRenderer::drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight);
|
||||
}
|
||||
|
||||
if (numbers[number][1]) {
|
||||
graphics::ClockRenderer::drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight);
|
||||
}
|
||||
|
||||
if (numbers[number][2]) {
|
||||
graphics::ClockRenderer::drawVerticalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight);
|
||||
}
|
||||
|
||||
if (numbers[number][3]) {
|
||||
graphics::ClockRenderer::drawHorizontalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight);
|
||||
}
|
||||
|
||||
if (numbers[number][4]) {
|
||||
graphics::ClockRenderer::drawVerticalSegment(display, segmentFiveX, segmentFiveY, segmentWidth, segmentHeight);
|
||||
}
|
||||
|
||||
if (numbers[number][5]) {
|
||||
graphics::ClockRenderer::drawVerticalSegment(display, segmentSixX, segmentSixY, segmentWidth, segmentHeight);
|
||||
}
|
||||
|
||||
if (numbers[number][6]) {
|
||||
graphics::ClockRenderer::drawHorizontalSegment(display, segmentSevenX, segmentSevenY, segmentWidth, segmentHeight);
|
||||
}
|
||||
}
|
||||
|
||||
void 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 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 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 drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
display->clear();
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
int line = 1;
|
||||
|
||||
#ifdef T_WATCH_S3
|
||||
if (nimbleBluetooth && nimbleBluetooth->isConnected()) {
|
||||
graphics::ClockRenderer::drawBluetoothConnectedIcon(display, display->getWidth() - 18, y + 2);
|
||||
}
|
||||
|
||||
drawWatchFaceToggleButton(display, display->getWidth() - 36, display->getHeight() - 36,
|
||||
graphics::ClockRenderer::digitalWatchFace, 1);
|
||||
#endif
|
||||
|
||||
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
|
||||
char timeString[16];
|
||||
int hour = 0;
|
||||
int minute = 0;
|
||||
int second = 0;
|
||||
if (rtc_sec > 0) {
|
||||
long hms = rtc_sec % SEC_PER_DAY;
|
||||
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
|
||||
|
||||
hour = hms / SEC_PER_HOUR;
|
||||
minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||
second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
|
||||
}
|
||||
|
||||
bool isPM = hour >= 12;
|
||||
// hour = hour > 12 ? hour - 12 : hour;
|
||||
if (config.display.use_12h_clock) {
|
||||
hour %= 12;
|
||||
if (hour == 0)
|
||||
hour = 12;
|
||||
bool isPM = hour >= 12;
|
||||
snprintf(timeString, sizeof(timeString), "%d:%02d", hour, minute);
|
||||
} else {
|
||||
snprintf(timeString, sizeof(timeString), "%02d:%02d", hour, minute);
|
||||
}
|
||||
|
||||
// Format seconds string
|
||||
char secondString[8];
|
||||
snprintf(secondString, sizeof(secondString), "%02d", second);
|
||||
|
||||
#ifdef T_WATCH_S3
|
||||
float scale = 1.5;
|
||||
#else
|
||||
float scale = 0.75;
|
||||
if (SCREEN_WIDTH > 128) {
|
||||
scale = 1.5;
|
||||
}
|
||||
#endif
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t hourMinuteTextX = (display->getWidth() / 2) - (timeStringWidth / 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_SMALL);
|
||||
int xOffset = (SCREEN_WIDTH > 128) ? 0 : -1;
|
||||
if (hour >= 10) {
|
||||
xOffset += (SCREEN_WIDTH > 128) ? 32 : 18;
|
||||
}
|
||||
int yOffset = (SCREEN_WIDTH > 128) ? 3 : 1;
|
||||
if (config.display.use_12h_clock) {
|
||||
display->drawString(startingHourMinuteTextX + xOffset, (display->getHeight() - hourMinuteTextY) - yOffset - 2,
|
||||
isPM ? "pm" : "am");
|
||||
}
|
||||
#ifndef USE_EINK
|
||||
xOffset = (SCREEN_WIDTH > 128) ? 18 : 10;
|
||||
display->drawString(startingHourMinuteTextX + timeStringWidth - xOffset, (display->getHeight() - hourMinuteTextY) - yOffset,
|
||||
secondString);
|
||||
#endif
|
||||
}
|
||||
|
||||
void drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y)
|
||||
{
|
||||
display->drawFastImage(x, y, 18, 14, bluetoothConnectedIcon);
|
||||
}
|
||||
|
||||
// Draw an analog clock
|
||||
void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
|
||||
graphics::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);
|
||||
}
|
||||
#ifdef T_WATCH_S3
|
||||
if (nimbleBluetooth && nimbleBluetooth->isConnected()) {
|
||||
drawBluetoothConnectedIcon(display, display->getWidth() - 18, y + 2);
|
||||
}
|
||||
#endif
|
||||
drawWatchFaceToggleButton(display, display->getWidth() - 36, display->getHeight() - 36,
|
||||
graphics::ClockRenderer::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);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ClockRenderer
|
||||
|
||||
} // namespace graphics
|
||||
#endif
|
||||
33
src/graphics/draw/ClockRenderer.h
Normal file
33
src/graphics/draw/ClockRenderer.h
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <OLEDDisplay.h>
|
||||
#include <OLEDDisplayUi.h>
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
|
||||
/// Forward declarations
|
||||
class Screen;
|
||||
|
||||
namespace ClockRenderer
|
||||
{
|
||||
// Whether we are showing the digital watch face or the analog one
|
||||
static bool digitalWatchFace = true;
|
||||
|
||||
// Clock frame functions
|
||||
void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
// Segmented display functions
|
||||
void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t number, float scale = 1);
|
||||
void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale = 1);
|
||||
void drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int height);
|
||||
void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int height);
|
||||
|
||||
// UI elements for clock displays
|
||||
void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode = true, float scale = 1);
|
||||
void drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y);
|
||||
|
||||
} // namespace ClockRenderer
|
||||
|
||||
} // namespace graphics
|
||||
140
src/graphics/draw/CompassRenderer.cpp
Normal file
140
src/graphics/draw/CompassRenderer.cpp
Normal file
@@ -0,0 +1,140 @@
|
||||
#include "CompassRenderer.h"
|
||||
#include "NodeDB.h"
|
||||
#include "UIRenderer.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/GeoCoord.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include <cmath>
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
namespace CompassRenderer
|
||||
{
|
||||
|
||||
// Point helper class for compass calculations
|
||||
struct Point {
|
||||
float x, y;
|
||||
Point(float x, float y) : x(x), y(y) {}
|
||||
|
||||
void rotate(float angle)
|
||||
{
|
||||
float cos_a = cos(angle);
|
||||
float sin_a = sin(angle);
|
||||
float new_x = x * cos_a - y * sin_a;
|
||||
float new_y = x * sin_a + y * cos_a;
|
||||
x = new_x;
|
||||
y = new_y;
|
||||
}
|
||||
|
||||
void scale(float factor)
|
||||
{
|
||||
x *= factor;
|
||||
y *= factor;
|
||||
}
|
||||
|
||||
void translate(float dx, float dy)
|
||||
{
|
||||
x += dx;
|
||||
y += dy;
|
||||
}
|
||||
};
|
||||
|
||||
void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading, int16_t radius)
|
||||
{
|
||||
// Show the compass heading (not implemented in original)
|
||||
// This could draw a "N" indicator or north arrow
|
||||
// For now, we'll draw a simple north indicator
|
||||
// const float radius = 17.0f;
|
||||
if (display->width() > 128) {
|
||||
radius += 4;
|
||||
}
|
||||
Point north(0, -radius);
|
||||
north.rotate(-myHeading);
|
||||
north.translate(compassX, compassY);
|
||||
|
||||
display->setFont(FONT_SMALL);
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display->setColor(BLACK);
|
||||
if (display->width() > 128) {
|
||||
display->fillRect(north.x - 8, north.y - 1, display->getStringWidth("N") + 3, FONT_HEIGHT_SMALL - 6);
|
||||
} else {
|
||||
display->fillRect(north.x - 4, north.y - 1, display->getStringWidth("N") + 2, FONT_HEIGHT_SMALL - 6);
|
||||
}
|
||||
display->setColor(WHITE);
|
||||
display->drawString(north.x, north.y - 3, "N");
|
||||
}
|
||||
|
||||
void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian)
|
||||
{
|
||||
Point tip(0.0f, -0.5f), tail(0.0f, 0.35f); // pointing up initially
|
||||
float arrowOffsetX = 0.14f, arrowOffsetY = 0.9f;
|
||||
Point leftArrow(tip.x - arrowOffsetX, tip.y + arrowOffsetY), rightArrow(tip.x + arrowOffsetX, tip.y + arrowOffsetY);
|
||||
|
||||
Point *arrowPoints[] = {&tip, &tail, &leftArrow, &rightArrow};
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
arrowPoints[i]->rotate(headingRadian);
|
||||
arrowPoints[i]->scale(compassDiam * 0.6);
|
||||
arrowPoints[i]->translate(compassX, compassY);
|
||||
}
|
||||
|
||||
#ifdef USE_EINK
|
||||
display->drawTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y);
|
||||
#else
|
||||
display->fillTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y);
|
||||
#endif
|
||||
display->drawTriangle(tip.x, tip.y, leftArrow.x, leftArrow.y, tail.x, tail.y);
|
||||
}
|
||||
|
||||
void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, float bearing)
|
||||
{
|
||||
float radians = bearing * DEG_TO_RAD;
|
||||
|
||||
Point tip(0, -size / 2);
|
||||
Point left(-size / 4, size / 4);
|
||||
Point right(size / 4, size / 4);
|
||||
|
||||
tip.rotate(radians);
|
||||
left.rotate(radians);
|
||||
right.rotate(radians);
|
||||
|
||||
tip.translate(x, y);
|
||||
left.translate(x, y);
|
||||
right.translate(x, y);
|
||||
|
||||
display->drawTriangle(tip.x, tip.y, left.x, left.y, right.x, right.y);
|
||||
}
|
||||
|
||||
float estimatedHeading(double lat, double lon)
|
||||
{
|
||||
// Simple magnetic declination estimation
|
||||
// This is a very basic implementation - the original might be more sophisticated
|
||||
return 0.0f; // Return 0 for now, indicating no heading available
|
||||
}
|
||||
|
||||
uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight)
|
||||
{
|
||||
// Calculate appropriate compass diameter based on display size
|
||||
uint16_t minDimension = (displayWidth < displayHeight) ? displayWidth : displayHeight;
|
||||
uint16_t maxDiam = minDimension / 3; // Use 1/3 of the smaller dimension
|
||||
|
||||
// Ensure minimum and maximum bounds
|
||||
if (maxDiam < 16)
|
||||
maxDiam = 16;
|
||||
if (maxDiam > 64)
|
||||
maxDiam = 64;
|
||||
|
||||
return maxDiam;
|
||||
}
|
||||
|
||||
float calculateBearing(double lat1, double lon1, double lat2, double lon2)
|
||||
{
|
||||
double dLon = (lon2 - lon1) * DEG_TO_RAD;
|
||||
double y = sin(dLon) * cos(lat2 * DEG_TO_RAD);
|
||||
double x = cos(lat1 * DEG_TO_RAD) * sin(lat2 * DEG_TO_RAD) - sin(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * cos(dLon);
|
||||
double bearing = atan2(y, x) * RAD_TO_DEG;
|
||||
return fmod(bearing + 360.0, 360.0);
|
||||
}
|
||||
|
||||
} // namespace CompassRenderer
|
||||
} // namespace graphics
|
||||
36
src/graphics/draw/CompassRenderer.h
Normal file
36
src/graphics/draw/CompassRenderer.h
Normal file
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include "graphics/Screen.h"
|
||||
#include "mesh/generated/meshtastic/mesh.pb.h"
|
||||
#include <OLEDDisplay.h>
|
||||
#include <OLEDDisplayUi.h>
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
|
||||
/// Forward declarations
|
||||
class Screen;
|
||||
|
||||
/**
|
||||
* @brief Compass and navigation drawing functions
|
||||
*
|
||||
* Contains all functions related to drawing compass elements, headings,
|
||||
* navigation arrows, and location-based UI components.
|
||||
*/
|
||||
namespace CompassRenderer
|
||||
{
|
||||
// Compass drawing functions
|
||||
void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading, int16_t radius);
|
||||
void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian);
|
||||
void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, float bearing);
|
||||
|
||||
// Navigation and location functions
|
||||
float estimatedHeading(double lat, double lon);
|
||||
uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight);
|
||||
|
||||
// Utility functions for bearing calculations
|
||||
float calculateBearing(double lat1, double lon1, double lat2, double lon2);
|
||||
|
||||
} // namespace CompassRenderer
|
||||
|
||||
} // namespace graphics
|
||||
634
src/graphics/draw/DebugRenderer.cpp
Normal file
634
src/graphics/draw/DebugRenderer.cpp
Normal file
@@ -0,0 +1,634 @@
|
||||
#include "configuration.h"
|
||||
#if HAS_SCREEN
|
||||
#include "../Screen.h"
|
||||
#include "DebugRenderer.h"
|
||||
#include "FSCommon.h"
|
||||
#include "NodeDB.h"
|
||||
#include "Throttle.h"
|
||||
#include "UIRenderer.h"
|
||||
#include "airtime.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/images.h"
|
||||
#include "main.h"
|
||||
#include "mesh/Channels.h"
|
||||
#include "mesh/generated/meshtastic/deviceonly.pb.h"
|
||||
#include "sleep.h"
|
||||
|
||||
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
|
||||
#include "mesh/wifi/WiFiAPClient.h"
|
||||
#include <WiFi.h>
|
||||
#ifdef ARCH_ESP32
|
||||
#include "mesh/wifi/WiFiAPClient.h"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
#include "modules/StoreForwardModule.h"
|
||||
#endif
|
||||
#include <DisplayFormatters.h>
|
||||
#include <RadioLibInterface.h>
|
||||
#include <target_specific.h>
|
||||
|
||||
using namespace meshtastic;
|
||||
|
||||
// External variables
|
||||
extern graphics::Screen *screen;
|
||||
extern PowerStatus *powerStatus;
|
||||
extern NodeStatus *nodeStatus;
|
||||
extern GPSStatus *gpsStatus;
|
||||
extern Channels channels;
|
||||
extern AirTime *airTime;
|
||||
|
||||
// External functions from Screen.cpp
|
||||
extern bool heartbeat;
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
extern StoreForwardModule *storeForwardModule;
|
||||
#endif
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
namespace DebugRenderer
|
||||
{
|
||||
|
||||
void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
display->setFont(FONT_SMALL);
|
||||
|
||||
// The coordinates define the left starting point of the text
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) {
|
||||
display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL);
|
||||
display->setColor(BLACK);
|
||||
}
|
||||
|
||||
char channelStr[20];
|
||||
snprintf(channelStr, sizeof(channelStr), "#%s", channels.getName(channels.getPrimaryIndex()));
|
||||
|
||||
// Display power status
|
||||
if (powerStatus->getHasBattery()) {
|
||||
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) {
|
||||
UIRenderer::drawBattery(display, x, y + 2, imgBattery, powerStatus);
|
||||
} else {
|
||||
UIRenderer::drawBattery(display, x + 1, y + 3, imgBattery, powerStatus);
|
||||
}
|
||||
} else if (powerStatus->knowsUSB()) {
|
||||
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) {
|
||||
display->drawFastImage(x, y + 2, 16, 8, powerStatus->getHasUSB() ? imgUSB : imgPower);
|
||||
} else {
|
||||
display->drawFastImage(x + 1, y + 3, 16, 8, powerStatus->getHasUSB() ? imgUSB : imgPower);
|
||||
}
|
||||
}
|
||||
// Display nodes status
|
||||
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) {
|
||||
UIRenderer::drawNodes(display, x + (SCREEN_WIDTH * 0.25), y + 2, nodeStatus);
|
||||
} else {
|
||||
UIRenderer::drawNodes(display, x + (SCREEN_WIDTH * 0.25), y + 3, nodeStatus);
|
||||
}
|
||||
#if HAS_GPS
|
||||
// Display GPS status
|
||||
if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
|
||||
UIRenderer::drawGpsPowerStatus(display, x, y + 2, gpsStatus);
|
||||
} else {
|
||||
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) {
|
||||
UIRenderer::drawGps(display, x + (SCREEN_WIDTH * 0.63), y + 2, gpsStatus);
|
||||
} else {
|
||||
UIRenderer::drawGps(display, x + (SCREEN_WIDTH * 0.63), y + 3, gpsStatus);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
display->setColor(WHITE);
|
||||
// Draw the channel name
|
||||
display->drawString(x, y + FONT_HEIGHT_SMALL, channelStr);
|
||||
// Draw our hardware ID to assist with bluetooth pairing. Either prefix with Info or S&F Logo
|
||||
if (moduleConfig.store_forward.enabled) {
|
||||
#ifdef ARCH_ESP32
|
||||
if (!Throttle::isWithinTimespanMs(storeForwardModule->lastHeartbeat,
|
||||
(storeForwardModule->heartbeatInterval * 1200))) { // no heartbeat, overlap a bit
|
||||
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
|
||||
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || ARCH_PORTDUINO) && \
|
||||
!defined(DISPLAY_FORCE_SMALL_FONTS)
|
||||
display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12,
|
||||
8, imgQuestionL1);
|
||||
display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 11 + FONT_HEIGHT_SMALL, 12,
|
||||
8, imgQuestionL2);
|
||||
#else
|
||||
display->drawFastImage(x + SCREEN_WIDTH - 10 - display->getStringWidth(screen->ourId), y + 2 + FONT_HEIGHT_SMALL, 8,
|
||||
8, imgQuestion);
|
||||
#endif
|
||||
} else {
|
||||
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
|
||||
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS)) && \
|
||||
!defined(DISPLAY_FORCE_SMALL_FONTS)
|
||||
display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 16,
|
||||
8, imgSFL1);
|
||||
display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(screen->ourId), y + 11 + FONT_HEIGHT_SMALL, 16,
|
||||
8, imgSFL2);
|
||||
#else
|
||||
display->drawFastImage(x + SCREEN_WIDTH - 13 - display->getStringWidth(screen->ourId), y + 2 + FONT_HEIGHT_SMALL, 11,
|
||||
8, imgSF);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
// TODO: Raspberry Pi supports more than just the one screen size
|
||||
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
|
||||
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || ARCH_PORTDUINO) && \
|
||||
!defined(DISPLAY_FORCE_SMALL_FONTS)
|
||||
display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8,
|
||||
imgInfoL1);
|
||||
display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 11 + FONT_HEIGHT_SMALL, 12, 8,
|
||||
imgInfoL2);
|
||||
#else
|
||||
display->drawFastImage(x + SCREEN_WIDTH - 10 - display->getStringWidth(screen->ourId), y + 2 + FONT_HEIGHT_SMALL, 8, 8,
|
||||
imgInfo);
|
||||
#endif
|
||||
}
|
||||
|
||||
display->drawString(x + SCREEN_WIDTH - display->getStringWidth(screen->ourId), y + FONT_HEIGHT_SMALL, screen->ourId);
|
||||
|
||||
// Draw any log messages
|
||||
display->drawLogBuffer(x, y + (FONT_HEIGHT_SMALL * 2));
|
||||
|
||||
/* Display a heartbeat pixel that blinks every time the frame is redrawn */
|
||||
#ifdef SHOW_REDRAWS
|
||||
if (heartbeat)
|
||||
display->setPixel(0, 0);
|
||||
heartbeat = !heartbeat;
|
||||
#endif
|
||||
}
|
||||
|
||||
// ****************************
|
||||
// * WiFi Screen *
|
||||
// ****************************
|
||||
void drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
#if HAS_WIFI && !defined(ARCH_PORTDUINO)
|
||||
display->clear();
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
int line = 1;
|
||||
|
||||
// === Set Title
|
||||
const char *titleStr = "WiFi";
|
||||
|
||||
// === Header ===
|
||||
graphics::drawCommonHeader(display, x, y, titleStr);
|
||||
|
||||
const char *wifiName = config.network.wifi_ssid;
|
||||
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
display->drawString(x, getTextPositions(display)[line++], "WiFi: Not Connected");
|
||||
} else {
|
||||
display->drawString(x, getTextPositions(display)[line++], "WiFi: Connected");
|
||||
|
||||
char rssiStr[32];
|
||||
snprintf(rssiStr, sizeof(rssiStr), "RSSI: %d", WiFi.RSSI());
|
||||
display->drawString(x, getTextPositions(display)[line++], rssiStr);
|
||||
}
|
||||
|
||||
/*
|
||||
- WL_CONNECTED: assigned when connected to a WiFi network;
|
||||
- WL_NO_SSID_AVAIL: assigned when no SSID are available;
|
||||
- WL_CONNECT_FAILED: assigned when the connection fails for all the attempts;
|
||||
- WL_CONNECTION_LOST: assigned when the connection is lost;
|
||||
- WL_DISCONNECTED: assigned when disconnected from a network;
|
||||
- WL_IDLE_STATUS: it is a temporary status assigned when WiFi.begin() is called and remains active until the number of
|
||||
attempts expires (resulting in WL_CONNECT_FAILED) or a connection is established (resulting in WL_CONNECTED);
|
||||
- WL_SCAN_COMPLETED: assigned when the scan networks is completed;
|
||||
- WL_NO_SHIELD: assigned when no WiFi shield is present;
|
||||
|
||||
*/
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
char ipStr[64];
|
||||
snprintf(ipStr, sizeof(ipStr), "IP: %s", WiFi.localIP().toString().c_str());
|
||||
display->drawString(x, getTextPositions(display)[line++], ipStr);
|
||||
} else if (WiFi.status() == WL_NO_SSID_AVAIL) {
|
||||
display->drawString(x, getTextPositions(display)[line++], "SSID Not Found");
|
||||
} else if (WiFi.status() == WL_CONNECTION_LOST) {
|
||||
display->drawString(x, getTextPositions(display)[line++], "Connection Lost");
|
||||
} else if (WiFi.status() == WL_IDLE_STATUS) {
|
||||
display->drawString(x, getTextPositions(display)[line++], "Idle ... Reconnecting");
|
||||
} else if (WiFi.status() == WL_CONNECT_FAILED) {
|
||||
display->drawString(x, getTextPositions(display)[line++], "Connection Failed");
|
||||
}
|
||||
#ifdef ARCH_ESP32
|
||||
else {
|
||||
// Codes:
|
||||
// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/wifi.html#wi-fi-reason-code
|
||||
display->drawString(x, getTextPositions(display)[line++],
|
||||
WiFi.disconnectReasonName(static_cast<wifi_err_reason_t>(getWifiDisconnectReason())));
|
||||
}
|
||||
#else
|
||||
else {
|
||||
char statusStr[32];
|
||||
snprintf(statusStr, sizeof(statusStr), "Unknown status: %d", WiFi.status());
|
||||
display->drawString(x, getTextPositions(display)[line++], statusStr);
|
||||
}
|
||||
#endif
|
||||
|
||||
char ssidStr[64];
|
||||
snprintf(ssidStr, sizeof(ssidStr), "SSID: %s", wifiName);
|
||||
display->drawString(x, getTextPositions(display)[line++], ssidStr);
|
||||
|
||||
display->drawString(x, getTextPositions(display)[line++], "URL: http://meshtastic.local");
|
||||
|
||||
/* Display a heartbeat pixel that blinks every time the frame is redrawn */
|
||||
#ifdef SHOW_REDRAWS
|
||||
if (heartbeat)
|
||||
display->setPixel(0, 0);
|
||||
heartbeat = !heartbeat;
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
display->setFont(FONT_SMALL);
|
||||
|
||||
// The coordinates define the left starting point of the text
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
|
||||
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) {
|
||||
display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL);
|
||||
display->setColor(BLACK);
|
||||
}
|
||||
|
||||
char batStr[20];
|
||||
if (powerStatus->getHasBattery()) {
|
||||
int batV = powerStatus->getBatteryVoltageMv() / 1000;
|
||||
int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10;
|
||||
|
||||
snprintf(batStr, sizeof(batStr), "B %01d.%02dV %3d%% %c%c", batV, batCv, powerStatus->getBatteryChargePercent(),
|
||||
powerStatus->getIsCharging() ? '+' : ' ', powerStatus->getHasUSB() ? 'U' : ' ');
|
||||
|
||||
// Line 1
|
||||
display->drawString(x, y, batStr);
|
||||
if (config.display.heading_bold)
|
||||
display->drawString(x + 1, y, batStr);
|
||||
} else {
|
||||
// Line 1
|
||||
display->drawString(x, y, "USB");
|
||||
if (config.display.heading_bold)
|
||||
display->drawString(x + 1, y, "USB");
|
||||
}
|
||||
|
||||
// auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, true);
|
||||
|
||||
// display->drawString(x + SCREEN_WIDTH - display->getStringWidth(mode), y, mode);
|
||||
// if (config.display.heading_bold)
|
||||
// display->drawString(x + SCREEN_WIDTH - display->getStringWidth(mode) - 1, y, mode);
|
||||
|
||||
uint32_t currentMillis = millis();
|
||||
uint32_t seconds = currentMillis / 1000;
|
||||
uint32_t minutes = seconds / 60;
|
||||
uint32_t hours = minutes / 60;
|
||||
uint32_t days = hours / 24;
|
||||
// currentMillis %= 1000;
|
||||
// seconds %= 60;
|
||||
// minutes %= 60;
|
||||
// hours %= 24;
|
||||
|
||||
// Show uptime as days, hours, minutes OR seconds
|
||||
std::string uptime = UIRenderer::drawTimeDelta(days, hours, minutes, seconds);
|
||||
|
||||
// Line 1 (Still)
|
||||
display->drawString(x + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str());
|
||||
if (config.display.heading_bold)
|
||||
display->drawString(x - 1 + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str());
|
||||
|
||||
display->setColor(WHITE);
|
||||
|
||||
// Setup string to assemble analogClock string
|
||||
std::string analogClock = "";
|
||||
|
||||
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
|
||||
if (rtc_sec > 0) {
|
||||
long hms = rtc_sec % SEC_PER_DAY;
|
||||
// hms += tz.tz_dsttime * SEC_PER_HOUR;
|
||||
// hms -= tz.tz_minuteswest * SEC_PER_MIN;
|
||||
// mod `hms` to ensure in positive range of [0...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 min = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||
int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
|
||||
|
||||
char timebuf[12];
|
||||
|
||||
if (config.display.use_12h_clock) {
|
||||
std::string meridiem = "am";
|
||||
if (hour >= 12) {
|
||||
if (hour > 12)
|
||||
hour -= 12;
|
||||
meridiem = "pm";
|
||||
}
|
||||
if (hour == 00) {
|
||||
hour = 12;
|
||||
}
|
||||
snprintf(timebuf, sizeof(timebuf), "%d:%02d:%02d%s", hour, min, sec, meridiem.c_str());
|
||||
} else {
|
||||
snprintf(timebuf, sizeof(timebuf), "%02d:%02d:%02d", hour, min, sec);
|
||||
}
|
||||
analogClock += timebuf;
|
||||
}
|
||||
|
||||
// Line 2
|
||||
display->drawString(x, y + FONT_HEIGHT_SMALL * 1, analogClock.c_str());
|
||||
|
||||
// Display Channel Utilization
|
||||
char chUtil[13];
|
||||
snprintf(chUtil, sizeof(chUtil), "ChUtil %2.0f%%", airTime->channelUtilizationPercent());
|
||||
display->drawString(x + SCREEN_WIDTH - display->getStringWidth(chUtil), y + FONT_HEIGHT_SMALL * 1, chUtil);
|
||||
|
||||
#if HAS_GPS
|
||||
if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
|
||||
// Line 3
|
||||
if (config.display.gps_format !=
|
||||
meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DMS) // if DMS then don't draw altitude
|
||||
UIRenderer::drawGpsAltitude(display, x, y + FONT_HEIGHT_SMALL * 2, gpsStatus);
|
||||
|
||||
// Line 4
|
||||
UIRenderer::drawGpsCoordinates(display, x, y + FONT_HEIGHT_SMALL * 3, gpsStatus);
|
||||
} else {
|
||||
UIRenderer::drawGpsPowerStatus(display, x, y + FONT_HEIGHT_SMALL * 2, gpsStatus);
|
||||
}
|
||||
#endif
|
||||
/* Display a heartbeat pixel that blinks every time the frame is redrawn */
|
||||
#ifdef SHOW_REDRAWS
|
||||
if (heartbeat)
|
||||
display->setPixel(0, 0);
|
||||
heartbeat = !heartbeat;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Trampoline functions for DebugInfo class access
|
||||
void drawDebugInfoTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
drawFrame(display, state, x, y);
|
||||
}
|
||||
|
||||
void drawDebugInfoSettingsTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
drawFrameSettings(display, state, x, y);
|
||||
}
|
||||
|
||||
void drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
drawFrameWiFi(display, state, x, y);
|
||||
}
|
||||
|
||||
// ****************************
|
||||
// * LoRa Focused Screen *
|
||||
// ****************************
|
||||
void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
display->clear();
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
int line = 1;
|
||||
|
||||
// === Set Title
|
||||
const char *titleStr = (SCREEN_WIDTH > 128) ? "LoRa Info" : "LoRa";
|
||||
|
||||
// === Header ===
|
||||
graphics::drawCommonHeader(display, x, y, titleStr);
|
||||
|
||||
// === First Row: Region / BLE Name ===
|
||||
graphics::UIRenderer::drawNodes(display, x, getTextPositions(display)[line] + 2, nodeStatus, 0, true, "");
|
||||
|
||||
uint8_t dmac[6];
|
||||
char shortnameble[35];
|
||||
getMacAddr(dmac);
|
||||
snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]);
|
||||
snprintf(shortnameble, sizeof(shortnameble), "BLE: %s", screen->ourId);
|
||||
int textWidth = display->getStringWidth(shortnameble);
|
||||
int nameX = (SCREEN_WIDTH - textWidth);
|
||||
display->drawString(nameX, getTextPositions(display)[line++], shortnameble);
|
||||
|
||||
// === Second Row: Radio Preset ===
|
||||
auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false);
|
||||
char regionradiopreset[25];
|
||||
const char *region = myRegion ? myRegion->name : NULL;
|
||||
if (region != nullptr) {
|
||||
snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode);
|
||||
}
|
||||
textWidth = display->getStringWidth(regionradiopreset);
|
||||
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
||||
display->drawString(nameX, getTextPositions(display)[line++], regionradiopreset);
|
||||
|
||||
// === Third Row: Frequency / ChanNum ===
|
||||
char frequencyslot[35];
|
||||
char freqStr[16];
|
||||
float freq = RadioLibInterface::instance->getFreq();
|
||||
snprintf(freqStr, sizeof(freqStr), "%.3f", freq);
|
||||
if (config.lora.channel_num == 0) {
|
||||
snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %smhz", freqStr);
|
||||
} else {
|
||||
snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %smhz (%d)", freqStr, config.lora.channel_num);
|
||||
}
|
||||
size_t len = strlen(frequencyslot);
|
||||
if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) {
|
||||
frequencyslot[len - 4] = '\0'; // Remove the last three characters
|
||||
}
|
||||
textWidth = display->getStringWidth(frequencyslot);
|
||||
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
||||
display->drawString(nameX, getTextPositions(display)[line++], frequencyslot);
|
||||
|
||||
// === Fourth Row: Channel Utilization ===
|
||||
const char *chUtil = "ChUtil:";
|
||||
char chUtilPercentage[10];
|
||||
snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent());
|
||||
|
||||
int chUtil_x = (SCREEN_WIDTH > 128) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5;
|
||||
int chUtil_y = getTextPositions(display)[line] + 3;
|
||||
|
||||
int chutil_bar_width = (SCREEN_WIDTH > 128) ? 100 : 50;
|
||||
int chutil_bar_height = (SCREEN_WIDTH > 128) ? 12 : 7;
|
||||
int extraoffset = (SCREEN_WIDTH > 128) ? 6 : 3;
|
||||
int chutil_percent = airTime->channelUtilizationPercent();
|
||||
|
||||
int centerofscreen = SCREEN_WIDTH / 2;
|
||||
int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2;
|
||||
int starting_position = centerofscreen - total_line_content_width;
|
||||
|
||||
display->drawString(starting_position, getTextPositions(display)[line++], chUtil);
|
||||
|
||||
// Force 56% or higher to show a full 100% bar, text would still show related percent.
|
||||
if (chutil_percent >= 61) {
|
||||
chutil_percent = 100;
|
||||
}
|
||||
|
||||
// Weighting for nonlinear segments
|
||||
float milestone1 = 25;
|
||||
float milestone2 = 40;
|
||||
float weight1 = 0.45; // Weight for 0–25%
|
||||
float weight2 = 0.35; // Weight for 25–40%
|
||||
float weight3 = 0.20; // Weight for 40–100%
|
||||
float totalWeight = weight1 + weight2 + weight3;
|
||||
|
||||
int seg1 = chutil_bar_width * (weight1 / totalWeight);
|
||||
int seg2 = chutil_bar_width * (weight2 / totalWeight);
|
||||
int seg3 = chutil_bar_width * (weight3 / totalWeight);
|
||||
|
||||
int fillRight = 0;
|
||||
|
||||
if (chutil_percent <= milestone1) {
|
||||
fillRight = (seg1 * (chutil_percent / milestone1));
|
||||
} else if (chutil_percent <= milestone2) {
|
||||
fillRight = seg1 + (seg2 * ((chutil_percent - milestone1) / (milestone2 - milestone1)));
|
||||
} else {
|
||||
fillRight = seg1 + seg2 + (seg3 * ((chutil_percent - milestone2) / (100 - milestone2)));
|
||||
}
|
||||
|
||||
// Draw outline
|
||||
display->drawRect(starting_position + chUtil_x, chUtil_y, chutil_bar_width, chutil_bar_height);
|
||||
|
||||
// Fill progress
|
||||
if (fillRight > 0) {
|
||||
display->fillRect(starting_position + chUtil_x, chUtil_y, fillRight, chutil_bar_height);
|
||||
}
|
||||
|
||||
display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[4],
|
||||
chUtilPercentage);
|
||||
}
|
||||
|
||||
// ****************************
|
||||
// * Memory Screen *
|
||||
// ****************************
|
||||
void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
display->clear();
|
||||
display->setFont(FONT_SMALL);
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
|
||||
// === Set Title
|
||||
const char *titleStr = "System";
|
||||
|
||||
// === Header ===
|
||||
graphics::drawCommonHeader(display, x, y, titleStr);
|
||||
|
||||
// === Layout ===
|
||||
int line = 1;
|
||||
const int barHeight = 6;
|
||||
const int labelX = x;
|
||||
const int barsOffset = (SCREEN_WIDTH > 128) ? 24 : 0;
|
||||
const int barX = x + 40 + barsOffset;
|
||||
|
||||
auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) {
|
||||
if (total == 0)
|
||||
return;
|
||||
|
||||
int percent = (used * 100) / total;
|
||||
|
||||
char combinedStr[24];
|
||||
if (SCREEN_WIDTH > 128) {
|
||||
snprintf(combinedStr, sizeof(combinedStr), "%s%3d%% %u/%uKB", (percent > 80) ? "! " : "", percent, used / 1024,
|
||||
total / 1024);
|
||||
} else {
|
||||
snprintf(combinedStr, sizeof(combinedStr), "%s%3d%%", (percent > 80) ? "! " : "", percent);
|
||||
}
|
||||
|
||||
int textWidth = display->getStringWidth(combinedStr);
|
||||
int adjustedBarWidth = SCREEN_WIDTH - barX - textWidth - 6;
|
||||
if (adjustedBarWidth < 10)
|
||||
adjustedBarWidth = 10;
|
||||
|
||||
int fillWidth = (used * adjustedBarWidth) / total;
|
||||
|
||||
// Label
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->drawString(labelX, getTextPositions(display)[line], label);
|
||||
|
||||
// Bar
|
||||
int barY = getTextPositions(display)[line] + (FONT_HEIGHT_SMALL - barHeight) / 2;
|
||||
display->setColor(WHITE);
|
||||
display->drawRect(barX, barY, adjustedBarWidth, barHeight);
|
||||
|
||||
display->fillRect(barX, barY, fillWidth, barHeight);
|
||||
display->setColor(WHITE);
|
||||
|
||||
// Value string
|
||||
display->setTextAlignment(TEXT_ALIGN_RIGHT);
|
||||
display->drawString(SCREEN_WIDTH - 2, getTextPositions(display)[line], combinedStr);
|
||||
};
|
||||
|
||||
// === Memory values ===
|
||||
uint32_t heapUsed = memGet.getHeapSize() - memGet.getFreeHeap();
|
||||
uint32_t heapTotal = memGet.getHeapSize();
|
||||
|
||||
uint32_t psramUsed = memGet.getPsramSize() - memGet.getFreePsram();
|
||||
uint32_t psramTotal = memGet.getPsramSize();
|
||||
|
||||
uint32_t flashUsed = 0, flashTotal = 0;
|
||||
#ifdef ESP32
|
||||
flashUsed = FSCom.usedBytes();
|
||||
flashTotal = FSCom.totalBytes();
|
||||
#endif
|
||||
|
||||
uint32_t sdUsed = 0, sdTotal = 0;
|
||||
bool hasSD = false;
|
||||
/*
|
||||
#ifdef HAS_SDCARD
|
||||
hasSD = SD.cardType() != CARD_NONE;
|
||||
if (hasSD) {
|
||||
sdUsed = SD.usedBytes();
|
||||
sdTotal = SD.totalBytes();
|
||||
}
|
||||
#endif
|
||||
*/
|
||||
// === Draw memory rows
|
||||
drawUsageRow("Heap:", heapUsed, heapTotal, true);
|
||||
#ifdef ESP32
|
||||
if (psramUsed > 0) {
|
||||
line += 1;
|
||||
drawUsageRow("PSRAM:", psramUsed, psramTotal);
|
||||
}
|
||||
if (flashTotal > 0) {
|
||||
line += 1;
|
||||
drawUsageRow("Flash:", flashUsed, flashTotal);
|
||||
}
|
||||
#endif
|
||||
if (hasSD && sdTotal > 0) {
|
||||
line += 1;
|
||||
drawUsageRow("SD:", sdUsed, sdTotal);
|
||||
}
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
// System Uptime
|
||||
if (line < 2) {
|
||||
line += 1;
|
||||
}
|
||||
line += 1;
|
||||
char appversionstr[35];
|
||||
snprintf(appversionstr, sizeof(appversionstr), "Ver.: %s", optstr(APP_VERSION));
|
||||
int textWidth = display->getStringWidth(appversionstr);
|
||||
int nameX = (SCREEN_WIDTH - textWidth) / 2;
|
||||
display->drawString(nameX, getTextPositions(display)[line], appversionstr);
|
||||
|
||||
if (SCREEN_HEIGHT > 64 || (SCREEN_HEIGHT <= 64 && line < 4)) { // Only show uptime if the screen can show it
|
||||
line += 1;
|
||||
char uptimeStr[32] = "";
|
||||
uint32_t uptime = millis() / 1000;
|
||||
uint32_t days = uptime / 86400;
|
||||
uint32_t hours = (uptime % 86400) / 3600;
|
||||
uint32_t mins = (uptime % 3600) / 60;
|
||||
// Show as "Up: 2d 3h", "Up: 5h 14m", or "Up: 37m"
|
||||
if (days)
|
||||
snprintf(uptimeStr, sizeof(uptimeStr), " Up: %ud %uh", days, hours);
|
||||
else if (hours)
|
||||
snprintf(uptimeStr, sizeof(uptimeStr), " Up: %uh %um", hours, mins);
|
||||
else
|
||||
snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %um", mins);
|
||||
textWidth = display->getStringWidth(uptimeStr);
|
||||
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
||||
display->drawString(nameX, getTextPositions(display)[line], uptimeStr);
|
||||
}
|
||||
}
|
||||
} // namespace DebugRenderer
|
||||
} // namespace graphics
|
||||
#endif
|
||||
38
src/graphics/draw/DebugRenderer.h
Normal file
38
src/graphics/draw/DebugRenderer.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <OLEDDisplay.h>
|
||||
#include <OLEDDisplayUi.h>
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
|
||||
/// Forward declarations
|
||||
class Screen;
|
||||
class DebugInfo;
|
||||
|
||||
/**
|
||||
* @brief Debug and diagnostic drawing functions
|
||||
*
|
||||
* Contains all functions related to drawing debug information,
|
||||
* WiFi status, settings screens, and diagnostic data.
|
||||
*/
|
||||
namespace DebugRenderer
|
||||
{
|
||||
// Debug frame functions
|
||||
void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
void drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
// Trampoline functions for framework callback compatibility
|
||||
void drawDebugInfoTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
void drawDebugInfoSettingsTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
void drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
// LoRa information display
|
||||
void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
// Memory screen display
|
||||
void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
} // namespace DebugRenderer
|
||||
|
||||
} // namespace graphics
|
||||
38
src/graphics/draw/DrawRenderers.h
Normal file
38
src/graphics/draw/DrawRenderers.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* @brief Master include file for all Screen draw renderers
|
||||
*
|
||||
* This file includes all the individual renderer headers to provide
|
||||
* a convenient single include for accessing all draw functions.
|
||||
*/
|
||||
|
||||
#include "graphics/draw/ClockRenderer.h"
|
||||
#include "graphics/draw/CompassRenderer.h"
|
||||
#include "graphics/draw/DebugRenderer.h"
|
||||
#include "graphics/draw/NodeListRenderer.h"
|
||||
#include "graphics/draw/ScreenRenderer.h"
|
||||
#include "graphics/draw/UIRenderer.h"
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Collection of all draw renderers
|
||||
*
|
||||
* This namespace provides access to all the specialized rendering
|
||||
* functions organized by category.
|
||||
*/
|
||||
namespace DrawRenderers
|
||||
{
|
||||
// Re-export all renderer namespaces for convenience
|
||||
using namespace ClockRenderer;
|
||||
using namespace CompassRenderer;
|
||||
using namespace DebugRenderer;
|
||||
using namespace NodeListRenderer;
|
||||
using namespace ScreenRenderer;
|
||||
using namespace UIRenderer;
|
||||
|
||||
} // namespace DrawRenderers
|
||||
|
||||
} // namespace graphics
|
||||
392
src/graphics/draw/MessageRenderer.cpp
Normal file
392
src/graphics/draw/MessageRenderer.cpp
Normal file
@@ -0,0 +1,392 @@
|
||||
/*
|
||||
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 "configuration.h"
|
||||
#if HAS_SCREEN
|
||||
#include "MessageRenderer.h"
|
||||
|
||||
// Core includes
|
||||
#include "NodeDB.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/emotes.h"
|
||||
#include "main.h"
|
||||
#include "meshUtils.h"
|
||||
|
||||
// Additional includes for UI rendering
|
||||
#include "UIRenderer.h"
|
||||
#include "graphics/TimeFormatters.h"
|
||||
|
||||
// Additional includes for dependencies
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// External declarations
|
||||
extern bool hasUnreadMessage;
|
||||
extern meshtastic_DeviceState devicestate;
|
||||
|
||||
using graphics::Emote;
|
||||
using graphics::emotes;
|
||||
using graphics::numEmotes;
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
namespace MessageRenderer
|
||||
{
|
||||
|
||||
void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount)
|
||||
{
|
||||
int cursorX = x;
|
||||
const int fontHeight = FONT_HEIGHT_SMALL;
|
||||
|
||||
// === Step 1: Find tallest emote in the line ===
|
||||
int maxIconHeight = fontHeight;
|
||||
for (size_t i = 0; i < line.length();) {
|
||||
bool matched = false;
|
||||
for (int e = 0; e < emoteCount; ++e) {
|
||||
size_t emojiLen = strlen(emotes[e].label);
|
||||
if (line.compare(i, emojiLen, emotes[e].label) == 0) {
|
||||
if (emotes[e].height > maxIconHeight)
|
||||
maxIconHeight = emotes[e].height;
|
||||
i += emojiLen;
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matched) {
|
||||
uint8_t c = static_cast<uint8_t>(line[i]);
|
||||
if ((c & 0xE0) == 0xC0)
|
||||
i += 2;
|
||||
else if ((c & 0xF0) == 0xE0)
|
||||
i += 3;
|
||||
else if ((c & 0xF8) == 0xF0)
|
||||
i += 4;
|
||||
else
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// === Step 2: Baseline alignment ===
|
||||
int lineHeight = std::max(fontHeight, maxIconHeight);
|
||||
int baselineOffset = (lineHeight - fontHeight) / 2;
|
||||
int fontY = y + baselineOffset;
|
||||
int fontMidline = fontY + fontHeight / 2;
|
||||
|
||||
// === Step 3: Render line in segments ===
|
||||
size_t i = 0;
|
||||
bool inBold = false;
|
||||
|
||||
while (i < line.length()) {
|
||||
// Check for ** start/end for faux bold
|
||||
if (line.compare(i, 2, "**") == 0) {
|
||||
inBold = !inBold;
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Look ahead for the next emote match
|
||||
size_t nextEmotePos = std::string::npos;
|
||||
const Emote *matchedEmote = nullptr;
|
||||
size_t emojiLen = 0;
|
||||
|
||||
for (int e = 0; e < emoteCount; ++e) {
|
||||
size_t pos = line.find(emotes[e].label, i);
|
||||
if (pos != std::string::npos && (nextEmotePos == std::string::npos || pos < nextEmotePos)) {
|
||||
nextEmotePos = pos;
|
||||
matchedEmote = &emotes[e];
|
||||
emojiLen = strlen(emotes[e].label);
|
||||
}
|
||||
}
|
||||
|
||||
// Render normal text segment up to the emote or bold toggle
|
||||
size_t nextControl = std::min(nextEmotePos, line.find("**", i));
|
||||
if (nextControl == std::string::npos)
|
||||
nextControl = line.length();
|
||||
|
||||
if (nextControl > i) {
|
||||
std::string textChunk = line.substr(i, nextControl - i);
|
||||
if (inBold) {
|
||||
// Faux bold: draw twice, offset by 1px
|
||||
display->drawString(cursorX + 1, fontY, textChunk.c_str());
|
||||
}
|
||||
display->drawString(cursorX, fontY, textChunk.c_str());
|
||||
cursorX += display->getStringWidth(textChunk.c_str());
|
||||
i = nextControl;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Render the emote (if found)
|
||||
if (matchedEmote && i == nextEmotePos) {
|
||||
int iconY = fontMidline - matchedEmote->height / 2 - 1;
|
||||
display->drawXbm(cursorX, iconY, matchedEmote->width, matchedEmote->height, matchedEmote->bitmap);
|
||||
cursorX += matchedEmote->width + 1;
|
||||
i += emojiLen;
|
||||
} else {
|
||||
// No more emotes — render the rest of the line
|
||||
std::string remaining = line.substr(i);
|
||||
if (inBold) {
|
||||
display->drawString(cursorX + 1, fontY, remaining.c_str());
|
||||
}
|
||||
display->drawString(cursorX, fontY, remaining.c_str());
|
||||
cursorX += display->getStringWidth(remaining.c_str());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
// Clear the unread message indicator when viewing the message
|
||||
hasUnreadMessage = false;
|
||||
|
||||
const meshtastic_MeshPacket &mp = devicestate.rx_text_message;
|
||||
const char *msg = reinterpret_cast<const char *>(mp.decoded.payload.bytes);
|
||||
|
||||
display->clear();
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
|
||||
const int navHeight = FONT_HEIGHT_SMALL;
|
||||
const int scrollBottom = SCREEN_HEIGHT - navHeight;
|
||||
const int usableHeight = scrollBottom;
|
||||
const int textWidth = SCREEN_WIDTH;
|
||||
|
||||
bool isInverted = (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED);
|
||||
bool isBold = config.display.heading_bold;
|
||||
|
||||
// === Set Title
|
||||
const char *titleStr = "Messages";
|
||||
|
||||
// Check if we have more than an empty message to show
|
||||
char messageBuf[237];
|
||||
snprintf(messageBuf, sizeof(messageBuf), "%s", msg);
|
||||
if (strlen(messageBuf) == 0) {
|
||||
// === Header ===
|
||||
graphics::drawCommonHeader(display, x, y, titleStr);
|
||||
const char *messageString = "No messages";
|
||||
int center_text = (SCREEN_WIDTH / 2) - (display->getStringWidth(messageString) / 2);
|
||||
display->drawString(center_text, getTextPositions(display)[2], messageString);
|
||||
return;
|
||||
}
|
||||
|
||||
// === Header Construction ===
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp));
|
||||
char headerStr[80];
|
||||
const char *sender = "???";
|
||||
if (node && node->has_user) {
|
||||
if (SCREEN_WIDTH >= 200 && strlen(node->user.long_name) > 0) {
|
||||
sender = node->user.long_name;
|
||||
} else {
|
||||
sender = node->user.short_name;
|
||||
}
|
||||
}
|
||||
uint32_t seconds = sinceReceived(&mp), minutes = seconds / 60, hours = minutes / 60, days = hours / 24;
|
||||
uint8_t timestampHours, timestampMinutes;
|
||||
int32_t daysAgo;
|
||||
bool useTimestamp = deltaToTimestamp(seconds, ×tampHours, ×tampMinutes, &daysAgo);
|
||||
|
||||
if (useTimestamp && minutes >= 15 && daysAgo == 0) {
|
||||
std::string prefix = (daysAgo == 1 && SCREEN_WIDTH >= 200) ? "Yesterday" : "At";
|
||||
if (config.display.use_12h_clock) {
|
||||
bool isPM = timestampHours >= 12;
|
||||
timestampHours = timestampHours % 12;
|
||||
if (timestampHours == 0)
|
||||
timestampHours = 12;
|
||||
snprintf(headerStr, sizeof(headerStr), "%s %d:%02d%s from %s", prefix.c_str(), timestampHours, timestampMinutes,
|
||||
isPM ? "p" : "a", sender);
|
||||
} else {
|
||||
snprintf(headerStr, sizeof(headerStr), "%s %d:%02d from %s", prefix.c_str(), timestampHours, timestampMinutes,
|
||||
sender);
|
||||
}
|
||||
} else {
|
||||
snprintf(headerStr, sizeof(headerStr), "%s ago from %s", UIRenderer::drawTimeDelta(days, hours, minutes, seconds).c_str(),
|
||||
sender);
|
||||
}
|
||||
|
||||
#ifndef EXCLUDE_EMOJI
|
||||
// === Bounce animation setup ===
|
||||
static uint32_t lastBounceTime = 0;
|
||||
static int bounceY = 0;
|
||||
const int bounceRange = 2; // Max pixels to bounce up/down
|
||||
const int bounceInterval = 10; // How quickly to change bounce direction (ms)
|
||||
|
||||
uint32_t now = millis();
|
||||
if (now - lastBounceTime >= bounceInterval) {
|
||||
lastBounceTime = now;
|
||||
bounceY = (bounceY + 1) % (bounceRange * 2);
|
||||
}
|
||||
for (int i = 0; i < numEmotes; ++i) {
|
||||
const Emote &e = emotes[i];
|
||||
if (strcmp(msg, e.label) == 0) {
|
||||
int headerY = getTextPositions(display)[1]; // same as scrolling header line
|
||||
display->drawString(x + 3, headerY, headerStr);
|
||||
if (isInverted && isBold)
|
||||
display->drawString(x + 4, headerY, headerStr);
|
||||
|
||||
// Draw separator (same as scroll version)
|
||||
for (int separatorX = 0; separatorX <= (display->getStringWidth(headerStr) + 3); separatorX += 2) {
|
||||
display->setPixel(separatorX, headerY + ((SCREEN_WIDTH > 128) ? 19 : 13));
|
||||
}
|
||||
|
||||
// Center the emote below the header line + separator + nav
|
||||
int remainingHeight = SCREEN_HEIGHT - (headerY + FONT_HEIGHT_SMALL) - navHeight;
|
||||
int emoteY = headerY + FONT_HEIGHT_SMALL + (remainingHeight - e.height) / 2 + bounceY - bounceRange;
|
||||
display->drawXbm((SCREEN_WIDTH - e.width) / 2, emoteY, e.width, e.height, e.bitmap);
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// === Word-wrap and build line list ===
|
||||
std::vector<std::string> lines;
|
||||
lines.push_back(std::string(headerStr)); // Header line is always first
|
||||
|
||||
std::string line, word;
|
||||
for (int i = 0; messageBuf[i]; ++i) {
|
||||
char ch = messageBuf[i];
|
||||
if (ch == '\n') {
|
||||
if (!word.empty())
|
||||
line += word;
|
||||
if (!line.empty())
|
||||
lines.push_back(line);
|
||||
line.clear();
|
||||
word.clear();
|
||||
} else if (ch == ' ') {
|
||||
line += word + ' ';
|
||||
word.clear();
|
||||
} else {
|
||||
word += ch;
|
||||
std::string test = line + word;
|
||||
if (display->getStringWidth(test.c_str()) > textWidth) {
|
||||
if (!line.empty())
|
||||
lines.push_back(line);
|
||||
line = word;
|
||||
word.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!word.empty())
|
||||
line += word;
|
||||
if (!line.empty())
|
||||
lines.push_back(line);
|
||||
|
||||
// === Scrolling logic ===
|
||||
std::vector<int> rowHeights;
|
||||
|
||||
for (const auto &_line : lines) {
|
||||
int lineHeight = FONT_HEIGHT_SMALL;
|
||||
bool hasEmote = false;
|
||||
|
||||
for (int i = 0; i < numEmotes; ++i) {
|
||||
const Emote &e = emotes[i];
|
||||
if (_line.find(e.label) != std::string::npos) {
|
||||
lineHeight = std::max(lineHeight, e.height);
|
||||
hasEmote = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply tighter spacing if no emotes on this line
|
||||
if (!hasEmote) {
|
||||
lineHeight -= 2; // reduce by 2px for tighter spacing
|
||||
if (lineHeight < 8)
|
||||
lineHeight = 8; // minimum safety
|
||||
}
|
||||
|
||||
rowHeights.push_back(lineHeight);
|
||||
}
|
||||
int totalHeight = 0;
|
||||
for (size_t i = 1; i < rowHeights.size(); ++i) {
|
||||
totalHeight += rowHeights[i];
|
||||
}
|
||||
int usableScrollHeight = usableHeight - rowHeights[0]; // remove header height
|
||||
int scrollStop = std::max(0, totalHeight - usableScrollHeight + rowHeights.back());
|
||||
|
||||
static float scrollY = 0.0f;
|
||||
static uint32_t lastTime = 0, scrollStartDelay = 0, pauseStart = 0;
|
||||
static bool waitingToReset = false, scrollStarted = false;
|
||||
|
||||
// === Smooth scrolling adjustment ===
|
||||
// You can tweak this divisor to change how smooth it scrolls.
|
||||
// Lower = smoother, but can feel slow.
|
||||
float delta = (now - lastTime) / 400.0f;
|
||||
lastTime = now;
|
||||
|
||||
const float scrollSpeed = 2.0f; // pixels per second
|
||||
|
||||
// Delay scrolling start by 2 seconds
|
||||
if (scrollStartDelay == 0)
|
||||
scrollStartDelay = now;
|
||||
if (!scrollStarted && now - scrollStartDelay > 2000)
|
||||
scrollStarted = true;
|
||||
|
||||
if (totalHeight > usableScrollHeight) {
|
||||
if (scrollStarted) {
|
||||
if (!waitingToReset) {
|
||||
scrollY += delta * scrollSpeed;
|
||||
if (scrollY >= scrollStop) {
|
||||
scrollY = scrollStop;
|
||||
waitingToReset = true;
|
||||
pauseStart = lastTime;
|
||||
}
|
||||
} else if (lastTime - pauseStart > 3000) {
|
||||
scrollY = 0;
|
||||
waitingToReset = false;
|
||||
scrollStarted = false;
|
||||
scrollStartDelay = lastTime;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
scrollY = 0;
|
||||
}
|
||||
|
||||
int scrollOffset = static_cast<int>(scrollY);
|
||||
int yOffset = -scrollOffset + getTextPositions(display)[1];
|
||||
for (int separatorX = 0; separatorX <= (display->getStringWidth(headerStr) + 3); separatorX += 2) {
|
||||
display->setPixel(separatorX, yOffset + ((SCREEN_WIDTH > 128) ? 19 : 13));
|
||||
}
|
||||
|
||||
// === Render visible lines ===
|
||||
for (size_t i = 0; i < lines.size(); ++i) {
|
||||
int lineY = yOffset;
|
||||
for (size_t j = 0; j < i; ++j)
|
||||
lineY += rowHeights[j];
|
||||
if (lineY > -rowHeights[i] && lineY < scrollBottom) {
|
||||
if (i == 0 && isInverted) {
|
||||
display->drawString(x + 3, lineY, lines[i].c_str());
|
||||
if (isBold)
|
||||
display->drawString(x + 4, lineY, lines[i].c_str());
|
||||
} else {
|
||||
drawStringWithEmotes(display, x, lineY, lines[i], emotes, numEmotes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw header at the end to sort out overlapping elements
|
||||
graphics::drawCommonHeader(display, x, y, titleStr);
|
||||
}
|
||||
|
||||
} // namespace MessageRenderer
|
||||
} // namespace graphics
|
||||
#endif
|
||||
18
src/graphics/draw/MessageRenderer.h
Normal file
18
src/graphics/draw/MessageRenderer.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
#include "OLEDDisplay.h"
|
||||
#include "OLEDDisplayUi.h"
|
||||
#include "graphics/emotes.h"
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
namespace MessageRenderer
|
||||
{
|
||||
|
||||
// Text and emote rendering
|
||||
void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount);
|
||||
|
||||
/// Draws the text message frame for displaying received messages
|
||||
void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
} // namespace MessageRenderer
|
||||
} // namespace graphics
|
||||
595
src/graphics/draw/NodeListRenderer.cpp
Normal file
595
src/graphics/draw/NodeListRenderer.cpp
Normal file
@@ -0,0 +1,595 @@
|
||||
#include "configuration.h"
|
||||
#if HAS_SCREEN
|
||||
#include "CompassRenderer.h"
|
||||
#include "NodeDB.h"
|
||||
#include "NodeListRenderer.h"
|
||||
#include "UIRenderer.h"
|
||||
#include "gps/GeoCoord.h"
|
||||
#include "gps/RTC.h" // for getTime() function
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/images.h"
|
||||
#include "meshUtils.h"
|
||||
#include <algorithm>
|
||||
|
||||
// Forward declarations for functions defined in Screen.cpp
|
||||
namespace graphics
|
||||
{
|
||||
extern bool haveGlyphs(const char *str);
|
||||
} // namespace graphics
|
||||
|
||||
// Global screen instance
|
||||
extern graphics::Screen *screen;
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
namespace NodeListRenderer
|
||||
{
|
||||
|
||||
// Function moved from Screen.cpp to NodeListRenderer.cpp since it's primarily used here
|
||||
void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display)
|
||||
{
|
||||
for (int row = 0; row < height; row++) {
|
||||
uint8_t rowMask = (1 << row);
|
||||
for (int col = 0; col < width; col++) {
|
||||
uint8_t colData = pgm_read_byte(&bitmapXBM[col]);
|
||||
if (colData & rowMask) {
|
||||
// Note: rows become X, columns become Y after transpose
|
||||
display->fillRect(x + row * 2, y + col * 2, 2, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Static variables for dynamic cycling
|
||||
static NodeListMode currentMode = MODE_LAST_HEARD;
|
||||
static int scrollIndex = 0;
|
||||
|
||||
// =============================
|
||||
// Utility Functions
|
||||
// =============================
|
||||
|
||||
const char *getSafeNodeName(meshtastic_NodeInfoLite *node)
|
||||
{
|
||||
static char nodeName[16] = "?";
|
||||
if (node->has_user && strlen(node->user.short_name) > 0) {
|
||||
bool valid = true;
|
||||
const char *name = node->user.short_name;
|
||||
for (size_t i = 0; i < strlen(name); i++) {
|
||||
uint8_t c = (uint8_t)name[i];
|
||||
if (c < 32 || c > 126) {
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (valid) {
|
||||
strncpy(nodeName, name, sizeof(nodeName) - 1);
|
||||
nodeName[sizeof(nodeName) - 1] = '\0';
|
||||
} else {
|
||||
snprintf(nodeName, sizeof(nodeName), "%04X", (uint16_t)(node->num & 0xFFFF));
|
||||
}
|
||||
} else {
|
||||
strcpy(nodeName, "?");
|
||||
}
|
||||
return nodeName;
|
||||
}
|
||||
|
||||
const char *getCurrentModeTitle(int screenWidth)
|
||||
{
|
||||
switch (currentMode) {
|
||||
case MODE_LAST_HEARD:
|
||||
return "Last Heard";
|
||||
case MODE_HOP_SIGNAL:
|
||||
return (screenWidth > 128) ? "Hops/Signal" : "Hops/Sig";
|
||||
case MODE_DISTANCE:
|
||||
return "Distance";
|
||||
default:
|
||||
return "Nodes";
|
||||
}
|
||||
}
|
||||
|
||||
// Use dynamic timing based on mode
|
||||
unsigned long getModeCycleIntervalMs()
|
||||
{
|
||||
return 3000;
|
||||
}
|
||||
|
||||
// Calculate bearing between two lat/lon points
|
||||
float calculateBearing(double lat1, double lon1, double lat2, double lon2)
|
||||
{
|
||||
double dLon = (lon2 - lon1) * DEG_TO_RAD;
|
||||
double y = sin(dLon) * cos(lat2 * DEG_TO_RAD);
|
||||
double x = cos(lat1 * DEG_TO_RAD) * sin(lat2 * DEG_TO_RAD) - sin(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * cos(dLon);
|
||||
double bearing = atan2(y, x) * RAD_TO_DEG;
|
||||
return fmod(bearing + 360.0, 360.0);
|
||||
}
|
||||
|
||||
int calculateMaxScroll(int totalEntries, int visibleRows)
|
||||
{
|
||||
return std::max(0, (totalEntries - 1) / (visibleRows * 2));
|
||||
}
|
||||
|
||||
void retrieveAndSortNodes(std::vector<NodeEntry> &nodeList)
|
||||
{
|
||||
size_t numNodes = nodeDB->getNumMeshNodes();
|
||||
for (size_t i = 0; i < numNodes; i++) {
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
||||
if (!node || node->num == nodeDB->getNodeNum())
|
||||
continue;
|
||||
|
||||
NodeEntry entry;
|
||||
entry.node = node;
|
||||
entry.sortValue = sinceLastSeen(node);
|
||||
|
||||
nodeList.push_back(entry);
|
||||
}
|
||||
|
||||
// Sort nodes: favorites first, then by last heard (most recent first)
|
||||
std::sort(nodeList.begin(), nodeList.end(), [](const NodeEntry &a, const NodeEntry &b) {
|
||||
bool aFav = a.node->is_favorite;
|
||||
bool bFav = b.node->is_favorite;
|
||||
if (aFav != bFav)
|
||||
return aFav;
|
||||
if (a.sortValue == 0 || a.sortValue == UINT32_MAX)
|
||||
return false;
|
||||
if (b.sortValue == 0 || b.sortValue == UINT32_MAX)
|
||||
return true;
|
||||
return a.sortValue < b.sortValue;
|
||||
});
|
||||
}
|
||||
|
||||
void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd)
|
||||
{
|
||||
int columnWidth = display->getWidth() / 2;
|
||||
int separatorX = x + columnWidth - 2;
|
||||
for (int y = yStart; y <= yEnd; y += 2) {
|
||||
display->setPixel(separatorX, y);
|
||||
}
|
||||
}
|
||||
|
||||
void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries, int scrollIndex, int columns, int scrollStartY)
|
||||
{
|
||||
if (totalEntries <= visibleNodeRows * columns)
|
||||
return;
|
||||
|
||||
int scrollbarX = display->getWidth() - 2;
|
||||
int scrollbarHeight = display->getHeight() - scrollStartY - 10;
|
||||
int thumbHeight = std::max(4, (scrollbarHeight * visibleNodeRows * columns) / totalEntries);
|
||||
int maxScroll = calculateMaxScroll(totalEntries, visibleNodeRows);
|
||||
int thumbY = scrollStartY + (scrollIndex * (scrollbarHeight - thumbHeight)) / std::max(1, maxScroll);
|
||||
|
||||
for (int i = 0; i < thumbHeight; i++) {
|
||||
display->setPixel(scrollbarX, thumbY + i);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================
|
||||
// Entry Renderers
|
||||
// =============================
|
||||
|
||||
void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
|
||||
{
|
||||
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
||||
int timeOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7);
|
||||
|
||||
const char *nodeName = getSafeNodeName(node);
|
||||
|
||||
char timeStr[10];
|
||||
uint32_t seconds = sinceLastSeen(node);
|
||||
if (seconds == 0 || seconds == UINT32_MAX) {
|
||||
snprintf(timeStr, sizeof(timeStr), "?");
|
||||
} else {
|
||||
uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24;
|
||||
snprintf(timeStr, sizeof(timeStr), (days > 365 ? "?" : "%d%c"),
|
||||
(days ? days
|
||||
: hours ? hours
|
||||
: minutes),
|
||||
(days ? 'd'
|
||||
: hours ? 'h'
|
||||
: 'm'));
|
||||
}
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
display->drawString(x + ((SCREEN_WIDTH > 128) ? 6 : 3), y, nodeName);
|
||||
if (node->is_favorite) {
|
||||
if (SCREEN_WIDTH > 128) {
|
||||
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
||||
} else {
|
||||
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
||||
}
|
||||
}
|
||||
|
||||
int rightEdge = x + columnWidth - timeOffset;
|
||||
if (timeStr[strlen(timeStr) - 1] == 'm') // Fix the fact that our fonts don't line up well all the time
|
||||
rightEdge -= 1;
|
||||
int textWidth = display->getStringWidth(timeStr);
|
||||
display->drawString(rightEdge - textWidth, y, timeStr);
|
||||
}
|
||||
|
||||
void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
|
||||
{
|
||||
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
||||
|
||||
int nameMaxWidth = columnWidth - 25;
|
||||
int barsOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19);
|
||||
int hopOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17);
|
||||
|
||||
int barsXOffset = columnWidth - barsOffset;
|
||||
|
||||
const char *nodeName = getSafeNodeName(node);
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
|
||||
display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 3), y, nameMaxWidth, nodeName);
|
||||
if (node->is_favorite) {
|
||||
if (SCREEN_WIDTH > 128) {
|
||||
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
||||
} else {
|
||||
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw signal strength bars
|
||||
int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0;
|
||||
int barWidth = 2;
|
||||
int barStartX = x + barsXOffset;
|
||||
int barStartY = y + 1 + (FONT_HEIGHT_SMALL / 2) + 2;
|
||||
|
||||
for (int b = 0; b < 4; b++) {
|
||||
if (b < bars) {
|
||||
int height = (b * 2);
|
||||
display->fillRect(barStartX + (b * (barWidth + 1)), barStartY - height, barWidth, height);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw hop count
|
||||
char hopStr[6] = "";
|
||||
if (node->has_hops_away && node->hops_away > 0)
|
||||
snprintf(hopStr, sizeof(hopStr), "[%d]", node->hops_away);
|
||||
|
||||
if (hopStr[0] != '\0') {
|
||||
int rightEdge = x + columnWidth - hopOffset;
|
||||
int textWidth = display->getStringWidth(hopStr);
|
||||
display->drawString(rightEdge - textWidth, y, hopStr);
|
||||
}
|
||||
}
|
||||
|
||||
void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
|
||||
{
|
||||
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
||||
int nameMaxWidth = columnWidth - (SCREEN_WIDTH > 128 ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
|
||||
|
||||
const char *nodeName = getSafeNodeName(node);
|
||||
char distStr[10] = "";
|
||||
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) {
|
||||
double lat1 = ourNode->position.latitude_i * 1e-7;
|
||||
double lon1 = ourNode->position.longitude_i * 1e-7;
|
||||
double lat2 = node->position.latitude_i * 1e-7;
|
||||
double lon2 = node->position.longitude_i * 1e-7;
|
||||
|
||||
double earthRadiusKm = 6371.0;
|
||||
double dLat = (lat2 - lat1) * DEG_TO_RAD;
|
||||
double dLon = (lon2 - lon1) * DEG_TO_RAD;
|
||||
|
||||
double a =
|
||||
sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLon / 2) * sin(dLon / 2);
|
||||
double c = 2 * atan2(sqrt(a), sqrt(1 - a));
|
||||
double distanceKm = earthRadiusKm * c;
|
||||
|
||||
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
|
||||
double miles = distanceKm * 0.621371;
|
||||
if (miles < 0.1) {
|
||||
int feet = (int)(miles * 5280);
|
||||
if (feet < 1000)
|
||||
snprintf(distStr, sizeof(distStr), "%dft", feet);
|
||||
else
|
||||
snprintf(distStr, sizeof(distStr), "¼mi"); // 4-char max
|
||||
} else {
|
||||
int roundedMiles = (int)(miles + 0.5);
|
||||
if (roundedMiles < 1000)
|
||||
snprintf(distStr, sizeof(distStr), "%dmi", roundedMiles);
|
||||
else
|
||||
snprintf(distStr, sizeof(distStr), "999"); // Max display cap
|
||||
}
|
||||
} else {
|
||||
if (distanceKm < 1.0) {
|
||||
int meters = (int)(distanceKm * 1000);
|
||||
if (meters < 1000)
|
||||
snprintf(distStr, sizeof(distStr), "%dm", meters);
|
||||
else
|
||||
snprintf(distStr, sizeof(distStr), "1k");
|
||||
} else {
|
||||
int km = (int)(distanceKm + 0.5);
|
||||
if (km < 1000)
|
||||
snprintf(distStr, sizeof(distStr), "%dk", km);
|
||||
else
|
||||
snprintf(distStr, sizeof(distStr), "999");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 3), y, nameMaxWidth, nodeName);
|
||||
if (node->is_favorite) {
|
||||
if (SCREEN_WIDTH > 128) {
|
||||
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
||||
} else {
|
||||
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
||||
}
|
||||
}
|
||||
|
||||
if (strlen(distStr) > 0) {
|
||||
int offset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column)
|
||||
: (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column)
|
||||
int rightEdge = x + columnWidth - offset;
|
||||
int textWidth = display->getStringWidth(distStr);
|
||||
display->drawString(rightEdge - textWidth, y, distStr);
|
||||
}
|
||||
}
|
||||
|
||||
void drawEntryDynamic(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
|
||||
{
|
||||
switch (currentMode) {
|
||||
case MODE_LAST_HEARD:
|
||||
drawEntryLastHeard(display, node, x, y, columnWidth);
|
||||
break;
|
||||
case MODE_HOP_SIGNAL:
|
||||
drawEntryHopSignal(display, node, x, y, columnWidth);
|
||||
break;
|
||||
case MODE_DISTANCE:
|
||||
drawNodeDistance(display, node, x, y, columnWidth);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
|
||||
{
|
||||
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
||||
|
||||
// Adjust max text width depending on column and screen width
|
||||
int nameMaxWidth = columnWidth - (SCREEN_WIDTH > 128 ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
|
||||
|
||||
const char *nodeName = getSafeNodeName(node);
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 3), y, nameMaxWidth, nodeName);
|
||||
if (node->is_favorite) {
|
||||
if (SCREEN_WIDTH > 128) {
|
||||
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
||||
} else {
|
||||
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading,
|
||||
double userLat, double userLon)
|
||||
{
|
||||
if (!nodeDB->hasValidPosition(node))
|
||||
return;
|
||||
|
||||
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
||||
int arrowXOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18);
|
||||
|
||||
int centerX = x + columnWidth - arrowXOffset;
|
||||
int centerY = y + FONT_HEIGHT_SMALL / 2;
|
||||
|
||||
double nodeLat = node->position.latitude_i * 1e-7;
|
||||
double nodeLon = node->position.longitude_i * 1e-7;
|
||||
float bearingToNode = calculateBearing(userLat, userLon, nodeLat, nodeLon);
|
||||
float relativeBearing = fmod((bearingToNode - myHeading + 360), 360);
|
||||
float angle = relativeBearing * DEG_TO_RAD;
|
||||
|
||||
// Shrink size by 2px
|
||||
int size = FONT_HEIGHT_SMALL - 5;
|
||||
float halfSize = size / 2.0;
|
||||
|
||||
// Point of the arrow
|
||||
int tipX = centerX + halfSize * cos(angle);
|
||||
int tipY = centerY - halfSize * sin(angle);
|
||||
|
||||
float baseAngle = radians(35);
|
||||
float sideLen = halfSize * 0.95;
|
||||
float notchInset = halfSize * 0.35;
|
||||
|
||||
// Left and right corners
|
||||
int leftX = centerX + sideLen * cos(angle + PI - baseAngle);
|
||||
int leftY = centerY - sideLen * sin(angle + PI - baseAngle);
|
||||
|
||||
int rightX = centerX + sideLen * cos(angle + PI + baseAngle);
|
||||
int rightY = centerY - sideLen * sin(angle + PI + baseAngle);
|
||||
|
||||
// Center notch (cut-in)
|
||||
int notchX = centerX - notchInset * cos(angle);
|
||||
int notchY = centerY + notchInset * sin(angle);
|
||||
|
||||
// Draw the chevron-style arrowhead
|
||||
display->fillTriangle(tipX, tipY, leftX, leftY, notchX, notchY);
|
||||
display->fillTriangle(tipX, tipY, notchX, notchY, rightX, rightY);
|
||||
}
|
||||
|
||||
// =============================
|
||||
// Main Screen Functions
|
||||
// =============================
|
||||
|
||||
void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title,
|
||||
EntryRenderer renderer, NodeExtrasRenderer extras, float heading, double lat, double lon)
|
||||
{
|
||||
const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1;
|
||||
const int rowYOffset = FONT_HEIGHT_SMALL - 3;
|
||||
|
||||
int columnWidth = display->getWidth() / 2;
|
||||
|
||||
display->clear();
|
||||
|
||||
// Draw the battery/time header
|
||||
graphics::drawCommonHeader(display, x, y, title);
|
||||
|
||||
// Space below header
|
||||
y += COMMON_HEADER_HEIGHT;
|
||||
|
||||
// Fetch and display sorted node list
|
||||
std::vector<NodeEntry> nodeList;
|
||||
retrieveAndSortNodes(nodeList);
|
||||
|
||||
int totalEntries = nodeList.size();
|
||||
int totalRowsAvailable = (display->getHeight() - y) / rowYOffset;
|
||||
#ifdef USE_EINK
|
||||
totalRowsAvailable -= 1;
|
||||
#endif
|
||||
int visibleNodeRows = totalRowsAvailable;
|
||||
int totalColumns = 2;
|
||||
|
||||
int startIndex = scrollIndex * visibleNodeRows * totalColumns;
|
||||
int endIndex = std::min(startIndex + visibleNodeRows * totalColumns, totalEntries);
|
||||
|
||||
int yOffset = 0;
|
||||
int col = 0;
|
||||
int lastNodeY = y;
|
||||
int shownCount = 0;
|
||||
int rowCount = 0;
|
||||
|
||||
for (int i = startIndex; i < endIndex; ++i) {
|
||||
int xPos = x + (col * columnWidth);
|
||||
int yPos = y + yOffset;
|
||||
renderer(display, nodeList[i].node, xPos, yPos, columnWidth);
|
||||
|
||||
if (extras) {
|
||||
extras(display, nodeList[i].node, xPos, yPos, columnWidth, heading, lat, lon);
|
||||
}
|
||||
|
||||
lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL);
|
||||
yOffset += rowYOffset;
|
||||
shownCount++;
|
||||
rowCount++;
|
||||
|
||||
if (rowCount >= totalRowsAvailable) {
|
||||
yOffset = 0;
|
||||
rowCount = 0;
|
||||
col++;
|
||||
if (col > (totalColumns - 1))
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Draw column separator
|
||||
if (shownCount > 0) {
|
||||
const int firstNodeY = y + 3;
|
||||
drawColumnSeparator(display, x, firstNodeY, lastNodeY);
|
||||
}
|
||||
|
||||
const int scrollStartY = y + 3;
|
||||
drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, scrollStartY);
|
||||
}
|
||||
|
||||
// =============================
|
||||
// Screen Frame Functions
|
||||
// =============================
|
||||
|
||||
#ifndef USE_EINK
|
||||
void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
// Static variables to track mode and duration
|
||||
static NodeListMode lastRenderedMode = MODE_COUNT;
|
||||
static unsigned long modeStartTime = 0;
|
||||
|
||||
unsigned long now = millis();
|
||||
|
||||
// On very first call (on boot or state enter)
|
||||
if (lastRenderedMode == MODE_COUNT) {
|
||||
currentMode = MODE_LAST_HEARD;
|
||||
modeStartTime = now;
|
||||
}
|
||||
|
||||
// Time to switch to next mode?
|
||||
if (now - modeStartTime >= getModeCycleIntervalMs()) {
|
||||
currentMode = static_cast<NodeListMode>((currentMode + 1) % MODE_COUNT);
|
||||
modeStartTime = now;
|
||||
}
|
||||
|
||||
// Render screen based on currentMode
|
||||
const char *title = getCurrentModeTitle(display->getWidth());
|
||||
drawNodeListScreen(display, state, x, y, title, drawEntryDynamic);
|
||||
|
||||
// Track the last mode to avoid reinitializing modeStartTime
|
||||
lastRenderedMode = currentMode;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_EINK
|
||||
void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
const char *title = "Last Heard";
|
||||
drawNodeListScreen(display, state, x, y, title, drawEntryLastHeard);
|
||||
}
|
||||
|
||||
void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
const char *title = "Hops/Signal";
|
||||
drawNodeListScreen(display, state, x, y, title, drawEntryHopSignal);
|
||||
}
|
||||
|
||||
void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
const char *title = "Distance";
|
||||
drawNodeListScreen(display, state, x, y, title, drawNodeDistance);
|
||||
}
|
||||
#endif
|
||||
|
||||
void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
float heading = 0;
|
||||
bool validHeading = false;
|
||||
double lat = 0;
|
||||
double lon = 0;
|
||||
|
||||
#if HAS_GPS
|
||||
if (screen->hasHeading()) {
|
||||
heading = screen->getHeading(); // degrees
|
||||
validHeading = true;
|
||||
} else {
|
||||
heading = screen->estimatedHeading(lat, lon);
|
||||
validHeading = !isnan(heading);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!validHeading)
|
||||
return;
|
||||
|
||||
drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow, heading, lat, lon);
|
||||
}
|
||||
|
||||
/// Draw a series of fields in a column, wrapping to multiple columns if needed
|
||||
void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields)
|
||||
{
|
||||
// The coordinates define the left starting point of the text
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
|
||||
const char **f = fields;
|
||||
int xo = x, yo = y;
|
||||
while (*f) {
|
||||
display->drawString(xo, yo, *f);
|
||||
if ((display->getColor() == BLACK) && config.display.heading_bold)
|
||||
display->drawString(xo + 1, yo, *f);
|
||||
|
||||
display->setColor(WHITE);
|
||||
yo += FONT_HEIGHT_SMALL;
|
||||
if (yo > SCREEN_HEIGHT - FONT_HEIGHT_SMALL) {
|
||||
xo += SCREEN_WIDTH / 2;
|
||||
yo = 0;
|
||||
}
|
||||
f++;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace NodeListRenderer
|
||||
} // namespace graphics
|
||||
#endif
|
||||
69
src/graphics/draw/NodeListRenderer.h
Normal file
69
src/graphics/draw/NodeListRenderer.h
Normal file
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include "graphics/Screen.h"
|
||||
#include "mesh/generated/meshtastic/mesh.pb.h"
|
||||
#include <OLEDDisplay.h>
|
||||
#include <OLEDDisplayUi.h>
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
|
||||
/// Forward declarations
|
||||
class Screen;
|
||||
|
||||
/**
|
||||
* @brief Node list and entry rendering functions
|
||||
*
|
||||
* Contains all functions related to drawing node lists and individual node entries
|
||||
* including last heard, hop signal, distance, and compass views.
|
||||
*/
|
||||
namespace NodeListRenderer
|
||||
{
|
||||
// Entry renderer function types
|
||||
typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int);
|
||||
typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int, float, double, double);
|
||||
|
||||
// Node entry structure
|
||||
struct NodeEntry {
|
||||
meshtastic_NodeInfoLite *node;
|
||||
uint32_t sortValue;
|
||||
};
|
||||
|
||||
// Node list mode enumeration
|
||||
enum NodeListMode { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_DISTANCE = 2, MODE_COUNT = 3 };
|
||||
|
||||
// Main node list screen function
|
||||
void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title,
|
||||
EntryRenderer renderer, NodeExtrasRenderer extras = nullptr, float heading = 0, double lat = 0,
|
||||
double lon = 0);
|
||||
|
||||
// Entry renderers
|
||||
void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
|
||||
void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
|
||||
void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
|
||||
void drawEntryDynamic(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
|
||||
void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
|
||||
|
||||
// Extras renderers
|
||||
void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading,
|
||||
double userLat, double userLon);
|
||||
|
||||
// Screen frame functions
|
||||
void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
// Utility functions
|
||||
const char *getCurrentModeTitle(int screenWidth);
|
||||
void retrieveAndSortNodes(std::vector<NodeEntry> &nodeList);
|
||||
const char *getSafeNodeName(meshtastic_NodeInfoLite *node);
|
||||
void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields);
|
||||
|
||||
// Bitmap drawing function
|
||||
void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display);
|
||||
|
||||
} // namespace NodeListRenderer
|
||||
|
||||
} // namespace graphics
|
||||
265
src/graphics/draw/NotificationRenderer.cpp
Normal file
265
src/graphics/draw/NotificationRenderer.cpp
Normal file
@@ -0,0 +1,265 @@
|
||||
#include "configuration.h"
|
||||
#if HAS_SCREEN
|
||||
|
||||
#include "DisplayFormatters.h"
|
||||
#include "NodeDB.h"
|
||||
#include "NotificationRenderer.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 std::vector<std::string> functionSymbol;
|
||||
extern std::string functionSymbolString;
|
||||
extern bool hasUnreadMessage;
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
|
||||
char NotificationRenderer::inEvent = INPUT_BROKER_NONE;
|
||||
int8_t NotificationRenderer::curSelected = 0;
|
||||
char NotificationRenderer::alertBannerMessage[256] = {0};
|
||||
uint32_t NotificationRenderer::alertBannerUntil = 0; // 0 is a special case meaning forever
|
||||
uint8_t NotificationRenderer::alertBannerOptions = 0; // last x lines are seelctable options
|
||||
std::function<void(int)> NotificationRenderer::alertBannerCallback = NULL;
|
||||
bool NotificationRenderer::pauseBanner = false;
|
||||
|
||||
// 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 . . ");
|
||||
}
|
||||
}
|
||||
|
||||
void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||
{
|
||||
// Exit if no message is active or duration has passed
|
||||
if (!isOverlayBannerShowing())
|
||||
return;
|
||||
|
||||
if (pauseBanner)
|
||||
return;
|
||||
|
||||
// === Layout Configuration ===
|
||||
constexpr uint16_t padding = 5; // Padding around text inside the box
|
||||
constexpr uint16_t vPadding = 2; // Padding around text inside the box
|
||||
constexpr uint8_t lineSpacing = 1; // Extra space between lines
|
||||
|
||||
// Search the message to determine if we need the bell added
|
||||
bool needs_bell = (strstr(alertBannerMessage, "Alert Received") != nullptr);
|
||||
|
||||
uint8_t firstOption = 0;
|
||||
uint8_t firstOptionToShow = 0;
|
||||
|
||||
// Setup font and alignment
|
||||
display->setFont(FONT_SMALL);
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT); // We will manually center per line
|
||||
const int MAX_LINES = 24;
|
||||
|
||||
uint16_t maxWidth = 0;
|
||||
uint16_t arrowsWidth = display->getStringWidth("> <", 4, true);
|
||||
uint16_t lineWidths[MAX_LINES] = {0};
|
||||
uint16_t lineLengths[MAX_LINES] = {0};
|
||||
char *lineStarts[MAX_LINES + 1];
|
||||
uint16_t lineCount = 0;
|
||||
char lineBuffer[40] = {0};
|
||||
// pointer to the terminating null
|
||||
char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage));
|
||||
lineStarts[lineCount] = alertBannerMessage;
|
||||
|
||||
// loop through lines finding \n characters
|
||||
while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) {
|
||||
lineStarts[lineCount + 1] = std::find(lineStarts[lineCount], alertEnd, '\n');
|
||||
lineLengths[lineCount] = lineStarts[lineCount + 1] - lineStarts[lineCount];
|
||||
if (lineStarts[lineCount + 1][0] == '\n') {
|
||||
lineStarts[lineCount + 1] += 1; // Move the start pointer beyond the \n
|
||||
}
|
||||
lineWidths[lineCount] = display->getStringWidth(lineStarts[lineCount], lineLengths[lineCount], true);
|
||||
if (lineWidths[lineCount] > maxWidth) {
|
||||
maxWidth = lineWidths[lineCount];
|
||||
}
|
||||
if (alertBannerOptions > 0 && lineCount > 0 && lineWidths[lineCount] + arrowsWidth > maxWidth) {
|
||||
maxWidth = lineWidths[lineCount] + arrowsWidth;
|
||||
}
|
||||
lineCount++;
|
||||
// if we are doing a selection, add extra width for arrows
|
||||
}
|
||||
|
||||
if (alertBannerOptions > 0) {
|
||||
// respond to input
|
||||
if (inEvent == INPUT_BROKER_UP || inEvent == INPUT_BROKER_ALT_PRESS) {
|
||||
curSelected--;
|
||||
} else if (inEvent == INPUT_BROKER_DOWN || inEvent == INPUT_BROKER_USER_PRESS) {
|
||||
curSelected++;
|
||||
} else if (inEvent == INPUT_BROKER_SELECT) {
|
||||
alertBannerCallback(curSelected);
|
||||
alertBannerMessage[0] = '\0';
|
||||
} else if ((inEvent == INPUT_BROKER_CANCEL || inEvent == INPUT_BROKER_ALT_LONG) && alertBannerUntil != 0) {
|
||||
alertBannerMessage[0] = '\0';
|
||||
}
|
||||
if (curSelected == -1)
|
||||
curSelected = alertBannerOptions - 1;
|
||||
if (curSelected == alertBannerOptions)
|
||||
curSelected = 0;
|
||||
// compare number of options to number of lines
|
||||
if (lineCount < alertBannerOptions)
|
||||
return;
|
||||
firstOption = lineCount - alertBannerOptions;
|
||||
if (curSelected > 1 && alertBannerOptions > 3) {
|
||||
firstOptionToShow = curSelected + firstOption - 1;
|
||||
// put the selected option in the middle
|
||||
} else {
|
||||
firstOptionToShow = firstOption;
|
||||
}
|
||||
} else { // not in an alert with a callback
|
||||
// TODO: check that at least a second has passed since the alert started
|
||||
if (inEvent == INPUT_BROKER_SELECT || inEvent == INPUT_BROKER_ALT_LONG || inEvent == INPUT_BROKER_CANCEL) {
|
||||
alertBannerMessage[0] = '\0'; // end the alert early
|
||||
}
|
||||
}
|
||||
inEvent = INPUT_BROKER_NONE;
|
||||
if (alertBannerMessage[0] == '\0')
|
||||
return;
|
||||
|
||||
// set width from longest line
|
||||
uint16_t boxWidth = padding * 2 + maxWidth;
|
||||
if (needs_bell) {
|
||||
if (SCREEN_WIDTH > 128 && boxWidth <= 150) {
|
||||
boxWidth += 26;
|
||||
}
|
||||
if (SCREEN_WIDTH <= 128 && boxWidth <= 100) {
|
||||
boxWidth += 20;
|
||||
}
|
||||
}
|
||||
// calculate max lines on screen? for now it's 4
|
||||
// set height from line count
|
||||
uint16_t boxHeight;
|
||||
if (lineCount <= 4) {
|
||||
boxHeight = vPadding * 2 + lineCount * FONT_HEIGHT_SMALL + (lineCount - 1) * lineSpacing;
|
||||
} else {
|
||||
boxHeight = vPadding * 2 + 4 * FONT_HEIGHT_SMALL + 4 * 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->fillRect(boxLeft, boxTop - 2, boxWidth, 1); // Top Line
|
||||
display->fillRect(boxLeft, boxTop + boxHeight + 1, boxWidth, 1); // Bottom Line
|
||||
display->fillRect(boxLeft - 2, boxTop, 1, boxHeight); // Left Line
|
||||
display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight); // Right Line
|
||||
display->setColor(WHITE);
|
||||
display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); // Border
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(boxLeft, boxTop, 1, 1); // Top Left
|
||||
display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1); // Top Right
|
||||
display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1); // Bottom Left
|
||||
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1); // Bottom Right
|
||||
display->setColor(WHITE);
|
||||
|
||||
// === Draw each line centered in the box ===
|
||||
int16_t lineY = boxTop + vPadding;
|
||||
|
||||
for (int i = 0; i < lineCount; i++) {
|
||||
// is this line selected?
|
||||
// if so, start the buffer with -> and strncpy to the 4th location
|
||||
if (i < lineCount - alertBannerOptions || alertBannerOptions == 0) {
|
||||
strncpy(lineBuffer, lineStarts[i], 40);
|
||||
if (lineLengths[i] > 39)
|
||||
lineBuffer[39] = '\0';
|
||||
else
|
||||
lineBuffer[lineLengths[i]] = '\0';
|
||||
} else if (i >= firstOptionToShow && i < firstOptionToShow + 3) {
|
||||
if (i == curSelected + firstOption) {
|
||||
if (lineLengths[i] > 35)
|
||||
lineLengths[i] = 35;
|
||||
strncpy(lineBuffer, "> ", 3);
|
||||
strncpy(lineBuffer + 2, lineStarts[i], 36);
|
||||
strncpy(lineBuffer + lineLengths[i] + 2, " <", 3);
|
||||
lineLengths[i] += 4;
|
||||
lineWidths[i] += display->getStringWidth("> <", 4, true);
|
||||
if (lineLengths[i] > 35)
|
||||
lineBuffer[39] = '\0';
|
||||
else
|
||||
lineBuffer[lineLengths[i]] = '\0';
|
||||
} else {
|
||||
strncpy(lineBuffer, lineStarts[i], 40);
|
||||
if (lineLengths[i] > 39)
|
||||
lineBuffer[39] = '\0';
|
||||
else
|
||||
lineBuffer[lineLengths[i]] = '\0';
|
||||
}
|
||||
} else { // add break for the additional lines
|
||||
continue;
|
||||
}
|
||||
|
||||
int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2;
|
||||
|
||||
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 + lineWidths[i] + 2, bellY, 8, 8, bell_alert);
|
||||
}
|
||||
|
||||
display->drawString(textX, lineY, lineBuffer);
|
||||
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.");
|
||||
}
|
||||
|
||||
bool NotificationRenderer::isOverlayBannerShowing()
|
||||
{
|
||||
return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil);
|
||||
}
|
||||
|
||||
} // namespace graphics
|
||||
#endif
|
||||
28
src/graphics/draw/NotificationRenderer.h
Normal file
28
src/graphics/draw/NotificationRenderer.h
Normal file
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "OLEDDisplay.h"
|
||||
#include "OLEDDisplayUi.h"
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
|
||||
class NotificationRenderer
|
||||
{
|
||||
public:
|
||||
static char inEvent;
|
||||
static int8_t curSelected;
|
||||
static char alertBannerMessage[256];
|
||||
static uint32_t alertBannerUntil; // 0 is a special case meaning forever
|
||||
static uint8_t alertBannerOptions; // last x lines are seelctable options
|
||||
static std::function<void(int)> alertBannerCallback;
|
||||
|
||||
static bool pauseBanner;
|
||||
|
||||
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 drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
static bool isOverlayBannerShowing();
|
||||
};
|
||||
|
||||
} // namespace graphics
|
||||
1240
src/graphics/draw/UIRenderer.cpp
Normal file
1240
src/graphics/draw/UIRenderer.cpp
Normal file
File diff suppressed because it is too large
Load Diff
93
src/graphics/draw/UIRenderer.h
Normal file
93
src/graphics/draw/UIRenderer.h
Normal file
@@ -0,0 +1,93 @@
|
||||
#pragma once
|
||||
|
||||
#include "graphics/Screen.h"
|
||||
#include "graphics/emotes.h"
|
||||
#include <OLEDDisplay.h>
|
||||
#include <OLEDDisplayUi.h>
|
||||
#include <string>
|
||||
|
||||
#define HOURS_IN_MONTH 730
|
||||
|
||||
// Forward declarations for status types
|
||||
namespace meshtastic
|
||||
{
|
||||
class PowerStatus;
|
||||
class NodeStatus;
|
||||
class GPSStatus;
|
||||
} // namespace meshtastic
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
|
||||
/// Forward declarations
|
||||
class Screen;
|
||||
|
||||
/**
|
||||
* @brief UI utility drawing functions
|
||||
*
|
||||
* Contains utility functions for drawing common UI elements, overlays,
|
||||
* battery indicators, and other shared graphical components.
|
||||
*/
|
||||
class UIRenderer
|
||||
{
|
||||
public:
|
||||
// Common UI elements
|
||||
static void drawBattery(OLEDDisplay *display, int16_t x, int16_t y, uint8_t *imgBuffer,
|
||||
const meshtastic::PowerStatus *powerStatus);
|
||||
static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus,
|
||||
int node_offset = 0, bool show_total = true, String additional_words = "");
|
||||
|
||||
// GPS status functions
|
||||
static void drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
|
||||
static void drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
|
||||
static void drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
|
||||
static void drawGpsPowerStatus(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
|
||||
|
||||
// Layout and utility functions
|
||||
static void drawScrollbar(OLEDDisplay *display, int visibleItems, int totalItems, int scrollIndex, int x, int startY);
|
||||
|
||||
// Overlay and special screens
|
||||
static void drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *text);
|
||||
|
||||
// Function overlay for showing mute/buzzer modifiers etc.
|
||||
static void drawFunctionOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
|
||||
// Navigation bar overlay
|
||||
static void drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
|
||||
static void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
// Icon and screen drawing functions
|
||||
static void drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
// Compass and location screen
|
||||
static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
static NodeNum currentFavoriteNodeNum;
|
||||
|
||||
// OEM screens
|
||||
#ifdef USERPREFS_OEM_TEXT
|
||||
static void drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
static void drawOEMBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
#endif
|
||||
|
||||
#ifdef USE_EINK
|
||||
/// Used on eink displays while in deep sleep
|
||||
static void drawDeepSleepFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
/// Used on eink displays when screen updates are paused
|
||||
static void drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
#endif
|
||||
|
||||
static std::string drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds);
|
||||
static int formatDateTime(char *buffer, size_t bufferSize, uint32_t rtc_sec, OLEDDisplay *display, bool showTime);
|
||||
|
||||
// Message filtering
|
||||
static bool shouldDrawMessage(const meshtastic_MeshPacket *packet);
|
||||
// Check if the display can render a string (detect special chars; emoji)
|
||||
static bool haveGlyphs(const char *str);
|
||||
}; // namespace UIRenderer
|
||||
|
||||
} // namespace graphics
|
||||
Reference in New Issue
Block a user