mirror of
https://github.com/meshtastic/firmware.git
synced 2025-12-13 22:32:27 +00:00
473 lines
16 KiB
C++
473 lines
16 KiB
C++
#include "configuration.h"
|
|
#if HAS_SCREEN
|
|
#include "ClockRenderer.h"
|
|
#include "gps/RTC.h"
|
|
#include "graphics/ScreenFonts.h"
|
|
#include "graphics/SharedUIDisplay.h"
|
|
#include "graphics/draw/UIRenderer.h"
|
|
#include "graphics/images.h"
|
|
#include "main.h"
|
|
|
|
#if !MESHTASTIC_EXCLUDE_BLUETOOTH
|
|
#include "nimble/NimbleBluetooth.h"
|
|
#endif
|
|
|
|
namespace graphics
|
|
{
|
|
|
|
namespace ClockRenderer
|
|
{
|
|
|
|
// Segment bitmaps for numerals 0-9 stored in flash to save RAM.
|
|
// Each row is a digit, each column is a segment state (1 = on, 0 = off).
|
|
// Segment layout reference:
|
|
//
|
|
// ___1___
|
|
// 6 | | 2
|
|
// |_7___|
|
|
// 5 | | 3
|
|
// |___4_|
|
|
//
|
|
// Segment order: [1, 2, 3, 4, 5, 6, 7]
|
|
//
|
|
static const uint8_t PROGMEM digitSegments[10][7] = {
|
|
{1, 1, 1, 1, 1, 1, 0}, // 0
|
|
{0, 1, 1, 0, 0, 0, 0}, // 1
|
|
{1, 1, 0, 1, 1, 0, 1}, // 2
|
|
{1, 1, 1, 1, 0, 0, 1}, // 3
|
|
{0, 1, 1, 0, 0, 1, 1}, // 4
|
|
{1, 0, 1, 1, 0, 1, 1}, // 5
|
|
{1, 0, 1, 1, 1, 1, 1}, // 6
|
|
{1, 1, 1, 0, 0, 1, 0}, // 7
|
|
{1, 1, 1, 1, 1, 1, 1}, // 8
|
|
{1, 1, 1, 1, 0, 1, 1} // 9
|
|
};
|
|
|
|
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 + static_cast<uint16_t>(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)
|
|
{
|
|
// Read 7-segment pattern for the digit from flash
|
|
uint8_t seg[7];
|
|
for (uint8_t i = 0; i < 7; i++) {
|
|
seg[i] = pgm_read_byte(&digitSegments[number][i]);
|
|
}
|
|
|
|
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
|
|
uint16_t segmentHeight = SEGMENT_HEIGHT * scale;
|
|
|
|
// Precompute segment positions
|
|
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;
|
|
|
|
// Draw only the active segments
|
|
if (seg[0])
|
|
drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight);
|
|
if (seg[1])
|
|
drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight);
|
|
if (seg[2])
|
|
drawVerticalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight);
|
|
if (seg[3])
|
|
drawHorizontalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight);
|
|
if (seg[4])
|
|
drawVerticalSegment(display, segmentFiveX, segmentFiveY, segmentWidth, segmentHeight);
|
|
if (seg[5])
|
|
drawVerticalSegment(display, segmentSixX, segmentSixY, segmentWidth, segmentHeight);
|
|
if (seg[6])
|
|
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 drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
|
{
|
|
display->clear();
|
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
|
|
|
// === Set Title, Blank for Clock
|
|
const char *titleStr = "";
|
|
// === Header ===
|
|
graphics::drawCommonHeader(display, x, y, titleStr, true, true);
|
|
|
|
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;
|
|
if (config.display.use_12h_clock) {
|
|
hour %= 12;
|
|
if (hour == 0) {
|
|
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);
|
|
|
|
static bool scaleInitialized = false;
|
|
static float scale = 0.75f;
|
|
static float segmentWidth = SEGMENT_WIDTH * 0.75f;
|
|
static float segmentHeight = SEGMENT_HEIGHT * 0.75f;
|
|
|
|
if (!scaleInitialized) {
|
|
float screenwidth_target_ratio = 0.80f; // Target 80% of display width (adjustable)
|
|
float max_scale = 3.5f; // Safety limit to avoid runaway scaling
|
|
float step = 0.05f; // Step increment per iteration
|
|
|
|
float target_width = display->getWidth() * screenwidth_target_ratio;
|
|
float target_height =
|
|
display->getHeight() -
|
|
(isHighResolution
|
|
? 46
|
|
: 33); // Be careful adjusting this number, we have to account for header and the text under the time
|
|
|
|
float calculated_width_size = 0.0f;
|
|
float calculated_height_size = 0.0f;
|
|
|
|
while (true) {
|
|
segmentWidth = SEGMENT_WIDTH * scale;
|
|
segmentHeight = SEGMENT_HEIGHT * scale;
|
|
|
|
calculated_width_size = segmentHeight + ((segmentWidth + (segmentHeight * 2) + 4) * 4);
|
|
calculated_height_size = segmentHeight + ((segmentHeight + (segmentHeight * 2) + 4) * 2);
|
|
|
|
if (calculated_width_size >= target_width || calculated_height_size >= target_height || scale >= max_scale) {
|
|
break;
|
|
}
|
|
|
|
scale += step;
|
|
}
|
|
|
|
// If we overshot width, back off one step and recompute segment sizes
|
|
if (calculated_width_size > target_width || calculated_height_size > target_height) {
|
|
scale -= step;
|
|
segmentWidth = SEGMENT_WIDTH * scale;
|
|
segmentHeight = SEGMENT_HEIGHT * scale;
|
|
}
|
|
|
|
scaleInitialized = true;
|
|
}
|
|
|
|
// calculate hours:minutes string width
|
|
size_t len = strlen(timeString);
|
|
uint16_t timeStringWidth = len * 5;
|
|
|
|
for (size_t i = 0; i < len; 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) + 2;
|
|
|
|
// iterate over characters in hours:minutes string and draw segmented characters
|
|
for (size_t i = 0; i < len; i++) {
|
|
char character = timeString[i];
|
|
|
|
if (character == ':') {
|
|
drawSegmentedDisplayColon(display, hourMinuteTextX, hourMinuteTextY, scale);
|
|
|
|
hourMinuteTextX += segmentHeight + 6;
|
|
if (scale >= 2.0f) {
|
|
hourMinuteTextX += (uint16_t)(4.5f * scale);
|
|
}
|
|
} else {
|
|
drawSegmentedDisplayCharacter(display, hourMinuteTextX, hourMinuteTextY, character - '0', scale);
|
|
|
|
hourMinuteTextX += segmentWidth + (segmentHeight * 2) + 4;
|
|
}
|
|
|
|
hourMinuteTextX += 5;
|
|
}
|
|
|
|
// draw seconds string + AM/PM
|
|
display->setFont(FONT_SMALL);
|
|
int xOffset = (isHighResolution) ? 0 : -1;
|
|
if (hour >= 10) {
|
|
xOffset += (isHighResolution) ? 32 : 18;
|
|
}
|
|
|
|
if (config.display.use_12h_clock) {
|
|
display->drawString(startingHourMinuteTextX + xOffset, (display->getHeight() - hourMinuteTextY) - 1, isPM ? "pm" : "am");
|
|
}
|
|
|
|
#ifndef USE_EINK
|
|
xOffset = (isHighResolution) ? 18 : 10;
|
|
if (scale >= 2.0f) {
|
|
xOffset -= (int)(4.5f * scale);
|
|
}
|
|
display->drawString(startingHourMinuteTextX + timeStringWidth - xOffset, (display->getHeight() - hourMinuteTextY) - 1,
|
|
secondString);
|
|
#endif
|
|
|
|
graphics::drawCommonFooter(display, x, y);
|
|
}
|
|
|
|
// Draw an analog clock
|
|
void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
|
{
|
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
|
// === Set Title, Blank for Clock
|
|
const char *titleStr = "";
|
|
// === Header ===
|
|
graphics::drawCommonHeader(display, x, y, titleStr, true, true);
|
|
|
|
// clock face center coordinates
|
|
int16_t centerX = display->getWidth() / 2;
|
|
int16_t centerY = display->getHeight() / 2;
|
|
|
|
// clock face radius
|
|
int16_t radius = (std::min(display->getWidth(), display->getHeight()) / 2) * 0.9;
|
|
#ifdef T_WATCH_S3
|
|
radius = (display->getWidth() / 2) * 0.8;
|
|
#endif
|
|
|
|
// 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;
|
|
|
|
double secondsTickMarkInnerNoonY = noonY + (isHighResolution ? 8 : 4);
|
|
double hoursTickMarkInnerNoonY = noonY + (isHighResolution ? 16 : 6);
|
|
|
|
// 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.35;
|
|
if (isHighResolution) {
|
|
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) {
|
|
int hour, minute, second;
|
|
decomposeTime(rtc_sec, hour, minute, second);
|
|
|
|
if (config.display.use_12h_clock) {
|
|
bool isPM = hour >= 12;
|
|
display->setFont(FONT_SMALL);
|
|
int yOffset = isHighResolution ? 1 : 0;
|
|
#ifdef USE_EINK
|
|
yOffset += 3;
|
|
#endif
|
|
display->drawString(centerX - (display->getStringWidth(isPM ? "pm" : "am") / 2), centerY + yOffset,
|
|
isPM ? "pm" : "am");
|
|
}
|
|
hour %= 12;
|
|
if (hour == 0)
|
|
hour = 12;
|
|
|
|
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;
|
|
|
|
#ifdef T_WATCH_S3
|
|
// draw hour number
|
|
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
|
|
#else
|
|
#ifdef USE_EINK
|
|
if (isHighResolution) {
|
|
// draw hour number
|
|
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
|
|
}
|
|
#else
|
|
if (isHighResolution && (hourInt == 3 || hourInt == 6 || hourInt == 9 || hourInt == 12)) {
|
|
// draw hour number
|
|
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
|
|
}
|
|
#endif
|
|
#endif
|
|
}
|
|
|
|
if (angle % degreesPerMinuteOrSecond == 0) {
|
|
double startX = sineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + noonX;
|
|
double startY = cosineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + centerY;
|
|
|
|
if (isHighResolution) {
|
|
// 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);
|
|
#ifndef USE_EINK
|
|
// draw second hand
|
|
display->drawLine(centerX, centerY, secondX, secondY);
|
|
#endif
|
|
}
|
|
graphics::drawCommonFooter(display, x, y);
|
|
}
|
|
|
|
} // namespace ClockRenderer
|
|
|
|
} // namespace graphics
|
|
#endif |