diff --git a/arch/portduino/portduino.ini b/arch/portduino/portduino.ini index 6964eaa4e..67c21e972 100644 --- a/arch/portduino/portduino.ini +++ b/arch/portduino/portduino.ini @@ -1,6 +1,6 @@ ; The Portduino based sim environment on top of any host OS, all hardware will be simulated [portduino_base] -platform = https://github.com/meshtastic/platform-native.git#f5ec3c031b0fcd89c0523de9e43eef3a92d59292 +platform = https://github.com/meshtastic/platform-native.git#ad8112adf82ce1f5b917092cf32be07a077801a0 framework = arduino build_src_filter = diff --git a/protobufs b/protobufs index 5cfadd148..b5dc871a1 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 5cfadd14890b7723a1fe6e7683f711911154b010 +Subproject commit b5dc871a1bfa2cc932126a4f490d9ef078476e4c diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index a2cdb5b30..864b246a3 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -96,13 +96,17 @@ void readFromRTC() * * If we haven't yet set our RTC this boot, set it from a GPS derived time */ -bool perhapsSetRTC(RTCQuality q, const struct timeval *tv) +bool perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate) { static uint32_t lastSetMsec = 0; uint32_t now = millis(); bool shouldSet; - if (q > currentQuality) { + if (forceUpdate) { + shouldSet = true; + LOG_DEBUG("Overriding current RTC quality (%s) with incoming time of RTC quality of %s\n", RtcName(currentQuality), + RtcName(q)); + } else if (q > currentQuality) { shouldSet = true; LOG_DEBUG("Upgrading time to quality %s\n", RtcName(q)); } else if (q >= RTCQualityNTP && (now - lastSetMsec) > (12 * 60 * 60 * 1000UL)) { diff --git a/src/gps/RTC.h b/src/gps/RTC.h index 1d609f136..4b065b376 100644 --- a/src/gps/RTC.h +++ b/src/gps/RTC.h @@ -25,7 +25,7 @@ enum RTCQuality { RTCQuality getRTCQuality(); /// If we haven't yet set our RTC this boot, set it from a GPS derived time -bool perhapsSetRTC(RTCQuality q, const struct timeval *tv); +bool perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpdate = false); bool perhapsSetRTC(RTCQuality q, struct tm &t); /// Return a string name for the quality diff --git a/src/graphics/PointStruct.h b/src/graphics/PointStruct.h new file mode 100644 index 000000000..218731978 --- /dev/null +++ b/src/graphics/PointStruct.h @@ -0,0 +1,4 @@ +struct PointStruct { + int x; + int y; +}; \ No newline at end of file diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index ee7506c31..6417d702e 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -419,6 +419,466 @@ static bool shouldDrawMessage(const meshtastic_MeshPacket *packet) return packet->from != 0 && !moduleConfig.store_forward.enabled; } +// Draw power bars or a charging indicator on an image of a battery, determined by battery charge voltage or percentage. +static void drawBattery(OLEDDisplay *display, int16_t x, int16_t y, uint8_t *imgBuffer, const PowerStatus *powerStatus) +{ + static const uint8_t powerBar[3] = {0x81, 0xBD, 0xBD}; + static const uint8_t lightning[8] = {0xA1, 0xA1, 0xA5, 0xAD, 0xB5, 0xA5, 0x85, 0x85}; + // Clear the bar area on the battery image + for (int i = 1; i < 14; i++) { + imgBuffer[i] = 0x81; + } + // If charging, draw a charging indicator + if (powerStatus->getIsCharging()) { + memcpy(imgBuffer + 3, lightning, 8); + // If not charging, Draw power bars + } else { + for (int i = 0; i < 4; i++) { + if (powerStatus->getBatteryChargePercent() >= 25 * i) + memcpy(imgBuffer + 1 + (i * 3), powerBar, 3); + } + } + display->drawFastImage(x, y, 16, 8, imgBuffer); +} + +#ifdef T_WATCH_S3 + +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); + + drawBattery(display, x, y + 7, imgBattery, powerStatus); + + if (powerStatus->getHasBattery()) { + String batteryPercent = String(powerStatus->getBatteryChargePercent()) + "%"; + + display->setFont(FONT_SMALL); + + display->drawString(x + 20, y + 2, batteryPercent); + } + + if (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; + } + + // hours string + String hourString = String(hour); + + // minutes string + String minuteString = minute < 10 ? "0" + String(minute) : String(minute); + + String timeString = hourString + ":" + minuteString; + + // seconds string + String secondString = second < 10 ? "0" + String(second) : String(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 = timeString.length() * 5; + + for (uint8_t i = 0; i < timeString.length(); i++) { + String character = String(timeString[i]); + + if (character == ":") { + timeStringWidth += segmentHeight; + } else { + timeStringWidth += segmentWidth + (segmentHeight * 2) + 4; + } + } + + // calculate seconds string width + uint16_t secondStringWidth = (secondString.length() * 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 < timeString.length(); i++) { + String character = String(timeString[i]); + + if (character == ":") { + drawSegmentedDisplayColon(display, hourMinuteTextX, hourMinuteTextY, scale); + + hourMinuteTextX += segmentHeight + 6; + } else { + drawSegmentedDisplayCharacter(display, hourMinuteTextX, hourMinuteTextY, character.toInt(), 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); + + drawBattery(display, x, y + 7, imgBattery, powerStatus); + + if (powerStatus->getHasBattery()) { + String batteryPercent = String(powerStatus->getBatteryChargePercent()) + "%"; + + display->setFont(FONT_SMALL); + + display->drawString(x + 20, y + 2, batteryPercent); + } + + if (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) { @@ -664,28 +1124,6 @@ static void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char * } } -// Draw power bars or a charging indicator on an image of a battery, determined by battery charge voltage or percentage. -static void drawBattery(OLEDDisplay *display, int16_t x, int16_t y, uint8_t *imgBuffer, const PowerStatus *powerStatus) -{ - static const uint8_t powerBar[3] = {0x81, 0xBD, 0xBD}; - static const uint8_t lightning[8] = {0xA1, 0xA1, 0xA5, 0xAD, 0xB5, 0xA5, 0x85, 0x85}; - // Clear the bar area on the battery image - for (int i = 1; i < 14; i++) { - imgBuffer[i] = 0x81; - } - // If charging, draw a charging indicator - if (powerStatus->getIsCharging()) { - memcpy(imgBuffer + 3, lightning, 8); - // If not charging, Draw power bars - } else { - for (int i = 0; i < 4; i++) { - if (powerStatus->getBatteryChargePercent() >= 25 * i) - memcpy(imgBuffer + 1 + (i * 3), powerBar, 3); - } - } - display->drawFastImage(x, y, 16, 8, imgBuffer); -} - // Draw nodes status static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const NodeStatus *nodeStatus) { @@ -1021,7 +1459,13 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ const char *username = node->has_user ? node->user.long_name : "Unknown Name"; static char signalStr[20]; - snprintf(signalStr, sizeof(signalStr), "Signal: %d%%", clamp((int)((node->snr + 10) * 5), 0, 100)); + + // section here to choose whether to display hops away rather than signal strength if more than 0 hops away. + if (node->hops_away > 0) { + snprintf(signalStr, sizeof(signalStr), "Hops Away: %d", node->hops_away); + } else { + snprintf(signalStr, sizeof(signalStr), "Signal: %d%%", clamp((int)((node->snr + 10) * 5), 0, 100)); + } uint32_t agoSecs = sinceLastSeen(node); static char lastStr[20]; @@ -1377,6 +1821,10 @@ int32_t Screen::runOnce() return RUN_SAME; } + if (displayHeight == 0) { + displayHeight = dispdev->getHeight(); + } + // Show boot screen for first logo_timeout seconds, then switch to normal operation. // serialSinceMsec adjusts for additional serial wait time during nRF52 bootup static bool showingBootScreen = true; @@ -1646,6 +2094,10 @@ void Screen::setFrames() if (error_code) normalFrames[numframes++] = drawCriticalFaultFrame; +#ifdef T_WATCH_S3 + normalFrames[numframes++] = screen->digitalWatchFace ? &Screen::drawDigitalClockFrame : &Screen::drawAnalogClockFrame; +#endif + // If we have a text message - show it next, unless it's a phone message and we aren't using any special modules if (devicestate.has_rx_text_message && shouldDrawMessage(&devicestate.rx_text_message)) { normalFrames[numframes++] = drawTextMessageFrame; @@ -2226,6 +2678,21 @@ int Screen::handleUIFrameEvent(const UIFrameEvent *event) int Screen::handleInputEvent(const InputEvent *event) { + +#ifdef T_WATCH_S3 + // For the T-Watch, intercept touches to the 'toggle digital/analog watch face' button + uint8_t watchFaceFrame = error_code ? 1 : 0; + + if (this->ui->getUiState()->currentFrame == watchFaceFrame && event->touchX >= 204 && event->touchX <= 240 && + event->touchY >= 204 && event->touchY <= 240) { + screen->digitalWatchFace = !screen->digitalWatchFace; + + setFrames(); + + return 0; + } +#endif + if (showingNormalScreen && moduleFrames.size() == 0) { // LOG_DEBUG("Screen::handleInputEvent from %s\n", event->source); if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) { diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index ff3ac798e..c4dc298af 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -52,6 +52,7 @@ class Screen #include "EInkDisplay2.h" #include "EInkDynamicDisplay.h" +#include "PointStruct.h" #include "TFTDisplay.h" #include "TypedQueue.h" #include "commands.h" @@ -81,6 +82,10 @@ class Screen #define EINK_BLACK OLEDDISPLAY_COLOR::WHITE #define EINK_WHITE OLEDDISPLAY_COLOR::BLACK +// Base segment dimensions for T-Watch segmented display +#define SEGMENT_WIDTH 16 +#define SEGMENT_HEIGHT 4 + namespace graphics { @@ -359,7 +364,7 @@ class Screen : public concurrency::OSThread bool enqueueCmd(const ScreenCmd &cmd) { if (!useDisplay) - return true; // claim success if our display is not in use + return false; // not enqueued if our display is not in use else { bool success = cmdQueue.enqueue(cmd, 0); enabled = true; // handle ASAP (we are the registered reader for cmdQueue, but might have been disabled) @@ -393,6 +398,27 @@ class Screen : public concurrency::OSThread static void drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +#ifdef T_WATCH_S3 + 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 + /// Queue of commands to execute in doTask. TypedQueue cmdQueue; /// Whether we are using a display diff --git a/src/graphics/images.h b/src/graphics/images.h index 42b4b5b81..d4c738610 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -14,6 +14,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}; +#ifdef T_WATCH_S3 +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 + #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ST7735_CS) || defined(ST7789_CS) || defined(HX8357_CS) || \ ARCH_PORTDUINO) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) diff --git a/src/input/InputBroker.h b/src/input/InputBroker.h index d73e6657a..57c25af4b 100644 --- a/src/input/InputBroker.h +++ b/src/input/InputBroker.h @@ -8,6 +8,8 @@ typedef struct _InputEvent { const char *source; char inputEvent; char kbchar; + uint16_t touchX; + uint16_t touchY; } InputEvent; class InputBroker : public Observable { diff --git a/src/input/TouchScreenImpl1.cpp b/src/input/TouchScreenImpl1.cpp index c863ead69..20196278d 100644 --- a/src/input/TouchScreenImpl1.cpp +++ b/src/input/TouchScreenImpl1.cpp @@ -49,6 +49,10 @@ void TouchScreenImpl1::onEvent(const TouchEvent &event) { InputEvent e; e.source = event.source; + + e.touchX = event.x; + e.touchY = event.y; + switch (event.touchEvent) { case TOUCH_ACTION_LEFT: { e.inputEvent = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT); diff --git a/src/mesh/generated/meshtastic/admin.pb.h b/src/mesh/generated/meshtastic/admin.pb.h index d692a3f30..2a209ad0a 100644 --- a/src/mesh/generated/meshtastic/admin.pb.h +++ b/src/mesh/generated/meshtastic/admin.pb.h @@ -75,7 +75,7 @@ typedef struct _meshtastic_HamParameters { Ensure your radio is capable of operating of the selected frequency before setting this. */ float frequency; /* Optional short name of user */ - char short_name[6]; + char short_name[5]; } meshtastic_HamParameters; /* Response envelope for node_remote_hardware_pins */ @@ -342,7 +342,7 @@ extern const pb_msgdesc_t meshtastic_NodeRemoteHardwarePinsResponse_msg; /* Maximum encoded size of messages (where known) */ #define MESHTASTIC_MESHTASTIC_ADMIN_PB_H_MAX_SIZE meshtastic_AdminMessage_size #define meshtastic_AdminMessage_size 500 -#define meshtastic_HamParameters_size 32 +#define meshtastic_HamParameters_size 31 #define meshtastic_NodeRemoteHardwarePinsResponse_size 496 #ifdef __cplusplus diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index f2076eeb2..c5dfa1f3c 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -47,6 +47,12 @@ CannedMessageModule::CannedMessageModule() disable(); } else { LOG_INFO("CannedMessageModule is enabled\n"); + + // T-Watch interface currently has no way to select destination type, so default to 'node' +#ifdef T_WATCH_S3 + this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NODE; +#endif + this->inputObserver.observe(inputBroker); } } else { @@ -67,8 +73,16 @@ int CannedMessageModule::splitConfiguredMessages() int messageIndex = 0; int i = 0; + String messages = cannedMessageModuleConfig.messages; + +#ifdef T_WATCH_S3 + String separator = messages.length() ? "|" : ""; + + messages = "[---- Free Text ----]" + separator + messages; +#endif + // collect all the message parts - strncpy(this->messageStore, cannedMessageModuleConfig.messages, sizeof(this->messageStore)); + strncpy(this->messageStore, messages.c_str(), sizeof(this->messageStore)); // The first message points to the beginning of the store. this->messages[messageIndex++] = this->messageStore; @@ -78,7 +92,6 @@ int CannedMessageModule::splitConfiguredMessages() if (this->messageStore[i] == '|') { // Message ending found, replace it with string-end character. this->messageStore[i] = '\0'; - LOG_DEBUG("CannedMessage %d is: '%s'\n", messageIndex - 1, this->messages[messageIndex - 1]); // hit our max messages, bail if (messageIndex >= CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT) { @@ -119,20 +132,30 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) bool validEvent = false; if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP)) { if (this->messagesCount > 0) { - // LOG_DEBUG("Canned message event UP\n"); this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_UP; validEvent = true; } } if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN)) { if (this->messagesCount > 0) { - // LOG_DEBUG("Canned message event DOWN\n"); this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_DOWN; validEvent = true; } } if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT)) { - LOG_DEBUG("Canned message event Select\n"); + +#ifdef T_WATCH_S3 + if (this->currentMessageIndex == 0) { + this->runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + + UIFrameEvent e = {false, true}; + e.frameChanged = true; + this->notifyObservers(&e); + + return 0; + } +#endif + // when inactive, call the onebutton shortpress instead. Activate Module only on up/down if ((this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED)) { powerFSM.trigger(EVENT_PRESS); @@ -143,38 +166,47 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) } } if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL)) { - LOG_DEBUG("Canned message event Cancel\n"); UIFrameEvent e = {false, true}; e.frameChanged = true; this->currentMessageIndex = -1; + +#ifndef T_WATCH_S3 this->freetext = ""; // clear freetext this->cursor = 0; this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; +#endif + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; this->notifyObservers(&e); } if ((event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK)) || (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) || (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT))) { - // LOG_DEBUG("Canned message event (%x)\n", event->kbchar); + +#ifdef T_WATCH_S3 + if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) { + this->payload = 0xb4; + } else if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT)) { + this->payload = 0xb7; + } +#else // tweak for left/right events generated via trackball/touch with empty kbchar if (!event->kbchar) { if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) { this->payload = 0xb4; - // this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NODE; } else if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT)) { this->payload = 0xb7; - // this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NODE; } } else { // pass the pressed key this->payload = event->kbchar; } +#endif + this->lastTouchMillis = millis(); validEvent = true; } if (event->inputEvent == static_cast(ANYKEY)) { - LOG_DEBUG("Canned message event any key pressed\n"); // when inactive, this will switch to the freetext mode if ((this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED)) { @@ -250,8 +282,68 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) screen->removeFunctionSymbal("Fn"); // remove modifier (function) symbal } } + +#ifdef T_WATCH_S3 + if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { + String keyTapped = keyForCoordinates(event->touchX, event->touchY); + + if (keyTapped == "⇧") { + this->highlight = -1; + + this->payload = 0x00; + + validEvent = true; + + this->shift = !this->shift; + } else if (keyTapped == "⌫") { + this->highlight = keyTapped[0]; + + this->payload = 0x08; + + validEvent = true; + + this->shift = false; + } else if (keyTapped == "123" || keyTapped == "ABC") { + this->highlight = -1; + + this->payload = 0x00; + + this->charSet = this->charSet == 0 ? 1 : 0; + + validEvent = true; + } else if (keyTapped == " ") { + this->highlight = keyTapped[0]; + + this->payload = keyTapped[0]; + + validEvent = true; + + this->shift = false; + } else if (keyTapped == "↵") { + this->highlight = 0x00; + + this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; + + this->payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; + + this->currentMessageIndex = event->kbchar - 1; + + validEvent = true; + + this->shift = false; + } else if (keyTapped != "") { + this->highlight = keyTapped[0]; + + this->payload = this->shift ? keyTapped[0] : std::tolower(keyTapped[0]); + + validEvent = true; + + this->shift = false; + } + } +#endif + if (event->inputEvent == static_cast(MATRIXKEY)) { - LOG_DEBUG("Canned message event Matrix key pressed\n"); // this will send the text immediately on matrix press this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; this->payload = MATRIXKEY; @@ -311,17 +403,24 @@ int32_t CannedMessageModule::runOnce() this->currentMessageIndex = -1; this->freetext = ""; // clear freetext this->cursor = 0; + +#ifndef T_WATCH_S3 this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; +#endif + this->notifyObservers(&e); } else if (((this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT)) && ((millis() - this->lastTouchMillis) > INACTIVATE_AFTER_MS)) { // Reset module - LOG_DEBUG("Reset due to lack of activity.\n"); e.frameChanged = true; this->currentMessageIndex = -1; this->freetext = ""; // clear freetext this->cursor = 0; + +#ifndef T_WATCH_S3 this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; +#endif + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; this->notifyObservers(&e); } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) { @@ -330,7 +429,6 @@ int32_t CannedMessageModule::runOnce() sendText(this->dest, indexChannels[this->channel], this->freetext.c_str(), true); this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; } else { - LOG_DEBUG("Reset message is empty.\n"); this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; } } else { @@ -339,11 +437,15 @@ int32_t CannedMessageModule::runOnce() powerFSM.trigger(EVENT_PRESS); return INT32_MAX; } else { +#ifdef T_WATCH_S3 + sendText(this->dest, indexChannels[this->channel], this->messages[this->currentMessageIndex], true); +#else sendText(NODENUM_BROADCAST, channels.getPrimaryIndex(), this->messages[this->currentMessageIndex], true); +#endif } this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; } else { - LOG_DEBUG("Reset message is empty.\n"); + // LOG_DEBUG("Reset message is empty.\n"); this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; } } @@ -351,7 +453,11 @@ int32_t CannedMessageModule::runOnce() this->currentMessageIndex = -1; this->freetext = ""; // clear freetext this->cursor = 0; + +#ifndef T_WATCH_S3 this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; +#endif + this->notifyObservers(&e); return 2000; } else if ((this->runState != CANNED_MESSAGE_RUN_STATE_FREETEXT) && (this->currentMessageIndex == -1)) { @@ -364,7 +470,11 @@ int32_t CannedMessageModule::runOnce() this->currentMessageIndex = getPrevIndex(); this->freetext = ""; // clear freetext this->cursor = 0; + +#ifndef T_WATCH_S3 this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; +#endif + this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; LOG_DEBUG("MOVE UP (%d):%s\n", this->currentMessageIndex, this->getCurrentMessage()); } @@ -373,7 +483,11 @@ int32_t CannedMessageModule::runOnce() this->currentMessageIndex = this->getNextIndex(); this->freetext = ""; // clear freetext this->cursor = 0; + +#ifndef T_WATCH_S3 this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; +#endif + this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; LOG_DEBUG("MOVE DOWN (%d):%s\n", this->currentMessageIndex, this->getCurrentMessage()); } @@ -457,7 +571,7 @@ int32_t CannedMessageModule::runOnce() switch (this->payload) { // code below all trigger the freetext window (where you type to send a message) or reset the // display back to the default window case 0x08: // backspace - if (this->freetext.length() > 0) { + if (this->freetext.length() > 0 && this->highlight == 0x00) { if (this->cursor == this->freetext.length()) { this->freetext = this->freetext.substring(0, this->freetext.length() - 1); } else { @@ -495,13 +609,19 @@ int32_t CannedMessageModule::runOnce() runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; break; default: + if (this->highlight != 0x00) { + break; + } + if (this->cursor == this->freetext.length()) { this->freetext += this->payload; } else { this->freetext = this->freetext.substring(0, this->cursor) + this->payload + this->freetext.substring(this->cursor); } + this->cursor += 1; + uint16_t maxChars = meshtastic_Constants_DATA_PAYLOAD_LEN - (moduleConfig.canned_message.send_bell ? 1 : 0); if (this->freetext.length() > maxChars) { this->cursor = maxChars; @@ -594,6 +714,201 @@ void CannedMessageModule::showTemporaryMessage(const String &message) setIntervalFromNow(2000); } +#ifdef T_WATCH_S3 + +String CannedMessageModule::keyForCoordinates(uint x, uint y) +{ + int outerSize = *(&this->keyboard[this->charSet] + 1) - this->keyboard[this->charSet]; + + for (int8_t outerIndex = 0; outerIndex < outerSize; outerIndex++) { + int innerSize = *(&this->keyboard[this->charSet][outerIndex] + 1) - this->keyboard[this->charSet][outerIndex]; + + for (int8_t innerIndex = 0; innerIndex < innerSize; innerIndex++) { + Letter letter = this->keyboard[this->charSet][outerIndex][innerIndex]; + + if (x > letter.rectX && x < (letter.rectX + letter.rectWidth) && y > letter.rectY && + y < (letter.rectY + letter.rectHeight)) { + return letter.character; + } + } + } + + return ""; +} + +void CannedMessageModule::drawKeyboard(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + int outerSize = *(&this->keyboard[this->charSet] + 1) - this->keyboard[this->charSet]; + + int xOffset = 0; + + int yOffset = 56; + + display->setTextAlignment(TEXT_ALIGN_LEFT); + + display->setFont(FONT_SMALL); + + display->setColor(OLEDDISPLAY_COLOR::WHITE); + + display->drawStringMaxWidth(0, 0, display->getWidth(), + cannedMessageModule->drawWithCursor(cannedMessageModule->freetext, cannedMessageModule->cursor)); + + display->setFont(FONT_MEDIUM); + + int cellHeight = round((display->height() - 64) / outerSize); + + int yCorrection = 8; + + for (int8_t outerIndex = 0; outerIndex < outerSize; outerIndex++) { + yOffset += outerIndex > 0 ? cellHeight : 0; + + int innerSizeBound = *(&this->keyboard[this->charSet][outerIndex] + 1) - this->keyboard[this->charSet][outerIndex]; + + int innerSize = 0; + + for (int8_t innerIndex = 0; innerIndex < innerSizeBound; innerIndex++) { + if (this->keyboard[this->charSet][outerIndex][innerIndex].character != "") { + innerSize++; + } + } + + int cellWidth = display->width() / innerSize; + + for (int8_t innerIndex = 0; innerIndex < innerSize; innerIndex++) { + xOffset += innerIndex > 0 ? cellWidth : 0; + + Letter letter = this->keyboard[this->charSet][outerIndex][innerIndex]; + + Letter updatedLetter = {letter.character, letter.width, xOffset, yOffset, cellWidth, cellHeight}; + + this->keyboard[this->charSet][outerIndex][innerIndex] = updatedLetter; + + float characterOffset = ((cellWidth / 2) - (letter.width / 2)); + + if (letter.character == "⇧") { + if (this->shift) { + display->fillRect(xOffset, yOffset, cellWidth, cellHeight); + + display->setColor(OLEDDISPLAY_COLOR::BLACK); + + drawShiftIcon(display, xOffset + characterOffset, yOffset + yCorrection + 5, 1.2); + + display->setColor(OLEDDISPLAY_COLOR::WHITE); + } else { + display->drawRect(xOffset, yOffset, cellWidth, cellHeight); + + drawShiftIcon(display, xOffset + characterOffset, yOffset + yCorrection + 5, 1.2); + } + } else if (letter.character == "⌫") { + if (this->highlight == letter.character[0]) { + display->fillRect(xOffset, yOffset, cellWidth, cellHeight); + + display->setColor(OLEDDISPLAY_COLOR::BLACK); + + drawBackspaceIcon(display, xOffset + characterOffset, yOffset + yCorrection + 5, 1.2); + + display->setColor(OLEDDISPLAY_COLOR::WHITE); + + setIntervalFromNow(0); + } else { + display->drawRect(xOffset, yOffset, cellWidth, cellHeight); + + drawBackspaceIcon(display, xOffset + characterOffset, yOffset + yCorrection + 5, 1.2); + } + } else if (letter.character == "↵") { + display->drawRect(xOffset, yOffset, cellWidth, cellHeight); + + drawEnterIcon(display, xOffset + characterOffset, yOffset + yCorrection + 5, 1.7); + } else { + if (this->highlight == letter.character[0]) { + display->fillRect(xOffset, yOffset, cellWidth, cellHeight); + + display->setColor(OLEDDISPLAY_COLOR::BLACK); + + display->drawString(xOffset + characterOffset, yOffset + yCorrection, + letter.character == " " ? "space" : letter.character); + + display->setColor(OLEDDISPLAY_COLOR::WHITE); + + setIntervalFromNow(0); + } else { + display->drawRect(xOffset, yOffset, cellWidth, cellHeight); + + display->drawString(xOffset + characterOffset, yOffset + yCorrection, + letter.character == " " ? "space" : letter.character); + } + } + } + + xOffset = 0; + } + + this->highlight = 0x00; +} + +void CannedMessageModule::drawShiftIcon(OLEDDisplay *display, int x, int y, float scale) +{ + PointStruct shiftIcon[10] = {{8, 0}, {15, 7}, {15, 8}, {12, 8}, {12, 12}, {4, 12}, {4, 8}, {1, 8}, {1, 7}, {8, 0}}; + + int size = 10; + + for (int i = 0; i < size - 1; i++) { + int x0 = x + (shiftIcon[i].x * scale); + int y0 = y + (shiftIcon[i].y * scale); + int x1 = x + (shiftIcon[i + 1].x * scale); + int y1 = y + (shiftIcon[i + 1].y * scale); + + display->drawLine(x0, y0, x1, y1); + } +} + +void CannedMessageModule::drawBackspaceIcon(OLEDDisplay *display, int x, int y, float scale) +{ + PointStruct backspaceIcon[6] = {{0, 7}, {5, 2}, {15, 2}, {15, 12}, {5, 12}, {0, 7}}; + + int size = 6; + + for (int i = 0; i < size - 1; i++) { + int x0 = x + (backspaceIcon[i].x * scale); + int y0 = y + (backspaceIcon[i].y * scale); + int x1 = x + (backspaceIcon[i + 1].x * scale); + int y1 = y + (backspaceIcon[i + 1].y * scale); + + display->drawLine(x0, y0, x1, y1); + } + + PointStruct backspaceIconX[4] = {{7, 4}, {13, 10}, {7, 10}, {13, 4}}; + + size = 4; + + for (int i = 0; i < size - 1; i++) { + int x0 = x + (backspaceIconX[i].x * scale); + int y0 = y + (backspaceIconX[i].y * scale); + int x1 = x + (backspaceIconX[i + 1].x * scale); + int y1 = y + (backspaceIconX[i + 1].y * scale); + + display->drawLine(x0, y0, x1, y1); + } +} + +void CannedMessageModule::drawEnterIcon(OLEDDisplay *display, int x, int y, float scale) +{ + PointStruct enterIcon[6] = {{0, 7}, {4, 3}, {4, 11}, {0, 7}, {15, 7}, {15, 0}}; + + int size = 6; + + for (int i = 0; i < size - 1; i++) { + int x0 = x + (enterIcon[i].x * scale); + int y0 = y + (enterIcon[i].y * scale); + int x1 = x + (enterIcon[i + 1].x * scale); + int y1 = y + (enterIcon[i + 1].y * scale); + + display->drawLine(x0, y0, x1, y1); + } +} + +#endif + #if !HAS_TFT void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { @@ -615,6 +930,16 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st } display->drawStringf(display->getWidth() / 2 + x, 0 + y + 12, buffer, displayString, cannedMessageModule->getNodeName(this->incoming)); + + display->setFont(FONT_SMALL); + + String snrString = "Last Rx SNR: %f"; + String rssiString = "Last Rx RSSI: %d"; + + if (this->ack) { + display->drawStringf(display->getWidth() / 2 + x, y + 100, buffer, snrString, this->lastRxSnr); + display->drawStringf(display->getWidth() / 2 + x, y + 130, buffer, rssiString, this->lastRxRssi); + } } else if (cannedMessageModule->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) { display->setTextAlignment(TEXT_ALIGN_CENTER); display->setFont(FONT_MEDIUM); @@ -624,6 +949,11 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st display->setFont(FONT_SMALL); display->drawString(10 + x, 0 + y + FONT_HEIGHT_SMALL, "Canned Message\nModule disabled."); } else if (cannedMessageModule->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { + +#ifdef T_WATCH_S3 + drawKeyboard(display, state, 0, 0); +#else + display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); if (this->destSelect != CANNED_MESSAGE_DESTINATION_TYPE_NONE) { @@ -664,6 +994,7 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st display->drawStringMaxWidth( 0 + x, 0 + y + FONT_HEIGHT_SMALL, x + display->getWidth(), cannedMessageModule->drawWithCursor(cannedMessageModule->freetext, cannedMessageModule->cursor)); +#endif } else { if (this->messagesCount > 0) { display->setTextAlignment(TEXT_ALIGN_LEFT); diff --git a/src/modules/CannedMessageModule.h b/src/modules/CannedMessageModule.h index 215bfa321..ab21e513e 100644 --- a/src/modules/CannedMessageModule.h +++ b/src/modules/CannedMessageModule.h @@ -22,6 +22,17 @@ enum cannedMessageDestinationType { CANNED_MESSAGE_DESTINATION_TYPE_CHANNEL }; +enum CannedMessageModuleIconType { shift, backspace, space, enter }; + +struct Letter { + String character; + float width; + int rectX; + int rectY; + int rectWidth; + int rectHeight; +}; + #define CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT 50 /** * Sum of CannedMessageModuleConfig part sizes. @@ -61,6 +72,14 @@ class CannedMessageModule : public SinglePortModule, public Observablerx_rssi != 0) { + this->lastRxRssi = p->rx_rssi; + } + + if (p->rx_snr > 0) { + this->lastRxSnr = p->rx_snr; + } + switch (p->decoded.portnum) { case meshtastic_PortNum_TEXT_MESSAGE_APP: case meshtastic_PortNum_ROUTING_APP: @@ -79,6 +98,18 @@ class CannedMessageModule : public SinglePortModule, public ObservableshouldDraw(); } virtual Observable *getUIFrameObservable() override { return this; } @@ -112,12 +143,84 @@ class CannedMessageModule : public SinglePortModule, public ObservablesetLocalPosition(p, true); return false; } else { @@ -71,8 +80,17 @@ bool PositionModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mes p.time); if (p.time && channels.getByIndex(mp.channel).role == meshtastic_Channel_Role_PRIMARY) { + bool force = false; + +#ifdef T_WATCH_S3 + // The T-Watch appears to "pause" its RTC when shut down, such that the time it reads upon powering on is the same as when + // it was shut down. So we need to force the update here, since otherwise RTC::perhapsSetRTC will ignore it because it + // will always be an equivalent or lesser RTCQuality (RTCQualityNTP or RTCQualityNet). + force = true; +#endif + // Set from phone RTC Quality to RTCQualityNTP since it should be approximately so - trySetRtc(p, isLocal); + trySetRtc(p, isLocal, force); } nodeDB->updatePosition(getFrom(&mp), p); @@ -104,14 +122,14 @@ void PositionModule::alterReceivedProtobuf(meshtastic_MeshPacket &mp, meshtastic } } -void PositionModule::trySetRtc(meshtastic_Position p, bool isLocal) +void PositionModule::trySetRtc(meshtastic_Position p, bool isLocal, bool forceUpdate) { struct timeval tv; uint32_t secs = p.time; tv.tv_sec = secs; tv.tv_usec = 0; - perhapsSetRTC(isLocal ? RTCQualityNTP : RTCQualityFromNet, &tv); + perhapsSetRTC(isLocal ? RTCQualityNTP : RTCQualityFromNet, &tv, forceUpdate); } meshtastic_MeshPacket *PositionModule::allocReply() diff --git a/src/modules/PositionModule.h b/src/modules/PositionModule.h index 1161159f7..763b51e5c 100644 --- a/src/modules/PositionModule.h +++ b/src/modules/PositionModule.h @@ -54,7 +54,7 @@ class PositionModule : public ProtobufModule, private concu private: struct SmartPosition getDistanceTraveledSinceLastSend(meshtastic_PositionLite currentPosition); meshtastic_MeshPacket *allocAtakPli(); - void trySetRtc(meshtastic_Position p, bool isLocal); + void trySetRtc(meshtastic_Position p, bool isLocal, bool forceUpdate = false); uint32_t precision; void sendLostAndFoundText();