From ce5d7b759fdfb3ea9f21644564aafa7e69ffac54 Mon Sep 17 00:00:00 2001 From: Jason P Date: Sun, 8 Jun 2025 15:45:46 -0500 Subject: [PATCH] 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 --- src/graphics/Screen.cpp | 481 ++-------------------------- src/graphics/Screen.h | 21 -- src/graphics/draw/ClockRenderer.cpp | 462 ++++++++++++++++++++++++++ src/graphics/draw/ClockRenderer.h | 10 +- src/graphics/draw/DebugRenderer.cpp | 3 - src/graphics/images.h | 5 +- src/input/ButtonThreadImpl.cpp | 4 - 7 files changed, 487 insertions(+), 499 deletions(-) create mode 100644 src/graphics/draw/ClockRenderer.cpp diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 91aa82f23..049771e88 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -28,6 +28,7 @@ along with this program. If not, see . #include #include "DisplayFormatters.h" +#include "draw/ClockRenderer.h" #include "draw/DebugRenderer.h" #include "draw/MessageRenderer.h" #include "draw/NodeListRenderer.h" @@ -97,9 +98,6 @@ static uint32_t targetFramerate = IDLE_FRAMERATE; uint32_t logo_timeout = 5000; // 4 seconds for EACH logo -// This image definition is here instead of images.h because it's modified dynamically by the drawBattery function -uint8_t imgBattery[16] = {0xFF, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0xE7, 0x3C}; - // Threshold values for the GPS lock accuracy bar display uint32_t dopThresholds[5] = {2000, 1000, 500, 200, 100}; @@ -174,443 +172,6 @@ static bool shouldDrawMessage(const meshtastic_MeshPacket *packet) return packet->from != 0 && !moduleConfig.store_forward.enabled; } -#if defined(DISPLAY_CLOCK_FRAME) - -void Screen::drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode, float scale) -{ - uint16_t segmentWidth = SEGMENT_WIDTH * scale; - uint16_t segmentHeight = SEGMENT_HEIGHT * scale; - - if (digitalMode) { - uint16_t radius = (segmentWidth + (segmentHeight * 2) + 4) / 2; - uint16_t centerX = (x + segmentHeight + 2) + (radius / 2); - uint16_t centerY = (y + segmentHeight + 2) + (radius / 2); - - display->drawCircle(centerX, centerY, radius); - display->drawCircle(centerX, centerY, radius + 1); - display->drawLine(centerX, centerY, centerX, centerY - radius + 3); - display->drawLine(centerX, centerY, centerX + radius - 3, centerY); - } else { - uint16_t segmentOneX = x + segmentHeight + 2; - uint16_t segmentOneY = y; - - uint16_t segmentTwoX = segmentOneX + segmentWidth + 2; - uint16_t segmentTwoY = segmentOneY + segmentHeight + 2; - - uint16_t segmentThreeX = segmentOneX; - uint16_t segmentThreeY = segmentTwoY + segmentWidth + 2; - - uint16_t segmentFourX = x; - uint16_t segmentFourY = y + segmentHeight + 2; - - drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight); - drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight); - drawHorizontalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight); - drawVerticalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight); - } -} - -// Draw a digital clock -void Screen::drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->setTextAlignment(TEXT_ALIGN_LEFT); - - UIRenderer::drawBattery(display, x, y + 7, imgBattery, powerStatus); - - if (powerStatus->getHasBattery()) { - char batteryPercent[8]; - snprintf(batteryPercent, sizeof(batteryPercent), "%d%%", powerStatus->getBatteryChargePercent()); - - display->setFont(FONT_SMALL); - - display->drawString(x + 20, y + 2, batteryPercent); - } - - if (nimbleBluetooth && nimbleBluetooth->isConnected()) { - drawBluetoothConnectedIcon(display, display->getWidth() - 18, y + 2); - } - - drawWatchFaceToggleButton(display, display->getWidth() - 36, display->getHeight() - 36, screen->digitalWatchFace, 1); - - display->setColor(OLEDDISPLAY_COLOR::WHITE); - - uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone - if (rtc_sec > 0) { - long hms = rtc_sec % SEC_PER_DAY; - hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; - - int hour = hms / SEC_PER_HOUR; - int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; - int second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN - - hour = hour > 12 ? hour - 12 : hour; - - if (hour == 0) { - hour = 12; - } - - // Format time string - char timeString[16]; - snprintf(timeString, sizeof(timeString), "%d:%02d", hour, minute); - - // Format seconds string - char secondString[8]; - snprintf(secondString, sizeof(secondString), "%02d", second); - - float scale = 1.5; - - uint16_t segmentWidth = SEGMENT_WIDTH * scale; - uint16_t segmentHeight = SEGMENT_HEIGHT * scale; - - // calculate hours:minutes string width - uint16_t timeStringWidth = strlen(timeString) * 5; - - for (uint8_t i = 0; i < strlen(timeString); i++) { - char character = timeString[i]; - - if (character == ':') { - timeStringWidth += segmentHeight; - } else { - timeStringWidth += segmentWidth + (segmentHeight * 2) + 4; - } - } - - // calculate seconds string width - uint16_t secondStringWidth = (strlen(secondString) * 12) + 4; - - // sum these to get total string width - uint16_t totalWidth = timeStringWidth + secondStringWidth; - - uint16_t hourMinuteTextX = (display->getWidth() / 2) - (totalWidth / 2); - - uint16_t startingHourMinuteTextX = hourMinuteTextX; - - uint16_t hourMinuteTextY = (display->getHeight() / 2) - (((segmentWidth * 2) + (segmentHeight * 3) + 8) / 2); - - // iterate over characters in hours:minutes string and draw segmented characters - for (uint8_t i = 0; i < strlen(timeString); i++) { - char character = timeString[i]; - - if (character == ':') { - drawSegmentedDisplayColon(display, hourMinuteTextX, hourMinuteTextY, scale); - - hourMinuteTextX += segmentHeight + 6; - } else { - drawSegmentedDisplayCharacter(display, hourMinuteTextX, hourMinuteTextY, character - '0', scale); - - hourMinuteTextX += segmentWidth + (segmentHeight * 2) + 4; - } - - hourMinuteTextX += 5; - } - - // draw seconds string - display->setFont(FONT_MEDIUM); - display->drawString(startingHourMinuteTextX + timeStringWidth + 4, - (display->getHeight() - hourMinuteTextY) - FONT_HEIGHT_MEDIUM + 6, secondString); - } -} - -void Screen::drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale) -{ - uint16_t segmentWidth = SEGMENT_WIDTH * scale; - uint16_t segmentHeight = SEGMENT_HEIGHT * scale; - - uint16_t cellHeight = (segmentWidth * 2) + (segmentHeight * 3) + 8; - - uint16_t topAndBottomX = x + (4 * scale); - - uint16_t quarterCellHeight = cellHeight / 4; - - uint16_t topY = y + quarterCellHeight; - uint16_t bottomY = y + (quarterCellHeight * 3); - - display->fillRect(topAndBottomX, topY, segmentHeight, segmentHeight); - display->fillRect(topAndBottomX, bottomY, segmentHeight, segmentHeight); -} - -void Screen::drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t number, float scale) -{ - // the numbers 0-9, each expressed as an array of seven boolean (0|1) values encoding the on/off state of - // segment {innerIndex + 1} - // e.g., to display the numeral '0', segments 1-6 are on, and segment 7 is off. - uint8_t numbers[10][7] = { - {1, 1, 1, 1, 1, 1, 0}, // 0 Display segment key - {0, 1, 1, 0, 0, 0, 0}, // 1 1 - {1, 1, 0, 1, 1, 0, 1}, // 2 ___ - {1, 1, 1, 1, 0, 0, 1}, // 3 6 | | 2 - {0, 1, 1, 0, 0, 1, 1}, // 4 |_7̲_| - {1, 0, 1, 1, 0, 1, 1}, // 5 5 | | 3 - {1, 0, 1, 1, 1, 1, 1}, // 6 |___| - {1, 1, 1, 0, 0, 1, 0}, // 7 - {1, 1, 1, 1, 1, 1, 1}, // 8 4 - {1, 1, 1, 1, 0, 1, 1}, // 9 - }; - - // the width and height of each segment's central rectangle: - // _____________________ - // ⋰| (only this part, |⋱ - // ⋰ | not including | ⋱ - // ⋱ | the triangles | ⋰ - // ⋱| on the ends) |⋰ - // ‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ - - uint16_t segmentWidth = SEGMENT_WIDTH * scale; - uint16_t segmentHeight = SEGMENT_HEIGHT * scale; - - // segment x and y coordinates - uint16_t segmentOneX = x + segmentHeight + 2; - uint16_t segmentOneY = y; - - uint16_t segmentTwoX = segmentOneX + segmentWidth + 2; - uint16_t segmentTwoY = segmentOneY + segmentHeight + 2; - - uint16_t segmentThreeX = segmentTwoX; - uint16_t segmentThreeY = segmentTwoY + segmentWidth + 2 + segmentHeight + 2; - - uint16_t segmentFourX = segmentOneX; - uint16_t segmentFourY = segmentThreeY + segmentWidth + 2; - - uint16_t segmentFiveX = x; - uint16_t segmentFiveY = segmentThreeY; - - uint16_t segmentSixX = x; - uint16_t segmentSixY = segmentTwoY; - - uint16_t segmentSevenX = segmentOneX; - uint16_t segmentSevenY = segmentTwoY + segmentWidth + 2; - - if (numbers[number][0]) { - drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight); - } - - if (numbers[number][1]) { - drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight); - } - - if (numbers[number][2]) { - drawVerticalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight); - } - - if (numbers[number][3]) { - drawHorizontalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight); - } - - if (numbers[number][4]) { - drawVerticalSegment(display, segmentFiveX, segmentFiveY, segmentWidth, segmentHeight); - } - - if (numbers[number][5]) { - drawVerticalSegment(display, segmentSixX, segmentSixY, segmentWidth, segmentHeight); - } - - if (numbers[number][6]) { - drawHorizontalSegment(display, segmentSevenX, segmentSevenY, segmentWidth, segmentHeight); - } -} - -void Screen::drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int height) -{ - int halfHeight = height / 2; - - // draw central rectangle - display->fillRect(x, y, width, height); - - // draw end triangles - display->fillTriangle(x, y, x, y + height - 1, x - halfHeight, y + halfHeight); - - display->fillTriangle(x + width, y, x + width + halfHeight, y + halfHeight, x + width, y + height - 1); -} - -void Screen::drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int height) -{ - int halfHeight = height / 2; - - // draw central rectangle - display->fillRect(x, y, height, width); - - // draw end triangles - display->fillTriangle(x + halfHeight, y - halfHeight, x + height - 1, y, x, y); - - display->fillTriangle(x, y + width, x + height - 1, y + width, x + halfHeight, y + width + halfHeight); -} - -void Screen::drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y) -{ - display->drawFastImage(x, y, 18, 14, bluetoothConnectedIcon); -} - -// Draw an analog clock -void Screen::drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->setTextAlignment(TEXT_ALIGN_LEFT); - - UIRenderer::drawBattery(display, x, y + 7, imgBattery, powerStatus); - - if (powerStatus->getHasBattery()) { - char batteryPercent[8]; - snprintf(batteryPercent, sizeof(batteryPercent), "%d%%", powerStatus->getBatteryChargePercent()); - - display->setFont(FONT_SMALL); - - display->drawString(x + 20, y + 2, batteryPercent); - } - - if (nimbleBluetooth && nimbleBluetooth->isConnected()) { - drawBluetoothConnectedIcon(display, display->getWidth() - 18, y + 2); - } - - drawWatchFaceToggleButton(display, display->getWidth() - 36, display->getHeight() - 36, screen->digitalWatchFace, 1); - - // clock face center coordinates - int16_t centerX = display->getWidth() / 2; - int16_t centerY = display->getHeight() / 2; - - // clock face radius - int16_t radius = (display->getWidth() / 2) * 0.8; - - // noon (0 deg) coordinates (outermost circle) - int16_t noonX = centerX; - int16_t noonY = centerY - radius; - - // second hand radius and y coordinate (outermost circle) - int16_t secondHandNoonY = noonY + 1; - - // tick mark outer y coordinate; (first nested circle) - int16_t tickMarkOuterNoonY = secondHandNoonY; - - // seconds tick mark inner y coordinate; (second nested circle) - double secondsTickMarkInnerNoonY = (double)noonY + 8; - - // hours tick mark inner y coordinate; (third nested circle) - double hoursTickMarkInnerNoonY = (double)noonY + 16; - - // minute hand y coordinate - int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4; - - // hour string y coordinate - int16_t hourStringNoonY = minuteHandNoonY + 18; - - // hour hand radius and y coordinate - int16_t hourHandRadius = radius * 0.55; - int16_t hourHandNoonY = centerY - hourHandRadius; - - display->setColor(OLEDDISPLAY_COLOR::WHITE); - display->drawCircle(centerX, centerY, radius); - - uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone - if (rtc_sec > 0) { - long hms = rtc_sec % SEC_PER_DAY; - hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; - - // Tear apart hms into h:m:s - int hour = hms / SEC_PER_HOUR; - int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; - int second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN - - hour = hour > 12 ? hour - 12 : hour; - - int16_t degreesPerHour = 30; - int16_t degreesPerMinuteOrSecond = 6; - - double hourBaseAngle = hour * degreesPerHour; - double hourAngleOffset = ((double)minute / 60) * degreesPerHour; - double hourAngle = radians(hourBaseAngle + hourAngleOffset); - - double minuteBaseAngle = minute * degreesPerMinuteOrSecond; - double minuteAngleOffset = ((double)second / 60) * degreesPerMinuteOrSecond; - double minuteAngle = radians(minuteBaseAngle + minuteAngleOffset); - - double secondAngle = radians(second * degreesPerMinuteOrSecond); - - double hourX = sin(-hourAngle) * (hourHandNoonY - centerY) + noonX; - double hourY = cos(-hourAngle) * (hourHandNoonY - centerY) + centerY; - - double minuteX = sin(-minuteAngle) * (minuteHandNoonY - centerY) + noonX; - double minuteY = cos(-minuteAngle) * (minuteHandNoonY - centerY) + centerY; - - double secondX = sin(-secondAngle) * (secondHandNoonY - centerY) + noonX; - double secondY = cos(-secondAngle) * (secondHandNoonY - centerY) + centerY; - - display->setFont(FONT_MEDIUM); - - // draw minute and hour tick marks and hour numbers - for (uint16_t angle = 0; angle < 360; angle += 6) { - double angleInRadians = radians(angle); - - double sineAngleInRadians = sin(-angleInRadians); - double cosineAngleInRadians = cos(-angleInRadians); - - double endX = sineAngleInRadians * (tickMarkOuterNoonY - centerY) + noonX; - double endY = cosineAngleInRadians * (tickMarkOuterNoonY - centerY) + centerY; - - if (angle % degreesPerHour == 0) { - double startX = sineAngleInRadians * (hoursTickMarkInnerNoonY - centerY) + noonX; - double startY = cosineAngleInRadians * (hoursTickMarkInnerNoonY - centerY) + centerY; - - // draw hour tick mark - display->drawLine(startX, startY, endX, endY); - - static char buffer[2]; - - uint8_t hourInt = (angle / 30); - - if (hourInt == 0) { - hourInt = 12; - } - - // hour number x offset needs to be adjusted for some cases - int8_t hourStringXOffset; - int8_t hourStringYOffset = 13; - - switch (hourInt) { - case 3: - hourStringXOffset = 5; - break; - case 9: - hourStringXOffset = 7; - break; - case 10: - case 11: - hourStringXOffset = 8; - break; - case 12: - hourStringXOffset = 13; - break; - default: - hourStringXOffset = 6; - break; - } - - double hourStringX = (sineAngleInRadians * (hourStringNoonY - centerY) + noonX) - hourStringXOffset; - double hourStringY = (cosineAngleInRadians * (hourStringNoonY - centerY) + centerY) - hourStringYOffset; - - // draw hour number - display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt); - } - - if (angle % degreesPerMinuteOrSecond == 0) { - double startX = sineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + noonX; - double startY = cosineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + centerY; - - // draw minute tick mark - display->drawLine(startX, startY, endX, endY); - } - } - - // draw hour hand - display->drawLine(centerX, centerY, hourX, hourY); - - // draw minute hand - display->drawLine(centerX, centerY, minuteX, minuteY); - - // draw second hand - display->drawLine(centerX, centerY, secondX, secondY); - } -} - -#endif - // Get an absolute time from "seconds ago" info. Returns false if no valid timestamp possible bool deltaToTimestamp(uint32_t secondsAgo, uint8_t *hours, uint8_t *minutes, int32_t *daysAgo) { @@ -1373,7 +934,8 @@ void Screen::setFrames(FrameFocus focus) } #if defined(DISPLAY_CLOCK_FRAME) - normalFrames[numframes++] = screen->digitalWatchFace ? &Screen::drawDigitalClockFrame : &Screen::drawAnalogClockFrame; + normalFrames[numframes++] = graphics::ClockRenderer::digitalWatchFace ? graphics::ClockRenderer::drawDigitalClockFrame + : &graphics::ClockRenderer::drawAnalogClockFrame; indicatorIcons.push_back(icon_clock); #endif @@ -1422,6 +984,10 @@ void Screen::setFrames(FrameFocus focus) normalFrames[numframes++] = graphics::DebugRenderer::drawMemoryUsage; indicatorIcons.push_back(icon_memory); } +#if !defined(DISPLAY_CLOCK_FRAME) + normalFrames[numframes++] = graphics::ClockRenderer::drawDigitalClockFrame; + indicatorIcons.push_back(icon_clock); +#endif for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); @@ -1431,17 +997,6 @@ void Screen::setFrames(FrameFocus focus) } } - // then the debug info - - // Since frames are basic function pointers, we have to use a helper to - // call a method on debugInfo object. - // fsi.positions.log = numframes; - // normalFrames[numframes++] = graphics::DebugRenderer::drawDebugInfoTrampoline; - - // call a method on debugInfoScreen object (for more details) - // fsi.positions.settings = numframes; - // normalFrames[numframes++] = graphics::DebugRenderer::drawDebugInfoSettingsTrampoline; - #if HAS_WIFI && !defined(ARCH_PORTDUINO) if (!dismissedFrames.wifi && isWifiAvailable()) { fsi.positions.wifi = numframes; @@ -1768,19 +1323,21 @@ int Screen::handleInputEvent(const InputEvent *event) ui->update(); return 0; } -#if defined(DISPLAY_CLOCK_FRAME) - // For the T-Watch, intercept touches to the 'toggle digital/analog watch face' button - uint8_t watchFaceFrame = error_code ? 1 : 0; + /* + #if defined(DISPLAY_CLOCK_FRAME) + // For the T-Watch, intercept touches to the 'toggle digital/analog watch face' button + uint8_t watchFaceFrame = error_code ? 1 : 0; - if (this->ui->getUiState()->currentFrame == watchFaceFrame && event->touchX >= 204 && event->touchX <= 240 && - event->touchY >= 204 && event->touchY <= 240) { - screen->digitalWatchFace = !screen->digitalWatchFace; + if (this->ui->getUiState()->currentFrame == watchFaceFrame && event->touchX >= 204 && event->touchX <= 240 && + event->touchY >= 204 && event->touchY <= 240) { + screen->digitalWatchFace = !screen->digitalWatchFace; - setFrames(); + setFrames(); - return 0; - } -#endif + return 0; + } + #endif + */ // Use left or right input from a keyboard to move between frames, // so long as a mesh module isn't using these events for some other purpose diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index fff52da77..185462028 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -636,27 +636,6 @@ class Screen : public concurrency::OSThread // Sets frame up for immediate drawing void setFrameImmediateDraw(FrameCallback *drawFrames); -#if defined(DISPLAY_CLOCK_FRAME) - static void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - - static void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - - static void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t number, float scale = 1); - - static void drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int height); - - static void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int height); - - static void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale = 1); - - static void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode = true, float scale = 1); - - static void drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y); - - // Whether we are showing the digital watch face or the analog one - bool digitalWatchFace = true; -#endif - /// callback for current alert frame FrameCallback alertFrame; diff --git a/src/graphics/draw/ClockRenderer.cpp b/src/graphics/draw/ClockRenderer.cpp new file mode 100644 index 000000000..7700b0731 --- /dev/null +++ b/src/graphics/draw/ClockRenderer.cpp @@ -0,0 +1,462 @@ +#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 + +#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 + if (rtc_sec > 0) { + long hms = rtc_sec % SEC_PER_DAY; + hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; + + int hour = hms / SEC_PER_HOUR; + int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + int second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN + + hour = hour > 12 ? hour - 12 : hour; + + if (hour == 0) { + hour = 12; + } + + // Format time string + char timeString[16]; + snprintf(timeString, sizeof(timeString), "%d:%02d", hour, minute); + + // Format seconds string + char secondString[8]; + snprintf(secondString, sizeof(secondString), "%02d", second); + +#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; + } + } + + // calculate seconds string width + uint16_t secondStringWidth = (strlen(secondString) * 12) + 4; + + // sum these to get total string width + uint16_t totalWidth = timeStringWidth + secondStringWidth; + + uint16_t hourMinuteTextX = (display->getWidth() / 2) - (totalWidth / 2); + + uint16_t startingHourMinuteTextX = hourMinuteTextX; + + uint16_t hourMinuteTextY = (display->getHeight() / 2) - (((segmentWidth * 2) + (segmentHeight * 3) + 8) / 2); + + // iterate over characters in hours:minutes string and draw segmented characters + for (uint8_t i = 0; i < strlen(timeString); i++) { + char character = timeString[i]; + + if (character == ':') { + drawSegmentedDisplayColon(display, hourMinuteTextX, hourMinuteTextY, scale); + + hourMinuteTextX += segmentHeight + 6; + } else { + drawSegmentedDisplayCharacter(display, hourMinuteTextX, hourMinuteTextY, character - '0', scale); + + hourMinuteTextX += segmentWidth + (segmentHeight * 2) + 4; + } + + hourMinuteTextX += 5; + } + + // draw seconds string + display->setFont(FONT_MEDIUM); + display->drawString(startingHourMinuteTextX + timeStringWidth + 4, + (display->getHeight() - hourMinuteTextY) - FONT_HEIGHT_MEDIUM + 6, secondString); + } +} + +void 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); + } + + if (nimbleBluetooth && nimbleBluetooth->isConnected()) { + drawBluetoothConnectedIcon(display, display->getWidth() - 18, y + 2); + } + + 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 \ No newline at end of file diff --git a/src/graphics/draw/ClockRenderer.h b/src/graphics/draw/ClockRenderer.h index 23ad79d39..b679b6796 100644 --- a/src/graphics/draw/ClockRenderer.h +++ b/src/graphics/draw/ClockRenderer.h @@ -1,6 +1,5 @@ #pragma once -#include "graphics/Screen.h" #include #include @@ -10,14 +9,11 @@ namespace graphics /// Forward declarations class Screen; -/** - * @brief Clock drawing functions - * - * Contains all functions related to drawing analog and digital clocks, - * segmented displays, and time-related UI elements. - */ 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); diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index 4b0c0b273..4b9effb4c 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -36,9 +36,6 @@ const int textPositions[7] = {textZeroLine, textFirstLine, textSecondLine, tex using namespace meshtastic; -// Battery icon array -static uint8_t imgBattery[16] = {0xFF, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0xE7, 0x3C}; - // External variables extern graphics::Screen *screen; extern PowerStatus *powerStatus; diff --git a/src/graphics/images.h b/src/graphics/images.h index 980052cd4..78e6cdb81 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -19,11 +19,12 @@ const uint8_t imgUser[] PROGMEM = {0x3C, 0x42, 0x99, 0xA5, 0xA5, 0x99, 0x42, 0x3 const uint8_t imgPositionEmpty[] PROGMEM = {0x20, 0x30, 0x28, 0x24, 0x42, 0xFF}; const uint8_t imgPositionSolid[] PROGMEM = {0x20, 0x30, 0x38, 0x3C, 0x7E, 0xFF}; -#if defined(DISPLAY_CLOCK_FRAME) const uint8_t bluetoothConnectedIcon[36] PROGMEM = {0xfe, 0x01, 0xff, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x00, 0xe3, 0x1f, 0xf3, 0x3f, 0x33, 0x30, 0x33, 0x33, 0x33, 0x33, 0x03, 0x33, 0xff, 0x33, 0xfe, 0x31, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0xf0, 0x3f, 0xe0, 0x1f}; -#endif + +// This image definition is here instead of images.h because it's modified dynamically by the drawBattery function +static uint8_t imgBattery[16] = {0xFF, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0xE7, 0x3C}; #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || ARCH_PORTDUINO) && \ diff --git a/src/input/ButtonThreadImpl.cpp b/src/input/ButtonThreadImpl.cpp index c57f1c8f0..2c92d8163 100644 --- a/src/input/ButtonThreadImpl.cpp +++ b/src/input/ButtonThreadImpl.cpp @@ -2,8 +2,6 @@ #include "InputBroker.h" #include "configuration.h" -#if defined(BUTTON_PIN) - ButtonThreadImpl *aButtonThreadImpl; ButtonThreadImpl::ButtonThreadImpl() : ButtonThread("UserButton") {} @@ -13,5 +11,3 @@ void ButtonThreadImpl::init() // init should give the pin number and the action if (inputBroker) inputBroker->registerSource(this); } - -#endif // INPUTBROKER_SERIAL_TYPE \ No newline at end of file