From 1daf5aad1ff4a84071e3e09cc05252ab21811963 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 21 Aug 2025 06:29:23 -0500 Subject: [PATCH 01/54] Revert "Add on-screen keyboard implementation on Trackball device. (#7625)" (#7704) This reverts commit fe3f14a63e2dbd160eabfcb8fdf1fb7cfb8eb1db. --- src/buzz/BuzzerFeedbackThread.cpp | 1 - src/graphics/Screen.cpp | 75 +-- src/graphics/Screen.h | 4 +- src/graphics/VirtualKeyboard.cpp | 738 --------------------- src/graphics/VirtualKeyboard.h | 80 --- src/graphics/draw/MenuHandler.cpp | 3 - src/graphics/draw/NotificationRenderer.cpp | 109 +-- src/graphics/draw/NotificationRenderer.h | 6 - src/input/InputBroker.h | 1 - src/input/TrackballInterruptBase.cpp | 76 +-- src/input/TrackballInterruptBase.h | 13 +- src/input/TrackballInterruptImpl1.cpp | 7 +- src/main.cpp | 6 - src/main.h | 1 - src/modules/CannedMessageModule.cpp | 136 +--- 15 files changed, 30 insertions(+), 1226 deletions(-) delete mode 100644 src/graphics/VirtualKeyboard.cpp delete mode 100644 src/graphics/VirtualKeyboard.h diff --git a/src/buzz/BuzzerFeedbackThread.cpp b/src/buzz/BuzzerFeedbackThread.cpp index 838224c69..ce762c764 100644 --- a/src/buzz/BuzzerFeedbackThread.cpp +++ b/src/buzz/BuzzerFeedbackThread.cpp @@ -28,7 +28,6 @@ int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event) case INPUT_BROKER_USER_PRESS: case INPUT_BROKER_ALT_PRESS: case INPUT_BROKER_SELECT: - case INPUT_BROKER_SELECT_LONG: playBeep(); // Confirmation feedback break; diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 5e29814cb..fa71e17d8 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -216,44 +216,6 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t ui->update(); } -void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs, - std::function textCallback) -{ - LOG_INFO("showTextInput called with header='%s', durationMs=%d", header ? header : "NULL", durationMs); - - if (NotificationRenderer::virtualKeyboard) { - delete NotificationRenderer::virtualKeyboard; - NotificationRenderer::virtualKeyboard = nullptr; - } - - NotificationRenderer::textInputCallback = nullptr; - - NotificationRenderer::virtualKeyboard = new VirtualKeyboard(); - if (header) { - NotificationRenderer::virtualKeyboard->setHeader(header); - } - if (initialText) { - NotificationRenderer::virtualKeyboard->setInputText(initialText); - } - - // Set up callback with safer cleanup mechanism - NotificationRenderer::textInputCallback = textCallback; - NotificationRenderer::virtualKeyboard->setCallback([textCallback](const std::string &text) { textCallback(text); }); - - // Store the message and set the expiration timestamp (use same pattern as other notifications) - strncpy(NotificationRenderer::alertBannerMessage, header ? header : "Text Input", 255); - NotificationRenderer::alertBannerMessage[255] = '\0'; - NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs; - NotificationRenderer::pauseBanner = false; - NotificationRenderer::current_notification_type = notificationTypeEnum::text_input; - - // Set the overlay using the same pattern as other notification types - static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; - ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); - ui->setTargetFPS(60); - ui->update(); -} - static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { uint8_t module_frame; @@ -751,19 +713,13 @@ int32_t Screen::runOnce() handleSetOn(false); break; case Cmd::ON_PRESS: - if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { - handleOnPress(); - } + handleOnPress(); break; case Cmd::SHOW_PREV_FRAME: - if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { - handleShowPrevFrame(); - } + handleShowPrevFrame(); break; case Cmd::SHOW_NEXT_FRAME: - if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { - handleShowNextFrame(); - } + handleShowNextFrame(); break; case Cmd::START_ALERT_FRAME: { showingBootScreen = false; // this should avoid the edge case where an alert triggers before the boot screen goes away @@ -785,9 +741,7 @@ int32_t Screen::runOnce() NotificationRenderer::pauseBanner = false; case Cmd::STOP_BOOT_SCREEN: EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame - if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { - setFrames(); - } + setFrames(); break; case Cmd::NOOP: break; @@ -823,7 +777,6 @@ int32_t Screen::runOnce() if (showingNormalScreen) { // standard screen loop handling here if (config.display.auto_screen_carousel_secs > 0 && - NotificationRenderer::current_notification_type != notificationTypeEnum::text_input && !Throttle::isWithinTimespanMs(lastScreenTransition, config.display.auto_screen_carousel_secs * 1000)) { // If an E-Ink display struggles with fast refresh, force carousel to use full refresh instead @@ -914,11 +867,6 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver) // Called when a frame should be added / removed, or custom frames should be cleared void Screen::setFrames(FrameFocus focus) { - // Block setFrames calls when virtual keyboard is active to prevent overlay interference - if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { - return; - } - uint8_t originalPosition = ui->getUiState()->currentFrame; uint8_t previousFrameCount = framesetInfo.frameCount; FramesetInfo fsi; // Location of specific frames, for applying focus parameter @@ -1365,11 +1313,6 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) // Triggered by MeshModules int Screen::handleUIFrameEvent(const UIFrameEvent *event) { - // Block UI frame events when virtual keyboard is active - if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { - return 0; - } - if (showingNormalScreen) { // Regenerate the frameset, potentially honoring a module's internal requestFocus() call if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET) @@ -1392,16 +1335,6 @@ int Screen::handleInputEvent(const InputEvent *event) if (!screenOn) return 0; - // Handle text input notifications specially - pass input to virtual keyboard - if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { - NotificationRenderer::inEvent = *event; - static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; - ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); - setFastFramerate(); // Draw ASAP - ui->update(); - return 0; - } - #ifdef USE_EINK // the screen is the last input handler, so if an event makes it here, we can assume it will prompt a screen draw. EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 0f100d455..265900131 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -12,7 +12,7 @@ #define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2) namespace graphics { -enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker, text_input }; +enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker }; struct BannerOverlayOptions { const char *message; @@ -313,8 +313,6 @@ class Screen : public concurrency::OSThread void showNodePicker(const char *message, uint32_t durationMs, std::function bannerCallback); void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function bannerCallback); - void showTextInput(const char *header, const char *initialText, uint32_t durationMs, - std::function textCallback); void requestMenu(graphics::menuHandler::screenMenus menuToShow) { diff --git a/src/graphics/VirtualKeyboard.cpp b/src/graphics/VirtualKeyboard.cpp deleted file mode 100644 index 84d5551cb..000000000 --- a/src/graphics/VirtualKeyboard.cpp +++ /dev/null @@ -1,738 +0,0 @@ -#include "VirtualKeyboard.h" -#include "configuration.h" -#include "graphics/Screen.h" -#include "graphics/ScreenFonts.h" -#include "graphics/SharedUIDisplay.h" -#include "main.h" -#include -#include - -namespace graphics -{ - -VirtualKeyboard::VirtualKeyboard() : cursorRow(0), cursorCol(0), lastActivityTime(millis()) -{ - initializeKeyboard(); - // Set cursor to H(2, 5) - cursorRow = 2; - cursorCol = 5; -} - -VirtualKeyboard::~VirtualKeyboard() {} - -void VirtualKeyboard::initializeKeyboard() -{ - // New 4 row, 11 column keyboard layout: - static const char LAYOUT[KEYBOARD_ROWS][KEYBOARD_COLS] = {{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '\b'}, - {'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '\n'}, - {'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', ' '}, - {'z', 'x', 'c', 'v', 'b', 'n', 'm', '.', ',', '?', '\x1b'}}; - - // Derive layout dimensions and assert they match the configured keyboard grid - constexpr int LAYOUT_ROWS = (int)(sizeof(LAYOUT) / sizeof(LAYOUT[0])); - constexpr int LAYOUT_COLS = (int)(sizeof(LAYOUT[0]) / sizeof(LAYOUT[0][0])); - static_assert(LAYOUT_ROWS == KEYBOARD_ROWS, "LAYOUT rows must equal KEYBOARD_ROWS"); - static_assert(LAYOUT_COLS == KEYBOARD_COLS, "LAYOUT cols must equal KEYBOARD_COLS"); - - // Initialize all keys to empty first - for (int row = 0; row < LAYOUT_ROWS; row++) { - for (int col = 0; col < LAYOUT_COLS; col++) { - keyboard[row][col] = {0, VK_CHAR, 0, 0, 0, 0}; - } - } - - // Fill keyboard from the 2D layout - for (int row = 0; row < LAYOUT_ROWS; row++) { - for (int col = 0; col < LAYOUT_COLS; col++) { - char ch = LAYOUT[row][col]; - // No empty slots in the simplified layout - - VirtualKeyType type = VK_CHAR; - if (ch == '\b') { - type = VK_BACKSPACE; - } else if (ch == '\n') { - type = VK_ENTER; - } else if (ch == '\x1b') { // ESC - type = VK_ESC; - } else if (ch == ' ') { - type = VK_SPACE; - } - - // Make action keys wider to fit text while keeping the last column aligned - uint8_t width = (type == VK_BACKSPACE || type == VK_ENTER || type == VK_SPACE) ? (KEY_WIDTH * 3) : KEY_WIDTH; - keyboard[row][col] = {ch, type, (uint8_t)(col * KEY_WIDTH), (uint8_t)(row * KEY_HEIGHT), width, KEY_HEIGHT}; - } - } -} - -void VirtualKeyboard::draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY) -{ - // Repeat ticking is driven by NotificationRenderer once per frame - // Base styles - display->setColor(WHITE); - display->setFont(FONT_SMALL); - - // Screen geometry - const int screenW = display->getWidth(); - const int screenH = display->getHeight(); - - // Decide wide-screen mode: if there is comfortable width, allow taller keys and reserve fixed width for last column labels - // Heuristic: if screen width >= 200px (e.g., 240x135), treat as wide - const bool isWide = screenW >= 200; - - // Determine last-column label max width - display->setFont(FONT_SMALL); - const int wENTER = display->getStringWidth("ENTER"); - int lastColLabelW = wENTER; // ENTER is usually the widest - // Smaller padding on very small screens to avoid excessive whitespace - const int lastColPad = (screenW <= 128 ? 2 : 6); - const int reservedLastColW = lastColLabelW + lastColPad; // reserved width for last column keys - - // Always reserve width for the rightmost text column to avoid overlap on small screens - int cellW = 0; - int leftoverW = 0; - { - const int leftCols = KEYBOARD_COLS - 1; // 10 input characters - int usableW = screenW - reservedLastColW; - if (usableW < leftCols) { - // Guard: ensure at least 1px per left cell if labels are extremely wide (unlikely) - usableW = leftCols; - } - cellW = usableW / leftCols; - leftoverW = usableW - cellW * leftCols; // distribute extra pixels over left columns (left to right) - } - - // Dynamic key geometry - int cellH = KEY_HEIGHT; - int keyboardStartY = 0; - if (screenH <= 64) { - const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL - 2); - const int gapBelowHeader = 0; - const int singleLineBoxHeight = FONT_HEIGHT_SMALL; - const int gapAboveKeyboard = 0; - keyboardStartY = offsetY + headerHeight + gapBelowHeader + singleLineBoxHeight + gapAboveKeyboard; - if (keyboardStartY < 0) - keyboardStartY = 0; - if (keyboardStartY > screenH) - keyboardStartY = screenH; - int keyboardHeight = screenH - keyboardStartY; - cellH = std::max(1, keyboardHeight / KEYBOARD_ROWS); - } else if (isWide) { - // For wide screens (e.g., T114 240x135), prefer square keys: height equals left-column key width. - cellH = std::max((int)KEY_HEIGHT, cellW); - - // Guarantee at least 2 lines of input are visible by reducing cell height minimally if needed. - // Replicate the spacing used in drawInputArea(): headerGap=1, box-to-header gap=1, gap above keyboard=1 - display->setFont(FONT_SMALL); - const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL + 1); - const int headerToBoxGap = 1; - const int gapAboveKb = 1; - const int minBoxHeightForTwoLines = 2 * FONT_HEIGHT_SMALL + 2; // inner 1px top/bottom - int maxKeyboardHeight = screenH - (offsetY + headerHeight + headerToBoxGap + minBoxHeightForTwoLines + gapAboveKb); - int maxCellHAllowed = maxKeyboardHeight / KEYBOARD_ROWS; - if (maxCellHAllowed < (int)KEY_HEIGHT) - maxCellHAllowed = KEY_HEIGHT; - if (maxCellHAllowed > 0 && cellH > maxCellHAllowed) { - cellH = maxCellHAllowed; - } - // Keyboard placement from bottom for wide screens - int keyboardHeight = KEYBOARD_ROWS * cellH; - keyboardStartY = screenH - keyboardHeight; - if (keyboardStartY < 0) - keyboardStartY = 0; - } else { - // Default (non-wide, non-64px) behavior: use key height heuristic and place at bottom - cellH = KEY_HEIGHT; - int keyboardHeight = KEYBOARD_ROWS * cellH; - keyboardStartY = screenH - keyboardHeight; - if (keyboardStartY < 0) - keyboardStartY = 0; - } - - // Draw input area above keyboard - drawInputArea(display, offsetX, offsetY, keyboardStartY); - - // Precompute per-column x and width with leftover distributed over left columns for even spacing - int colX[KEYBOARD_COLS]; - int colW[KEYBOARD_COLS]; - int runningX = offsetX; - for (int col = 0; col < KEYBOARD_COLS - 1; ++col) { - int wcol = cellW + (col < leftoverW ? 1 : 0); - colX[col] = runningX; - colW[col] = wcol; - runningX += wcol; - } - // Last column - colX[KEYBOARD_COLS - 1] = runningX; - colW[KEYBOARD_COLS - 1] = reservedLastColW; - - // Draw keyboard grid - for (int row = 0; row < KEYBOARD_ROWS; row++) { - for (int col = 0; col < KEYBOARD_COLS; col++) { - const VirtualKey &k = keyboard[row][col]; - if (k.character != 0 || k.type != VK_CHAR) { - const bool isLastCol = (col == KEYBOARD_COLS - 1); - int x = colX[col]; - int w = colW[col]; - int y = offsetY + keyboardStartY + row * cellH; - int h = cellH; - bool selected = (row == cursorRow && col == cursorCol); - drawKey(display, k, selected, x, y, (uint8_t)w, (uint8_t)h, isLastCol); - } - } - } -} - -void VirtualKeyboard::drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY) -{ - display->setColor(WHITE); - - const int screenWidth = display->getWidth(); - const int screenHeight = display->getHeight(); - // Use the standard small font metrics for input box sizing (restore original size) - const int inputLineH = FONT_HEIGHT_SMALL; - - // Header uses the standard small (which may be larger on big screens) - display->setFont(FONT_SMALL); - int headerHeight = 0; - if (!headerText.empty()) { - // Draw header and reserve exact font height (plus a tighter gap) to maximize input area - display->drawString(offsetX + 2, offsetY, headerText.c_str()); - if (screenHeight <= 64) { - headerHeight = FONT_HEIGHT_SMALL - 2; // 11px - } else { - headerHeight = FONT_HEIGHT_SMALL; // no extra padding baked in - } - } - - const int boxX = offsetX; - const int boxWidth = screenWidth; - int boxY; - int boxHeight; - if (screenHeight <= 64) { - const int gapBelowHeader = 0; - const int fixedBoxHeight = inputLineH; - const int gapAboveKeyboard = 0; - boxY = offsetY + headerHeight + gapBelowHeader; - boxHeight = fixedBoxHeight; - if (boxY + boxHeight + gapAboveKeyboard > keyboardStartY) { - int over = boxY + boxHeight + gapAboveKeyboard - keyboardStartY; - boxHeight = std::max(1, fixedBoxHeight - over); - } - } else { - const int gapBelowHeader = 1; - int gapAboveKeyboard = 1; - int tmpBoxY = offsetY + headerHeight + gapBelowHeader; - const int minBoxHeight = inputLineH + 2; - int availableH = keyboardStartY - tmpBoxY - gapAboveKeyboard; - if (availableH < minBoxHeight) - availableH = minBoxHeight; - boxY = tmpBoxY; - boxHeight = availableH; - } - - // Draw box border - display->drawRect(boxX, boxY, boxWidth, boxHeight); - - display->setFont(FONT_SMALL); - - // Text rendering: multi-line if space allows (>= 2 lines), else single-line with leading ellipsis - const int textX = boxX + 2; - const int maxTextWidth = boxWidth - 4; - const int maxLines = (boxHeight - 2) / inputLineH; - if (maxLines >= 2) { - // Inner bounds for caret clamping - const int innerLeft = boxX + 1; - const int innerRight = boxX + boxWidth - 2; - const int innerTop = boxY + 1; - const int innerBottom = boxY + boxHeight - 2; - - // Wrap text greedily into lines that fit maxTextWidth - std::vector lines; - { - std::string remaining = inputText; - while (!remaining.empty()) { - int bestLen = 0; - for (int len = 1; len <= (int)remaining.size(); ++len) { - int w = display->getStringWidth(remaining.substr(0, len).c_str()); - if (w <= maxTextWidth) - bestLen = len; - else - break; - } - if (bestLen == 0) { - // At least show one character to make progress - bestLen = 1; - } - lines.emplace_back(remaining.substr(0, bestLen)); - remaining.erase(0, bestLen); - } - } - - const bool scrolledUp = ((int)lines.size() > maxLines); - int caretX = textX; - int caretY = innerTop; - - // Leave a small top gap to render '...' without replacing the first line - const int topInset = 2; - const int lineStep = std::max(1, inputLineH - 1); // slightly tighter than font height - int lineY = innerTop + topInset; - - if (scrolledUp) { - // Draw three small dots centered horizontally, vertically at the midpoint of the gap - // between the inner top and the first line's top baseline. This avoids using a tall glyph. - const int firstLineTop = lineY; // baseline top for the first visible line - const int gapMidY = innerTop + (firstLineTop - innerTop) / 2 + 1; // shift down 1px as requested - const int centerX = boxX + boxWidth / 2; - const int dotSpacing = 3; // px between dots - const int dotSize = 1; // small square dot - display->fillRect(centerX - dotSpacing, gapMidY, dotSize, dotSize); - display->fillRect(centerX, gapMidY, dotSize, dotSize); - display->fillRect(centerX + dotSpacing, gapMidY, dotSize, dotSize); - } - - // How many lines fit with our top inset and tighter step - const int linesCapacity = std::max(1, (innerBottom - lineY + 1) / lineStep); - const int linesToShow = std::min((int)lines.size(), linesCapacity); - const int startIndex = scrolledUp ? ((int)lines.size() - linesToShow) : 0; - - for (int i = 0; i < linesToShow; ++i) { - const std::string &chunk = lines[startIndex + i]; - display->drawString(textX, lineY, chunk.c_str()); - caretX = textX + display->getStringWidth(chunk.c_str()); - caretY = lineY; - lineY += lineStep; - } - - // Draw caret at end of the last visible line - int caretPadY = 2; - if (boxHeight >= inputLineH + 4) - caretPadY = 3; - int cursorTop = caretY + caretPadY; - // Use lineStep so caret height matches the row spacing - int cursorH = lineStep - caretPadY * 2; - if (cursorH < 1) - cursorH = 1; - // Clamp vertical bounds to stay inside the inner rect - if (cursorTop < innerTop) - cursorTop = innerTop; - if (cursorTop + cursorH - 1 > innerBottom) - cursorH = innerBottom - cursorTop + 1; - if (cursorH < 1) - cursorH = 1; - // Only draw if cursor is inside inner bounds - if (caretX >= innerLeft && caretX <= innerRight) { - display->drawVerticalLine(caretX, cursorTop, cursorH); - } - } else { - std::string displayText = inputText; - int textW = display->getStringWidth(displayText.c_str()); - std::string scrolled = displayText; - if (textW > maxTextWidth) { - // Trim from the left until it fits - while (textW > maxTextWidth && !scrolled.empty()) { - scrolled.erase(0, 1); - textW = display->getStringWidth(scrolled.c_str()); - } - // Add leading ellipsis and ensure it still fits - if (scrolled != displayText) { - scrolled = "..." + scrolled; - textW = display->getStringWidth(scrolled.c_str()); - // If adding ellipsis causes overflow, trim more after the ellipsis - while (textW > maxTextWidth && scrolled.size() > 3) { - scrolled.erase(3, 1); // remove chars after the ellipsis - textW = display->getStringWidth(scrolled.c_str()); - } - } - } else { - // Keep textW in sync with what we draw - textW = display->getStringWidth(scrolled.c_str()); - } - - int textY; - if (screenHeight <= 64) { - textY = boxY + (boxHeight - inputLineH) / 2; - } else { - const int innerLeft = boxX + 1; - const int innerRight = boxX + boxWidth - 2; - const int innerTop = boxY + 1; - const int innerBottom = boxY + boxHeight - 2; - - // Center text vertically within inner box for single-line, then clamp so it never overlaps borders - int innerH = innerBottom - innerTop + 1; - textY = innerTop + std::max(0, (innerH - inputLineH) / 2); - // Clamp fully inside the inner rect - if (textY < innerTop) - textY = innerTop; - int maxTop = innerBottom - inputLineH + 1; - if (textY > maxTop) - textY = maxTop; - } - - if (!scrolled.empty()) { - display->drawString(textX, textY, scrolled.c_str()); - } - - int cursorX = textX + textW; - if (screenHeight > 64) { - const int innerRight = boxX + boxWidth - 2; - if (cursorX > innerRight) - cursorX = innerRight; - } - - int cursorTop, cursorH; - if (screenHeight <= 64) { - cursorH = 10; - cursorTop = boxY + (boxHeight - cursorH) / 2; - } else { - const int innerLeft = boxX + 1; - const int innerRight = boxX + boxWidth - 2; - const int innerTop = boxY + 1; - const int innerBottom = boxY + boxHeight - 2; - - cursorTop = boxY + 2; - cursorH = boxHeight - 4; - if (cursorH < 1) - cursorH = 1; - if (cursorTop < innerTop) - cursorTop = innerTop; - if (cursorTop + cursorH - 1 > innerBottom) - cursorH = innerBottom - cursorTop + 1; - if (cursorH < 1) - cursorH = 1; - - if (cursorX < innerLeft || cursorX > innerRight) - return; - } - - display->drawVerticalLine(cursorX, cursorTop, cursorH); - } -} - -void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t width, - uint8_t height, bool isLastCol) -{ - // Draw key content - display->setFont(FONT_SMALL); - const int fontH = FONT_HEIGHT_SMALL; - // Build label and metrics first - std::string keyText; - if (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC) { - // Keep literal text labels for the action keys on the rightmost column - keyText = (key.type == VK_BACKSPACE) ? "BACK" - : (key.type == VK_ENTER) ? "ENTER" - : (key.type == VK_SPACE) ? "SPACE" - : (key.type == VK_ESC) ? "ESC" - : ""; - } else { - char c = getCharForKey(key, false); - if (c >= 'a' && c <= 'z') { - c = c - 'a' + 'A'; - } - keyText = (key.character == ' ' || key.character == '_') ? "_" : std::string(1, c); - } - - int textWidth = display->getStringWidth(keyText.c_str()); - // Label alignment - // - Rightmost action column: right-align text with a small right padding (~2px) so it hugs screen edge neatly. - // - Other keys: center horizontally; use ceil-style rounding to avoid appearing left-biased on odd widths. - int textX; - if (isLastCol) { - const int rightPad = 1; - textX = x + width - textWidth - rightPad; - if (textX < x) - textX = x; // guard - } else { - if (display->getHeight() <= 64 && (key.character >= '0' && key.character <= '9')) { - textX = x + (width - textWidth + 1) / 2; - } else { - textX = x + (width - textWidth) / 2; - } - } - int contentTop = y; - int contentH = height; - if (selected) { - display->setColor(WHITE); - bool isAction = (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC); - - if (display->getHeight() <= 64 && !isAction) { - display->fillRect(x, y, width, height); - } else if (isAction) { - const int padX = 1; - const int padY = 2; - int hlW = textWidth + padX * 2; - int hlX = textX - padX; - - if (hlX < x) { - hlW -= (x - hlX); - hlX = x; - } - int maxW = (x + width) - hlX; - if (hlW > maxW) - hlW = maxW; - if (hlW < 1) - hlW = 1; - - int hlH = std::min(fontH + padY * 2, (int)height); - int hlY = y + (height - hlH) / 2; - display->fillRect(hlX, hlY, hlW, hlH); - contentTop = hlY; - contentH = hlH; - } else { - display->fillRect(x, y, width, height); - } - display->setColor(BLACK); - } else { - display->setColor(WHITE); - } - - int centeredTextY; - if (display->getHeight() <= 64) { - centeredTextY = y + (height - fontH) / 2; - } else { - centeredTextY = contentTop + (contentH - fontH) / 2; - } - if (display->getHeight() > 64) { - if (centeredTextY < contentTop) - centeredTextY = contentTop; - if (centeredTextY + fontH > contentTop + contentH) - centeredTextY = std::max(contentTop, contentTop + contentH - fontH); - } - - if (display->getHeight() <= 64 && keyText.size() == 1) { - char ch = keyText[0]; - if (ch == '.' || ch == ',' || ch == ';') { - centeredTextY -= 1; - } - } - display->drawString(textX, centeredTextY, keyText.c_str()); -} - -char VirtualKeyboard::getCharForKey(const VirtualKey &key, bool isLongPress) -{ - if (key.type != VK_CHAR) { - return key.character; - } - - char c = key.character; - - // Long-press: only keep letter lowercase->uppercase conversion; remove other symbol mappings - if (isLongPress && c >= 'a' && c <= 'z') { - c = (char)(c - 'a' + 'A'); - } - - return c; -} - -void VirtualKeyboard::moveCursorDelta(int dRow, int dCol) -{ - resetTimeout(); - // wrap around rows and cols in the 4x11 grid - int r = (int)cursorRow + dRow; - int c = (int)cursorCol + dCol; - if (r < 0) - r = KEYBOARD_ROWS - 1; - else if (r >= KEYBOARD_ROWS) - r = 0; - if (c < 0) - c = KEYBOARD_COLS - 1; - else if (c >= KEYBOARD_COLS) - c = 0; - cursorRow = (uint8_t)r; - cursorCol = (uint8_t)c; -} - -void VirtualKeyboard::moveCursorUp() -{ - moveCursorDelta(-1, 0); -} -void VirtualKeyboard::moveCursorDown() -{ - moveCursorDelta(1, 0); -} -void VirtualKeyboard::moveCursorLeft() -{ - resetTimeout(); - - if (cursorCol > 0) { - cursorCol--; - } else { - if (cursorRow > 0) { - cursorRow--; - cursorCol = KEYBOARD_COLS - 1; - } else { - cursorRow = KEYBOARD_ROWS - 1; - cursorCol = KEYBOARD_COLS - 1; - } - } -} -void VirtualKeyboard::moveCursorRight() -{ - resetTimeout(); - - if (cursorCol < KEYBOARD_COLS - 1) { - cursorCol++; - } else { - if (cursorRow < KEYBOARD_ROWS - 1) { - cursorRow++; - cursorCol = 0; - } else { - cursorRow = 0; - cursorCol = 0; - } - } -} - -void VirtualKeyboard::handlePress() -{ - resetTimeout(); // Reset timeout on any input activity - - const VirtualKey &key = keyboard[cursorRow][cursorCol]; - - // Don't handle press if the key is empty (but allow special keys) - if (key.character == 0 && key.type == VK_CHAR) { - return; - } - - // For character keys, insert lowercase character - if (key.type == VK_CHAR) { - insertCharacter(getCharForKey(key, false)); // false = lowercase/normal char - return; - } - - // Handle non-character keys immediately - switch (key.type) { - case VK_BACKSPACE: - deleteCharacter(); - break; - case VK_ENTER: - submitText(); - break; - case VK_SPACE: - insertCharacter(' '); - break; - case VK_ESC: - if (onTextEntered) { - std::function callback = onTextEntered; - onTextEntered = nullptr; - inputText = ""; - callback(""); - } - return; - default: - break; - } -} - -void VirtualKeyboard::handleLongPress() -{ - resetTimeout(); // Reset timeout on any input activity - - const VirtualKey &key = keyboard[cursorRow][cursorCol]; - - // Don't handle press if the key is empty (but allow special keys) - if (key.character == 0 && key.type == VK_CHAR) { - return; - } - - // For character keys, insert uppercase/alternate character - if (key.type == VK_CHAR) { - insertCharacter(getCharForKey(key, true)); // true = uppercase/alternate char - return; - } - - switch (key.type) { - case VK_BACKSPACE: - // One-shot: delete up to 5 characters on long press - for (int i = 0; i < 5; ++i) { - if (inputText.empty()) - break; - deleteCharacter(); - } - break; - case VK_ENTER: - submitText(); - break; - case VK_SPACE: - insertCharacter(' '); - break; - case VK_ESC: - if (onTextEntered) { - onTextEntered(""); - } - break; - default: - break; - } -} - -void VirtualKeyboard::insertCharacter(char c) -{ - if (inputText.length() < 160) { // Reasonable text length limit - inputText += c; - } -} - -void VirtualKeyboard::deleteCharacter() -{ - if (!inputText.empty()) { - inputText.pop_back(); - } -} - -void VirtualKeyboard::submitText() -{ - LOG_INFO("Virtual keyboard: submitting text '%s'", inputText.c_str()); - - // Only submit if text is not empty - if (!inputText.empty() && onTextEntered) { - // Store callback and text to submit before clearing callback - std::function callback = onTextEntered; - std::string textToSubmit = inputText; - onTextEntered = nullptr; - // Don't clear inputText here - let the calling module handle cleanup - // inputText = ""; // Removed: keep text visible until module cleans up - callback(textToSubmit); - } else if (inputText.empty()) { - // For empty text, just ignore the submission - don't clear callback - // This keeps the virtual keyboard responsive for further input - LOG_INFO("Virtual keyboard: empty text submitted, ignoring - keyboard remains active"); - } else { - // No callback available - if (screen) { - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - } - } -} - -void VirtualKeyboard::setInputText(const std::string &text) -{ - inputText = text; -} - -std::string VirtualKeyboard::getInputText() const -{ - return inputText; -} - -void VirtualKeyboard::setHeader(const std::string &header) -{ - headerText = header; -} - -void VirtualKeyboard::setCallback(std::function callback) -{ - onTextEntered = callback; -} - -void VirtualKeyboard::resetTimeout() -{ - lastActivityTime = millis(); -} - -bool VirtualKeyboard::isTimedOut() const -{ - return (millis() - lastActivityTime) > TIMEOUT_MS; -} - -} // namespace graphics diff --git a/src/graphics/VirtualKeyboard.h b/src/graphics/VirtualKeyboard.h deleted file mode 100644 index 169163b57..000000000 --- a/src/graphics/VirtualKeyboard.h +++ /dev/null @@ -1,80 +0,0 @@ -#pragma once - -#include "configuration.h" -#include -#include -#include - -namespace graphics -{ - -enum VirtualKeyType { VK_CHAR, VK_BACKSPACE, VK_ENTER, VK_SHIFT, VK_ESC, VK_SPACE }; - -struct VirtualKey { - char character; - VirtualKeyType type; - uint8_t x; - uint8_t y; - uint8_t width; - uint8_t height; -}; - -class VirtualKeyboard -{ - public: - VirtualKeyboard(); - ~VirtualKeyboard(); - - void draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY); - void setInputText(const std::string &text); - std::string getInputText() const; - void setHeader(const std::string &header); - void setCallback(std::function callback); - - // Navigation methods for encoder input - void moveCursorUp(); - void moveCursorDown(); - void moveCursorLeft(); - void moveCursorRight(); - void handlePress(); - void handleLongPress(); - - // Timeout management - void resetTimeout(); - bool isTimedOut() const; - - private: - static const uint8_t KEYBOARD_ROWS = 4; - static const uint8_t KEYBOARD_COLS = 11; - static const uint8_t KEY_WIDTH = 9; - static const uint8_t KEY_HEIGHT = 9; // Compressed to fit 4 rows on 64px displays - static const uint8_t KEYBOARD_START_Y = 26; // Start just below input box bottom - - VirtualKey keyboard[KEYBOARD_ROWS][KEYBOARD_COLS]; - - std::string inputText; - std::string headerText; - std::function onTextEntered; - - uint8_t cursorRow; - uint8_t cursorCol; - - // Timeout management for auto-exit - uint32_t lastActivityTime; - static const uint32_t TIMEOUT_MS = 60000; // 1 minute timeout - - void initializeKeyboard(); - void drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t w, uint8_t h, - bool isLastCol); - void drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY); - - // Unified cursor movement helper - void moveCursorDelta(int dRow, int dCol); - - char getCharForKey(const VirtualKey &key, bool isLongPress = false); - void insertCharacter(char c); - void deleteCharacter(); - void submitText(); -}; - -} // namespace graphics diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index e02948864..512f650ec 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -10,10 +10,7 @@ #include "graphics/Screen.h" #include "graphics/SharedUIDisplay.h" #include "graphics/draw/UIRenderer.h" -#include "input/RotaryEncoderInterruptImpl1.h" -#include "input/UpDownInterruptImpl1.h" #include "main.h" -#include "mesh/MeshTypes.h" #include "modules/AdminModule.h" #include "modules/CannedMessageModule.h" #include "modules/KeyVerificationModule.h" diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index 221d95075..3d635e588 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -38,8 +38,6 @@ bool NotificationRenderer::pauseBanner = false; notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none; uint32_t NotificationRenderer::numDigits = 0; uint32_t NotificationRenderer::currentNumber = 0; -VirtualKeyboard *NotificationRenderer::virtualKeyboard = nullptr; -std::function NotificationRenderer::textInputCallback = nullptr; uint32_t pow_of_10(uint32_t n) { @@ -91,33 +89,14 @@ void NotificationRenderer::resetBanner() void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state) { - // Handle text_input notifications first - they have their own timeout/banner logic - if (current_notification_type == notificationTypeEnum::text_input) { - // Check for timeout and reset if needed for text input - if (millis() > alertBannerUntil && alertBannerUntil > 0) { - resetBanner(); - return; - } - drawTextInput(display, state); - return; - } - - if (millis() > alertBannerUntil && alertBannerUntil > 0) { + if (!isOverlayBannerShowing() && alertBannerMessage[0] != '\0') resetBanner(); - } - - // Exit if no banner is showing or banner is paused - if (!isOverlayBannerShowing() || pauseBanner) { + if (!isOverlayBannerShowing() || pauseBanner) return; - } - switch (current_notification_type) { case notificationTypeEnum::none: // Do nothing - no notification to display break; - case notificationTypeEnum::text_input: - // Already handled above with dedicated logic (early return). Keep a case here to satisfy -Wswitch. - break; case notificationTypeEnum::text_banner: case notificationTypeEnum::selection_picker: drawAlertBannerOverlay(display, state); @@ -596,90 +575,6 @@ void NotificationRenderer::drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUi "Please be patient and do not power off."); } -void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state) -{ - if (virtualKeyboard) { - // Check for timeout and auto-exit if needed - if (virtualKeyboard->isTimedOut()) { - LOG_INFO("Virtual keyboard timeout - auto-exiting"); - // Cancel virtual keyboard - call callback with empty string to indicate timeout - auto callback = textInputCallback; // Store callback before clearing - - // Clean up first to prevent re-entry - delete virtualKeyboard; - virtualKeyboard = nullptr; - textInputCallback = nullptr; - resetBanner(); - - // Call callback after cleanup - if (callback) { - callback(""); - } - - // Restore normal overlays - if (screen) { - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - } - return; - } - - // Handle input events for virtual keyboard navigation - if (inEvent.inputEvent != INPUT_BROKER_NONE) { - if (inEvent.inputEvent == INPUT_BROKER_UP) { - virtualKeyboard->moveCursorUp(); - } else if (inEvent.inputEvent == INPUT_BROKER_DOWN) { - virtualKeyboard->moveCursorDown(); - } else if (inEvent.inputEvent == INPUT_BROKER_LEFT) { - virtualKeyboard->moveCursorLeft(); - } else if (inEvent.inputEvent == INPUT_BROKER_RIGHT) { - virtualKeyboard->moveCursorRight(); - } else if (inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) { - // Long press UP = move left - virtualKeyboard->moveCursorLeft(); - } else if (inEvent.inputEvent == INPUT_BROKER_USER_PRESS) { - // Long press DOWN = move right - virtualKeyboard->moveCursorRight(); - } else if (inEvent.inputEvent == INPUT_BROKER_SELECT) { - virtualKeyboard->handlePress(); - } else if (inEvent.inputEvent == INPUT_BROKER_SELECT_LONG) { - virtualKeyboard->handleLongPress(); - } else if (inEvent.inputEvent == INPUT_BROKER_CANCEL) { - // Cancel virtual keyboard - call callback with empty string - auto callback = textInputCallback; // Store callback before clearing - - // Clean up first to prevent re-entry - delete virtualKeyboard; - virtualKeyboard = nullptr; - textInputCallback = nullptr; - resetBanner(); - - // Call callback after cleanup - if (callback) { - callback(""); - } - - // Restore normal overlays - if (screen) { - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - } - return; - } - - // Reset input event after processing - inEvent.inputEvent = INPUT_BROKER_NONE; - } - - // Clear the display and draw virtual keyboard - display->setColor(BLACK); - display->fillRect(0, 0, display->getWidth(), display->getHeight()); - display->setColor(WHITE); - virtualKeyboard->draw(display, 0, 0); - } else { - // If virtualKeyboard is null, reset the banner to avoid getting stuck - resetBanner(); - } -} - bool NotificationRenderer::isOverlayBannerShowing() { return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil); diff --git a/src/graphics/draw/NotificationRenderer.h b/src/graphics/draw/NotificationRenderer.h index edb069513..9c30b329c 100644 --- a/src/graphics/draw/NotificationRenderer.h +++ b/src/graphics/draw/NotificationRenderer.h @@ -3,9 +3,6 @@ #include "OLEDDisplay.h" #include "OLEDDisplayUi.h" #include "graphics/Screen.h" -#include "graphics/VirtualKeyboard.h" -#include -#include #define MAX_LINES 5 namespace graphics @@ -25,8 +22,6 @@ class NotificationRenderer static std::function alertBannerCallback; static uint32_t numDigits; static uint32_t currentNumber; - static VirtualKeyboard *virtualKeyboard; - static std::function textInputCallback; static bool pauseBanner; @@ -35,7 +30,6 @@ class NotificationRenderer static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state); - static void drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1], uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth = 0); diff --git a/src/input/InputBroker.h b/src/input/InputBroker.h index 012a403f5..4487fa662 100644 --- a/src/input/InputBroker.h +++ b/src/input/InputBroker.h @@ -4,7 +4,6 @@ enum input_broker_event { INPUT_BROKER_NONE = 0, INPUT_BROKER_SELECT = 10, - INPUT_BROKER_SELECT_LONG, INPUT_BROKER_UP = 17, INPUT_BROKER_DOWN = 18, INPUT_BROKER_LEFT = 19, diff --git a/src/input/TrackballInterruptBase.cpp b/src/input/TrackballInterruptBase.cpp index 4c8ce6409..d41ad2fd6 100644 --- a/src/input/TrackballInterruptBase.cpp +++ b/src/input/TrackballInterruptBase.cpp @@ -1,14 +1,12 @@ #include "TrackballInterruptBase.h" #include "configuration.h" -extern bool osk_found; TrackballInterruptBase::TrackballInterruptBase(const char *name) : concurrency::OSThread(name), _originName(name) {} void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress, input_broker_event eventDown, input_broker_event eventUp, input_broker_event eventLeft, - input_broker_event eventRight, input_broker_event eventPressed, - input_broker_event eventPressedLong, void (*onIntDown)(), void (*onIntUp)(), - void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)()) + input_broker_event eventRight, input_broker_event eventPressed, void (*onIntDown)(), + void (*onIntUp)(), void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)()) { this->_pinDown = pinDown; this->_pinUp = pinUp; @@ -20,7 +18,6 @@ void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLef this->_eventLeft = eventLeft; this->_eventRight = eventRight; this->_eventPressed = eventPressed; - this->_eventPressedLong = eventPressedLong; if (pinPress != 255) { pinMode(pinPress, INPUT_PULLUP); @@ -43,9 +40,9 @@ void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLef attachInterrupt(this->_pinRight, onIntRight, TB_DIRECTION); } - LOG_DEBUG("Trackball GPIO initialized - UP:%d DOWN:%d LEFT:%d RIGHT:%d PRESS:%d", this->_pinUp, this->_pinDown, - this->_pinLeft, this->_pinRight, pinPress); - osk_found = true; + LOG_DEBUG("Trackball GPIO initialized (%d, %d, %d, %d, %d)", this->_pinUp, this->_pinDown, this->_pinLeft, this->_pinRight, + pinPress); + this->setInterval(100); } @@ -53,47 +50,10 @@ int32_t TrackballInterruptBase::runOnce() { InputEvent e; e.inputEvent = INPUT_BROKER_NONE; - - // Handle long press detection for press button - if (pressDetected && pressStartTime > 0) { - uint32_t pressDuration = millis() - pressStartTime; - bool buttonStillPressed = false; - -#if defined(T_DECK) - buttonStillPressed = (this->action == TB_ACTION_PRESSED); -#else - buttonStillPressed = !digitalRead(_pinPress); -#endif - - if (!buttonStillPressed) { - // Button released - if (pressDuration < LONG_PRESS_DURATION) { - // Short press - e.inputEvent = this->_eventPressed; - } - // Reset state - pressDetected = false; - pressStartTime = 0; - lastLongPressEventTime = 0; - this->action = TB_ACTION_NONE; - } else if (pressDuration >= LONG_PRESS_DURATION) { - // Long press detected - uint32_t currentTime = millis(); - // Only trigger long press event if enough time has passed since the last one - if (lastLongPressEventTime == 0 || (currentTime - lastLongPressEventTime) >= LONG_PRESS_REPEAT_INTERVAL) { - e.inputEvent = this->_eventPressedLong; - lastLongPressEventTime = currentTime; - } - this->action = TB_ACTION_PRESSED_LONG; - } - } - #if defined(T_DECK) // T-deck gets a super-simple debounce on trackball - if (this->action == TB_ACTION_PRESSED && !pressDetected) { - // Start long press detection - pressDetected = true; - pressStartTime = millis(); - // Don't send event yet, wait to see if it's a long press + if (this->action == TB_ACTION_PRESSED) { + // LOG_DEBUG("Trackball event Press"); + e.inputEvent = this->_eventPressed; } else if (this->action == TB_ACTION_UP && lastEvent == TB_ACTION_UP) { // LOG_DEBUG("Trackball event UP"); e.inputEvent = this->_eventUp; @@ -108,11 +68,9 @@ int32_t TrackballInterruptBase::runOnce() e.inputEvent = this->_eventRight; } #else - if (this->action == TB_ACTION_PRESSED && !digitalRead(_pinPress) && !pressDetected) { - // Start long press detection - pressDetected = true; - pressStartTime = millis(); - // Don't send event yet, wait to see if it's a long press + if (this->action == TB_ACTION_PRESSED && !digitalRead(_pinPress)) { + // LOG_DEBUG("Trackball event Press"); + e.inputEvent = this->_eventPressed; } else if (this->action == TB_ACTION_UP && !digitalRead(_pinUp)) { // LOG_DEBUG("Trackball event UP"); e.inputEvent = this->_eventUp; @@ -133,16 +91,10 @@ int32_t TrackballInterruptBase::runOnce() e.kbchar = 0x00; this->notifyObservers(&e); } + lastEvent = action; + this->action = TB_ACTION_NONE; - // Only update lastEvent for non-press actions or completed press actions - if (this->action != TB_ACTION_PRESSED || !pressDetected) { - lastEvent = action; - if (!pressDetected) { - this->action = TB_ACTION_NONE; - } - } - - return 50; // Check more frequently for better long press detection + return 100; } void TrackballInterruptBase::intPressHandler() diff --git a/src/input/TrackballInterruptBase.h b/src/input/TrackballInterruptBase.h index 38be22f20..92db8720e 100644 --- a/src/input/TrackballInterruptBase.h +++ b/src/input/TrackballInterruptBase.h @@ -18,8 +18,8 @@ class TrackballInterruptBase : public Observable, public con explicit TrackballInterruptBase(const char *name); void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress, input_broker_event eventDown, input_broker_event eventUp, input_broker_event eventLeft, input_broker_event eventRight, - input_broker_event eventPressed, input_broker_event eventPressedLong, void (*onIntDown)(), void (*onIntUp)(), - void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)()); + input_broker_event eventPressed, void (*onIntDown)(), void (*onIntUp)(), void (*onIntLeft)(), void (*onIntRight)(), + void (*onIntPress)()); void intPressHandler(); void intDownHandler(); void intUpHandler(); @@ -33,7 +33,6 @@ class TrackballInterruptBase : public Observable, public con enum TrackballInterruptBaseActionType { TB_ACTION_NONE, TB_ACTION_PRESSED, - TB_ACTION_PRESSED_LONG, TB_ACTION_UP, TB_ACTION_DOWN, TB_ACTION_LEFT, @@ -47,20 +46,12 @@ class TrackballInterruptBase : public Observable, public con volatile TrackballInterruptBaseActionType action = TB_ACTION_NONE; - // Long press detection for press button - uint32_t pressStartTime = 0; - bool pressDetected = false; - uint32_t lastLongPressEventTime = 0; - static const uint32_t LONG_PRESS_DURATION = 500; // ms - static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 500; // ms - interval between repeated long press events - private: input_broker_event _eventDown = INPUT_BROKER_NONE; input_broker_event _eventUp = INPUT_BROKER_NONE; input_broker_event _eventLeft = INPUT_BROKER_NONE; input_broker_event _eventRight = INPUT_BROKER_NONE; input_broker_event _eventPressed = INPUT_BROKER_NONE; - input_broker_event _eventPressedLong = INPUT_BROKER_NONE; const char *_originName; TrackballInterruptBaseActionType lastEvent = TB_ACTION_NONE; }; diff --git a/src/input/TrackballInterruptImpl1.cpp b/src/input/TrackballInterruptImpl1.cpp index 594facdeb..896238f38 100644 --- a/src/input/TrackballInterruptImpl1.cpp +++ b/src/input/TrackballInterruptImpl1.cpp @@ -13,12 +13,11 @@ void TrackballInterruptImpl1::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLe input_broker_event eventLeft = INPUT_BROKER_LEFT; input_broker_event eventRight = INPUT_BROKER_RIGHT; input_broker_event eventPressed = INPUT_BROKER_SELECT; - input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG; TrackballInterruptBase::init(pinDown, pinUp, pinLeft, pinRight, pinPress, eventDown, eventUp, eventLeft, eventRight, - eventPressed, eventPressedLong, TrackballInterruptImpl1::handleIntDown, - TrackballInterruptImpl1::handleIntUp, TrackballInterruptImpl1::handleIntLeft, - TrackballInterruptImpl1::handleIntRight, TrackballInterruptImpl1::handleIntPressed); + eventPressed, TrackballInterruptImpl1::handleIntDown, TrackballInterruptImpl1::handleIntUp, + TrackballInterruptImpl1::handleIntLeft, TrackballInterruptImpl1::handleIntRight, + TrackballInterruptImpl1::handleIntPressed); inputBroker->registerSource(this); } diff --git a/src/main.cpp b/src/main.cpp index 23ffa6b6d..ef5f5a721 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -191,8 +191,6 @@ ScanI2C::DeviceAddress cardkb_found = ScanI2C::ADDRESS_NONE; uint8_t kb_model; // global bool to record that a kb is present bool kb_found = false; -// global bool to record that on-screen keyboard (OSK) is present -bool osk_found = false; // The I2C address of the RTC Module (if found) ScanI2C::DeviceAddress rtc_found = ScanI2C::ADDRESS_NONE; @@ -1414,10 +1412,6 @@ void setup() #endif #endif -#if defined(HAS_TRACKBALL) || (defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2) - osk_found = true; -#endif - #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WEBSERVER // Start web server thread. webServerThread = new WebServerThread(); diff --git a/src/main.h b/src/main.h index 2ddd4862f..3568daad2 100644 --- a/src/main.h +++ b/src/main.h @@ -32,7 +32,6 @@ extern ScanI2C::DeviceAddress screen_found; extern ScanI2C::DeviceAddress cardkb_found; extern uint8_t kb_model; extern bool kb_found; -extern bool osk_found; extern ScanI2C::DeviceAddress rtc_found; extern ScanI2C::DeviceAddress accelerometer_found; extern ScanI2C::FoundDevice rgb_found; diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 76b950322..d40dcd24f 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -13,16 +13,12 @@ #include "detect/ScanI2C.h" #include "graphics/Screen.h" #include "graphics/SharedUIDisplay.h" -#include "graphics/draw/NotificationRenderer.h" #include "graphics/emotes.h" #include "graphics/images.h" #include "main.h" // for cardkb_found #include "mesh/generated/meshtastic/cannedmessages.pb.h" #include "modules/AdminModule.h" #include "modules/ExternalNotificationModule.h" // for buzzer control -#if HAS_TRACKBALL -#include "input/TrackballInterruptImpl1.h" -#endif #if !MESHTASTIC_EXCLUDE_GPS #include "GPS.h" #endif @@ -42,7 +38,6 @@ extern ScanI2C::DeviceAddress cardkb_found; extern bool graphics::isMuted; -extern bool osk_found; static const char *cannedMessagesConfigFile = "/prefs/cannedConf.proto"; static NodeNum lastDest = NODENUM_BROADCAST; @@ -156,13 +151,10 @@ int CannedMessageModule::splitConfiguredMessages() int tempCount = 0; // Insert at position 0 (top) tempMessages[tempCount++] = "[Select Destination]"; + #if defined(USE_VIRTUAL_KEYBOARD) - // Add a "Free Text" entry at the top if using a touch screen virtual keyboard + // Add a "Free Text" entry at the top if using a keyboard tempMessages[tempCount++] = "[-- Free Text --]"; -#else - if (osk_found && screen) { - tempMessages[tempCount++] = "[-- Free Text --]"; - } #endif // First message always starts at buffer start @@ -349,8 +341,6 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) case CANNED_MESSAGE_RUN_STATE_FREETEXT: return handleFreeTextInput(event); // All allowed input for this state - // Virtual keyboard mode: Show virtual keyboard and handle input - // If sending, block all input except global/system (handled above) case CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE: return 1; @@ -637,56 +627,6 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo notifyObservers(&e); return true; } -#else - if (strcmp(current, "[-- Free Text --]") == 0) { - if (osk_found && screen) { - char headerBuffer[64]; - if (this->dest == NODENUM_BROADCAST) { - snprintf(headerBuffer, sizeof(headerBuffer), "To: Broadcast@%s", channels.getName(this->channel)); - } else { - snprintf(headerBuffer, sizeof(headerBuffer), "To: %s", getNodeName(this->dest)); - } - screen->showTextInput(headerBuffer, "", 300000, [this](const std::string &text) { - if (!text.empty()) { - this->freetext = text.c_str(); - this->payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; - runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; - currentMessageIndex = -1; - - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - this->notifyObservers(&e); - screen->forceDisplay(); - - setIntervalFromNow(500); - return; - } else { - // Don't delete virtual keyboard immediately - it might still be executing - // Instead, just clear the callback and reset banner to stop input processing - graphics::NotificationRenderer::textInputCallback = nullptr; - graphics::NotificationRenderer::resetBanner(); - - // Return to inactive state - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - this->currentMessageIndex = -1; - this->freetext = ""; - this->cursor = 0; - - // Force display update to show normal screen - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - this->notifyObservers(&e); - screen->forceDisplay(); - - // Schedule cleanup for next loop iteration to ensure safe deletion - setIntervalFromNow(50); - return; - } - }); - - return true; - } - } #endif // Normal canned message selection @@ -1003,54 +943,12 @@ int32_t CannedMessageModule::runOnce() // Normal module disable/idle handling if ((this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) || (this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE)) { - // Clean up virtual keyboard if needed when going inactive - if (graphics::NotificationRenderer::virtualKeyboard && graphics::NotificationRenderer::textInputCallback == nullptr) { - LOG_INFO("Performing delayed virtual keyboard cleanup"); - delete graphics::NotificationRenderer::virtualKeyboard; - graphics::NotificationRenderer::virtualKeyboard = nullptr; - } - temporaryMessage = ""; return INT32_MAX; } - // Handle delayed virtual keyboard message sending - if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { - // Virtual keyboard message sending case - text was not empty - if (this->freetext.length() > 0) { - LOG_INFO("Processing delayed virtual keyboard send: '%s'", this->freetext.c_str()); - sendText(this->dest, this->channel, this->freetext.c_str(), true); - - // Clean up virtual keyboard after sending - if (graphics::NotificationRenderer::virtualKeyboard) { - LOG_INFO("Cleaning up virtual keyboard after message send"); - delete graphics::NotificationRenderer::virtualKeyboard; - graphics::NotificationRenderer::virtualKeyboard = nullptr; - graphics::NotificationRenderer::textInputCallback = nullptr; - graphics::NotificationRenderer::resetBanner(); - } - - // Clear payload to indicate virtual keyboard processing is complete - // But keep SENDING_ACTIVE state to show "Sending..." screen for 2 seconds - this->payload = 0; - } else { - // Empty message, just go inactive - LOG_INFO("Empty freetext detected in delayed processing, returning to inactive state"); - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - } - - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - this->currentMessageIndex = -1; - this->freetext = ""; - this->cursor = 0; - this->notifyObservers(&e); - return 2000; - } - UIFrameEvent e; - if ((this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload != 0 && - this->payload != CANNED_MESSAGE_RUN_STATE_FREETEXT) || + if ((this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) || (this->runState == CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION)) { this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; @@ -1060,18 +958,6 @@ int32_t CannedMessageModule::runOnce() this->freetext = ""; this->cursor = 0; this->notifyObservers(&e); - } - // Handle SENDING_ACTIVE state transition after virtual keyboard message - else if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload == 0) { - // This happens after virtual keyboard message sending is complete - LOG_INFO("Virtual keyboard message sending completed, returning to inactive state"); - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - temporaryMessage = ""; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - this->currentMessageIndex = -1; - this->freetext = ""; - this->cursor = 0; - this->notifyObservers(&e); } else if (((this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT)) && !Throttle::isWithinTimespanMs(this->lastTouchMillis, INACTIVATE_AFTER_MS)) { // Reset module on inactivity @@ -1080,23 +966,9 @@ int32_t CannedMessageModule::runOnce() this->freetext = ""; this->cursor = 0; this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - - // Clean up virtual keyboard if it exists during timeout - if (graphics::NotificationRenderer::virtualKeyboard) { - LOG_INFO("Cleaning up virtual keyboard due to module timeout"); - delete graphics::NotificationRenderer::virtualKeyboard; - graphics::NotificationRenderer::virtualKeyboard = nullptr; - graphics::NotificationRenderer::textInputCallback = nullptr; - graphics::NotificationRenderer::resetBanner(); - } - this->notifyObservers(&e); } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) { - if (this->payload == 0) { - // [Exit] button pressed - return to inactive state - LOG_INFO("Processing [Exit] action - returning to inactive state"); - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - } else if (this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { + if (this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { if (this->freetext.length() > 0) { sendText(this->dest, this->channel, this->freetext.c_str(), true); this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; From 093a37a2b0888934a1941bc3f9c525e80255b5da Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 21 Aug 2025 06:31:27 -0500 Subject: [PATCH 02/54] On screen keyboard (#7705) * Add on-screen keyboard implementation on Trackball device. * Update On-Screen Keyboard to new layout. * The on-screen keyboard dynamically adjusts the key size based on the screen. * Improve input box display on small screens. * Optimize the virtual keyboard layout and cursor movement logic, and adjust the keyboard starting position for small and wide screens. * Optimize the text alignment of numeric keys on ssd1306. --------- Co-authored-by: whywilson --- src/buzz/BuzzerFeedbackThread.cpp | 1 + src/graphics/Screen.cpp | 75 ++- src/graphics/Screen.h | 4 +- src/graphics/VirtualKeyboard.cpp | 738 +++++++++++++++++++++ src/graphics/VirtualKeyboard.h | 80 +++ src/graphics/draw/MenuHandler.cpp | 3 + src/graphics/draw/NotificationRenderer.cpp | 111 +++- src/graphics/draw/NotificationRenderer.h | 6 + src/input/InputBroker.h | 1 + src/input/TrackballInterruptBase.cpp | 76 ++- src/input/TrackballInterruptBase.h | 13 +- src/input/TrackballInterruptImpl1.cpp | 7 +- src/main.cpp | 6 + src/main.h | 1 + src/modules/CannedMessageModule.cpp | 136 +++- 15 files changed, 1227 insertions(+), 31 deletions(-) create mode 100644 src/graphics/VirtualKeyboard.cpp create mode 100644 src/graphics/VirtualKeyboard.h diff --git a/src/buzz/BuzzerFeedbackThread.cpp b/src/buzz/BuzzerFeedbackThread.cpp index ce762c764..838224c69 100644 --- a/src/buzz/BuzzerFeedbackThread.cpp +++ b/src/buzz/BuzzerFeedbackThread.cpp @@ -28,6 +28,7 @@ int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event) case INPUT_BROKER_USER_PRESS: case INPUT_BROKER_ALT_PRESS: case INPUT_BROKER_SELECT: + case INPUT_BROKER_SELECT_LONG: playBeep(); // Confirmation feedback break; diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index fa71e17d8..5e29814cb 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -216,6 +216,44 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t ui->update(); } +void Screen::showTextInput(const char *header, const char *initialText, uint32_t durationMs, + std::function textCallback) +{ + LOG_INFO("showTextInput called with header='%s', durationMs=%d", header ? header : "NULL", durationMs); + + if (NotificationRenderer::virtualKeyboard) { + delete NotificationRenderer::virtualKeyboard; + NotificationRenderer::virtualKeyboard = nullptr; + } + + NotificationRenderer::textInputCallback = nullptr; + + NotificationRenderer::virtualKeyboard = new VirtualKeyboard(); + if (header) { + NotificationRenderer::virtualKeyboard->setHeader(header); + } + if (initialText) { + NotificationRenderer::virtualKeyboard->setInputText(initialText); + } + + // Set up callback with safer cleanup mechanism + NotificationRenderer::textInputCallback = textCallback; + NotificationRenderer::virtualKeyboard->setCallback([textCallback](const std::string &text) { textCallback(text); }); + + // Store the message and set the expiration timestamp (use same pattern as other notifications) + strncpy(NotificationRenderer::alertBannerMessage, header ? header : "Text Input", 255); + NotificationRenderer::alertBannerMessage[255] = '\0'; + NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs; + NotificationRenderer::pauseBanner = false; + NotificationRenderer::current_notification_type = notificationTypeEnum::text_input; + + // Set the overlay using the same pattern as other notification types + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); + ui->setTargetFPS(60); + ui->update(); +} + static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { uint8_t module_frame; @@ -713,13 +751,19 @@ int32_t Screen::runOnce() handleSetOn(false); break; case Cmd::ON_PRESS: - handleOnPress(); + if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { + handleOnPress(); + } break; case Cmd::SHOW_PREV_FRAME: - handleShowPrevFrame(); + if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { + handleShowPrevFrame(); + } break; case Cmd::SHOW_NEXT_FRAME: - handleShowNextFrame(); + if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { + handleShowNextFrame(); + } break; case Cmd::START_ALERT_FRAME: { showingBootScreen = false; // this should avoid the edge case where an alert triggers before the boot screen goes away @@ -741,7 +785,9 @@ int32_t Screen::runOnce() NotificationRenderer::pauseBanner = false; case Cmd::STOP_BOOT_SCREEN: EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // E-Ink: Explicitly use full-refresh for next frame - setFrames(); + if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { + setFrames(); + } break; case Cmd::NOOP: break; @@ -777,6 +823,7 @@ int32_t Screen::runOnce() if (showingNormalScreen) { // standard screen loop handling here if (config.display.auto_screen_carousel_secs > 0 && + NotificationRenderer::current_notification_type != notificationTypeEnum::text_input && !Throttle::isWithinTimespanMs(lastScreenTransition, config.display.auto_screen_carousel_secs * 1000)) { // If an E-Ink display struggles with fast refresh, force carousel to use full refresh instead @@ -867,6 +914,11 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver) // Called when a frame should be added / removed, or custom frames should be cleared void Screen::setFrames(FrameFocus focus) { + // Block setFrames calls when virtual keyboard is active to prevent overlay interference + if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { + return; + } + uint8_t originalPosition = ui->getUiState()->currentFrame; uint8_t previousFrameCount = framesetInfo.frameCount; FramesetInfo fsi; // Location of specific frames, for applying focus parameter @@ -1313,6 +1365,11 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) // Triggered by MeshModules int Screen::handleUIFrameEvent(const UIFrameEvent *event) { + // Block UI frame events when virtual keyboard is active + if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { + return 0; + } + if (showingNormalScreen) { // Regenerate the frameset, potentially honoring a module's internal requestFocus() call if (event->action == UIFrameEvent::Action::REGENERATE_FRAMESET) @@ -1335,6 +1392,16 @@ int Screen::handleInputEvent(const InputEvent *event) if (!screenOn) return 0; + // Handle text input notifications specially - pass input to virtual keyboard + if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) { + NotificationRenderer::inEvent = *event; + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback}; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); + setFastFramerate(); // Draw ASAP + ui->update(); + return 0; + } + #ifdef USE_EINK // the screen is the last input handler, so if an event makes it here, we can assume it will prompt a screen draw. EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 265900131..0f100d455 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -12,7 +12,7 @@ #define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2) namespace graphics { -enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker }; +enum notificationTypeEnum { none, text_banner, selection_picker, node_picker, number_picker, text_input }; struct BannerOverlayOptions { const char *message; @@ -313,6 +313,8 @@ class Screen : public concurrency::OSThread void showNodePicker(const char *message, uint32_t durationMs, std::function bannerCallback); void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function bannerCallback); + void showTextInput(const char *header, const char *initialText, uint32_t durationMs, + std::function textCallback); void requestMenu(graphics::menuHandler::screenMenus menuToShow) { diff --git a/src/graphics/VirtualKeyboard.cpp b/src/graphics/VirtualKeyboard.cpp new file mode 100644 index 000000000..84d5551cb --- /dev/null +++ b/src/graphics/VirtualKeyboard.cpp @@ -0,0 +1,738 @@ +#include "VirtualKeyboard.h" +#include "configuration.h" +#include "graphics/Screen.h" +#include "graphics/ScreenFonts.h" +#include "graphics/SharedUIDisplay.h" +#include "main.h" +#include +#include + +namespace graphics +{ + +VirtualKeyboard::VirtualKeyboard() : cursorRow(0), cursorCol(0), lastActivityTime(millis()) +{ + initializeKeyboard(); + // Set cursor to H(2, 5) + cursorRow = 2; + cursorCol = 5; +} + +VirtualKeyboard::~VirtualKeyboard() {} + +void VirtualKeyboard::initializeKeyboard() +{ + // New 4 row, 11 column keyboard layout: + static const char LAYOUT[KEYBOARD_ROWS][KEYBOARD_COLS] = {{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '\b'}, + {'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '\n'}, + {'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', ' '}, + {'z', 'x', 'c', 'v', 'b', 'n', 'm', '.', ',', '?', '\x1b'}}; + + // Derive layout dimensions and assert they match the configured keyboard grid + constexpr int LAYOUT_ROWS = (int)(sizeof(LAYOUT) / sizeof(LAYOUT[0])); + constexpr int LAYOUT_COLS = (int)(sizeof(LAYOUT[0]) / sizeof(LAYOUT[0][0])); + static_assert(LAYOUT_ROWS == KEYBOARD_ROWS, "LAYOUT rows must equal KEYBOARD_ROWS"); + static_assert(LAYOUT_COLS == KEYBOARD_COLS, "LAYOUT cols must equal KEYBOARD_COLS"); + + // Initialize all keys to empty first + for (int row = 0; row < LAYOUT_ROWS; row++) { + for (int col = 0; col < LAYOUT_COLS; col++) { + keyboard[row][col] = {0, VK_CHAR, 0, 0, 0, 0}; + } + } + + // Fill keyboard from the 2D layout + for (int row = 0; row < LAYOUT_ROWS; row++) { + for (int col = 0; col < LAYOUT_COLS; col++) { + char ch = LAYOUT[row][col]; + // No empty slots in the simplified layout + + VirtualKeyType type = VK_CHAR; + if (ch == '\b') { + type = VK_BACKSPACE; + } else if (ch == '\n') { + type = VK_ENTER; + } else if (ch == '\x1b') { // ESC + type = VK_ESC; + } else if (ch == ' ') { + type = VK_SPACE; + } + + // Make action keys wider to fit text while keeping the last column aligned + uint8_t width = (type == VK_BACKSPACE || type == VK_ENTER || type == VK_SPACE) ? (KEY_WIDTH * 3) : KEY_WIDTH; + keyboard[row][col] = {ch, type, (uint8_t)(col * KEY_WIDTH), (uint8_t)(row * KEY_HEIGHT), width, KEY_HEIGHT}; + } + } +} + +void VirtualKeyboard::draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY) +{ + // Repeat ticking is driven by NotificationRenderer once per frame + // Base styles + display->setColor(WHITE); + display->setFont(FONT_SMALL); + + // Screen geometry + const int screenW = display->getWidth(); + const int screenH = display->getHeight(); + + // Decide wide-screen mode: if there is comfortable width, allow taller keys and reserve fixed width for last column labels + // Heuristic: if screen width >= 200px (e.g., 240x135), treat as wide + const bool isWide = screenW >= 200; + + // Determine last-column label max width + display->setFont(FONT_SMALL); + const int wENTER = display->getStringWidth("ENTER"); + int lastColLabelW = wENTER; // ENTER is usually the widest + // Smaller padding on very small screens to avoid excessive whitespace + const int lastColPad = (screenW <= 128 ? 2 : 6); + const int reservedLastColW = lastColLabelW + lastColPad; // reserved width for last column keys + + // Always reserve width for the rightmost text column to avoid overlap on small screens + int cellW = 0; + int leftoverW = 0; + { + const int leftCols = KEYBOARD_COLS - 1; // 10 input characters + int usableW = screenW - reservedLastColW; + if (usableW < leftCols) { + // Guard: ensure at least 1px per left cell if labels are extremely wide (unlikely) + usableW = leftCols; + } + cellW = usableW / leftCols; + leftoverW = usableW - cellW * leftCols; // distribute extra pixels over left columns (left to right) + } + + // Dynamic key geometry + int cellH = KEY_HEIGHT; + int keyboardStartY = 0; + if (screenH <= 64) { + const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL - 2); + const int gapBelowHeader = 0; + const int singleLineBoxHeight = FONT_HEIGHT_SMALL; + const int gapAboveKeyboard = 0; + keyboardStartY = offsetY + headerHeight + gapBelowHeader + singleLineBoxHeight + gapAboveKeyboard; + if (keyboardStartY < 0) + keyboardStartY = 0; + if (keyboardStartY > screenH) + keyboardStartY = screenH; + int keyboardHeight = screenH - keyboardStartY; + cellH = std::max(1, keyboardHeight / KEYBOARD_ROWS); + } else if (isWide) { + // For wide screens (e.g., T114 240x135), prefer square keys: height equals left-column key width. + cellH = std::max((int)KEY_HEIGHT, cellW); + + // Guarantee at least 2 lines of input are visible by reducing cell height minimally if needed. + // Replicate the spacing used in drawInputArea(): headerGap=1, box-to-header gap=1, gap above keyboard=1 + display->setFont(FONT_SMALL); + const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL + 1); + const int headerToBoxGap = 1; + const int gapAboveKb = 1; + const int minBoxHeightForTwoLines = 2 * FONT_HEIGHT_SMALL + 2; // inner 1px top/bottom + int maxKeyboardHeight = screenH - (offsetY + headerHeight + headerToBoxGap + minBoxHeightForTwoLines + gapAboveKb); + int maxCellHAllowed = maxKeyboardHeight / KEYBOARD_ROWS; + if (maxCellHAllowed < (int)KEY_HEIGHT) + maxCellHAllowed = KEY_HEIGHT; + if (maxCellHAllowed > 0 && cellH > maxCellHAllowed) { + cellH = maxCellHAllowed; + } + // Keyboard placement from bottom for wide screens + int keyboardHeight = KEYBOARD_ROWS * cellH; + keyboardStartY = screenH - keyboardHeight; + if (keyboardStartY < 0) + keyboardStartY = 0; + } else { + // Default (non-wide, non-64px) behavior: use key height heuristic and place at bottom + cellH = KEY_HEIGHT; + int keyboardHeight = KEYBOARD_ROWS * cellH; + keyboardStartY = screenH - keyboardHeight; + if (keyboardStartY < 0) + keyboardStartY = 0; + } + + // Draw input area above keyboard + drawInputArea(display, offsetX, offsetY, keyboardStartY); + + // Precompute per-column x and width with leftover distributed over left columns for even spacing + int colX[KEYBOARD_COLS]; + int colW[KEYBOARD_COLS]; + int runningX = offsetX; + for (int col = 0; col < KEYBOARD_COLS - 1; ++col) { + int wcol = cellW + (col < leftoverW ? 1 : 0); + colX[col] = runningX; + colW[col] = wcol; + runningX += wcol; + } + // Last column + colX[KEYBOARD_COLS - 1] = runningX; + colW[KEYBOARD_COLS - 1] = reservedLastColW; + + // Draw keyboard grid + for (int row = 0; row < KEYBOARD_ROWS; row++) { + for (int col = 0; col < KEYBOARD_COLS; col++) { + const VirtualKey &k = keyboard[row][col]; + if (k.character != 0 || k.type != VK_CHAR) { + const bool isLastCol = (col == KEYBOARD_COLS - 1); + int x = colX[col]; + int w = colW[col]; + int y = offsetY + keyboardStartY + row * cellH; + int h = cellH; + bool selected = (row == cursorRow && col == cursorCol); + drawKey(display, k, selected, x, y, (uint8_t)w, (uint8_t)h, isLastCol); + } + } + } +} + +void VirtualKeyboard::drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY) +{ + display->setColor(WHITE); + + const int screenWidth = display->getWidth(); + const int screenHeight = display->getHeight(); + // Use the standard small font metrics for input box sizing (restore original size) + const int inputLineH = FONT_HEIGHT_SMALL; + + // Header uses the standard small (which may be larger on big screens) + display->setFont(FONT_SMALL); + int headerHeight = 0; + if (!headerText.empty()) { + // Draw header and reserve exact font height (plus a tighter gap) to maximize input area + display->drawString(offsetX + 2, offsetY, headerText.c_str()); + if (screenHeight <= 64) { + headerHeight = FONT_HEIGHT_SMALL - 2; // 11px + } else { + headerHeight = FONT_HEIGHT_SMALL; // no extra padding baked in + } + } + + const int boxX = offsetX; + const int boxWidth = screenWidth; + int boxY; + int boxHeight; + if (screenHeight <= 64) { + const int gapBelowHeader = 0; + const int fixedBoxHeight = inputLineH; + const int gapAboveKeyboard = 0; + boxY = offsetY + headerHeight + gapBelowHeader; + boxHeight = fixedBoxHeight; + if (boxY + boxHeight + gapAboveKeyboard > keyboardStartY) { + int over = boxY + boxHeight + gapAboveKeyboard - keyboardStartY; + boxHeight = std::max(1, fixedBoxHeight - over); + } + } else { + const int gapBelowHeader = 1; + int gapAboveKeyboard = 1; + int tmpBoxY = offsetY + headerHeight + gapBelowHeader; + const int minBoxHeight = inputLineH + 2; + int availableH = keyboardStartY - tmpBoxY - gapAboveKeyboard; + if (availableH < minBoxHeight) + availableH = minBoxHeight; + boxY = tmpBoxY; + boxHeight = availableH; + } + + // Draw box border + display->drawRect(boxX, boxY, boxWidth, boxHeight); + + display->setFont(FONT_SMALL); + + // Text rendering: multi-line if space allows (>= 2 lines), else single-line with leading ellipsis + const int textX = boxX + 2; + const int maxTextWidth = boxWidth - 4; + const int maxLines = (boxHeight - 2) / inputLineH; + if (maxLines >= 2) { + // Inner bounds for caret clamping + const int innerLeft = boxX + 1; + const int innerRight = boxX + boxWidth - 2; + const int innerTop = boxY + 1; + const int innerBottom = boxY + boxHeight - 2; + + // Wrap text greedily into lines that fit maxTextWidth + std::vector lines; + { + std::string remaining = inputText; + while (!remaining.empty()) { + int bestLen = 0; + for (int len = 1; len <= (int)remaining.size(); ++len) { + int w = display->getStringWidth(remaining.substr(0, len).c_str()); + if (w <= maxTextWidth) + bestLen = len; + else + break; + } + if (bestLen == 0) { + // At least show one character to make progress + bestLen = 1; + } + lines.emplace_back(remaining.substr(0, bestLen)); + remaining.erase(0, bestLen); + } + } + + const bool scrolledUp = ((int)lines.size() > maxLines); + int caretX = textX; + int caretY = innerTop; + + // Leave a small top gap to render '...' without replacing the first line + const int topInset = 2; + const int lineStep = std::max(1, inputLineH - 1); // slightly tighter than font height + int lineY = innerTop + topInset; + + if (scrolledUp) { + // Draw three small dots centered horizontally, vertically at the midpoint of the gap + // between the inner top and the first line's top baseline. This avoids using a tall glyph. + const int firstLineTop = lineY; // baseline top for the first visible line + const int gapMidY = innerTop + (firstLineTop - innerTop) / 2 + 1; // shift down 1px as requested + const int centerX = boxX + boxWidth / 2; + const int dotSpacing = 3; // px between dots + const int dotSize = 1; // small square dot + display->fillRect(centerX - dotSpacing, gapMidY, dotSize, dotSize); + display->fillRect(centerX, gapMidY, dotSize, dotSize); + display->fillRect(centerX + dotSpacing, gapMidY, dotSize, dotSize); + } + + // How many lines fit with our top inset and tighter step + const int linesCapacity = std::max(1, (innerBottom - lineY + 1) / lineStep); + const int linesToShow = std::min((int)lines.size(), linesCapacity); + const int startIndex = scrolledUp ? ((int)lines.size() - linesToShow) : 0; + + for (int i = 0; i < linesToShow; ++i) { + const std::string &chunk = lines[startIndex + i]; + display->drawString(textX, lineY, chunk.c_str()); + caretX = textX + display->getStringWidth(chunk.c_str()); + caretY = lineY; + lineY += lineStep; + } + + // Draw caret at end of the last visible line + int caretPadY = 2; + if (boxHeight >= inputLineH + 4) + caretPadY = 3; + int cursorTop = caretY + caretPadY; + // Use lineStep so caret height matches the row spacing + int cursorH = lineStep - caretPadY * 2; + if (cursorH < 1) + cursorH = 1; + // Clamp vertical bounds to stay inside the inner rect + if (cursorTop < innerTop) + cursorTop = innerTop; + if (cursorTop + cursorH - 1 > innerBottom) + cursorH = innerBottom - cursorTop + 1; + if (cursorH < 1) + cursorH = 1; + // Only draw if cursor is inside inner bounds + if (caretX >= innerLeft && caretX <= innerRight) { + display->drawVerticalLine(caretX, cursorTop, cursorH); + } + } else { + std::string displayText = inputText; + int textW = display->getStringWidth(displayText.c_str()); + std::string scrolled = displayText; + if (textW > maxTextWidth) { + // Trim from the left until it fits + while (textW > maxTextWidth && !scrolled.empty()) { + scrolled.erase(0, 1); + textW = display->getStringWidth(scrolled.c_str()); + } + // Add leading ellipsis and ensure it still fits + if (scrolled != displayText) { + scrolled = "..." + scrolled; + textW = display->getStringWidth(scrolled.c_str()); + // If adding ellipsis causes overflow, trim more after the ellipsis + while (textW > maxTextWidth && scrolled.size() > 3) { + scrolled.erase(3, 1); // remove chars after the ellipsis + textW = display->getStringWidth(scrolled.c_str()); + } + } + } else { + // Keep textW in sync with what we draw + textW = display->getStringWidth(scrolled.c_str()); + } + + int textY; + if (screenHeight <= 64) { + textY = boxY + (boxHeight - inputLineH) / 2; + } else { + const int innerLeft = boxX + 1; + const int innerRight = boxX + boxWidth - 2; + const int innerTop = boxY + 1; + const int innerBottom = boxY + boxHeight - 2; + + // Center text vertically within inner box for single-line, then clamp so it never overlaps borders + int innerH = innerBottom - innerTop + 1; + textY = innerTop + std::max(0, (innerH - inputLineH) / 2); + // Clamp fully inside the inner rect + if (textY < innerTop) + textY = innerTop; + int maxTop = innerBottom - inputLineH + 1; + if (textY > maxTop) + textY = maxTop; + } + + if (!scrolled.empty()) { + display->drawString(textX, textY, scrolled.c_str()); + } + + int cursorX = textX + textW; + if (screenHeight > 64) { + const int innerRight = boxX + boxWidth - 2; + if (cursorX > innerRight) + cursorX = innerRight; + } + + int cursorTop, cursorH; + if (screenHeight <= 64) { + cursorH = 10; + cursorTop = boxY + (boxHeight - cursorH) / 2; + } else { + const int innerLeft = boxX + 1; + const int innerRight = boxX + boxWidth - 2; + const int innerTop = boxY + 1; + const int innerBottom = boxY + boxHeight - 2; + + cursorTop = boxY + 2; + cursorH = boxHeight - 4; + if (cursorH < 1) + cursorH = 1; + if (cursorTop < innerTop) + cursorTop = innerTop; + if (cursorTop + cursorH - 1 > innerBottom) + cursorH = innerBottom - cursorTop + 1; + if (cursorH < 1) + cursorH = 1; + + if (cursorX < innerLeft || cursorX > innerRight) + return; + } + + display->drawVerticalLine(cursorX, cursorTop, cursorH); + } +} + +void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t width, + uint8_t height, bool isLastCol) +{ + // Draw key content + display->setFont(FONT_SMALL); + const int fontH = FONT_HEIGHT_SMALL; + // Build label and metrics first + std::string keyText; + if (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC) { + // Keep literal text labels for the action keys on the rightmost column + keyText = (key.type == VK_BACKSPACE) ? "BACK" + : (key.type == VK_ENTER) ? "ENTER" + : (key.type == VK_SPACE) ? "SPACE" + : (key.type == VK_ESC) ? "ESC" + : ""; + } else { + char c = getCharForKey(key, false); + if (c >= 'a' && c <= 'z') { + c = c - 'a' + 'A'; + } + keyText = (key.character == ' ' || key.character == '_') ? "_" : std::string(1, c); + } + + int textWidth = display->getStringWidth(keyText.c_str()); + // Label alignment + // - Rightmost action column: right-align text with a small right padding (~2px) so it hugs screen edge neatly. + // - Other keys: center horizontally; use ceil-style rounding to avoid appearing left-biased on odd widths. + int textX; + if (isLastCol) { + const int rightPad = 1; + textX = x + width - textWidth - rightPad; + if (textX < x) + textX = x; // guard + } else { + if (display->getHeight() <= 64 && (key.character >= '0' && key.character <= '9')) { + textX = x + (width - textWidth + 1) / 2; + } else { + textX = x + (width - textWidth) / 2; + } + } + int contentTop = y; + int contentH = height; + if (selected) { + display->setColor(WHITE); + bool isAction = (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC); + + if (display->getHeight() <= 64 && !isAction) { + display->fillRect(x, y, width, height); + } else if (isAction) { + const int padX = 1; + const int padY = 2; + int hlW = textWidth + padX * 2; + int hlX = textX - padX; + + if (hlX < x) { + hlW -= (x - hlX); + hlX = x; + } + int maxW = (x + width) - hlX; + if (hlW > maxW) + hlW = maxW; + if (hlW < 1) + hlW = 1; + + int hlH = std::min(fontH + padY * 2, (int)height); + int hlY = y + (height - hlH) / 2; + display->fillRect(hlX, hlY, hlW, hlH); + contentTop = hlY; + contentH = hlH; + } else { + display->fillRect(x, y, width, height); + } + display->setColor(BLACK); + } else { + display->setColor(WHITE); + } + + int centeredTextY; + if (display->getHeight() <= 64) { + centeredTextY = y + (height - fontH) / 2; + } else { + centeredTextY = contentTop + (contentH - fontH) / 2; + } + if (display->getHeight() > 64) { + if (centeredTextY < contentTop) + centeredTextY = contentTop; + if (centeredTextY + fontH > contentTop + contentH) + centeredTextY = std::max(contentTop, contentTop + contentH - fontH); + } + + if (display->getHeight() <= 64 && keyText.size() == 1) { + char ch = keyText[0]; + if (ch == '.' || ch == ',' || ch == ';') { + centeredTextY -= 1; + } + } + display->drawString(textX, centeredTextY, keyText.c_str()); +} + +char VirtualKeyboard::getCharForKey(const VirtualKey &key, bool isLongPress) +{ + if (key.type != VK_CHAR) { + return key.character; + } + + char c = key.character; + + // Long-press: only keep letter lowercase->uppercase conversion; remove other symbol mappings + if (isLongPress && c >= 'a' && c <= 'z') { + c = (char)(c - 'a' + 'A'); + } + + return c; +} + +void VirtualKeyboard::moveCursorDelta(int dRow, int dCol) +{ + resetTimeout(); + // wrap around rows and cols in the 4x11 grid + int r = (int)cursorRow + dRow; + int c = (int)cursorCol + dCol; + if (r < 0) + r = KEYBOARD_ROWS - 1; + else if (r >= KEYBOARD_ROWS) + r = 0; + if (c < 0) + c = KEYBOARD_COLS - 1; + else if (c >= KEYBOARD_COLS) + c = 0; + cursorRow = (uint8_t)r; + cursorCol = (uint8_t)c; +} + +void VirtualKeyboard::moveCursorUp() +{ + moveCursorDelta(-1, 0); +} +void VirtualKeyboard::moveCursorDown() +{ + moveCursorDelta(1, 0); +} +void VirtualKeyboard::moveCursorLeft() +{ + resetTimeout(); + + if (cursorCol > 0) { + cursorCol--; + } else { + if (cursorRow > 0) { + cursorRow--; + cursorCol = KEYBOARD_COLS - 1; + } else { + cursorRow = KEYBOARD_ROWS - 1; + cursorCol = KEYBOARD_COLS - 1; + } + } +} +void VirtualKeyboard::moveCursorRight() +{ + resetTimeout(); + + if (cursorCol < KEYBOARD_COLS - 1) { + cursorCol++; + } else { + if (cursorRow < KEYBOARD_ROWS - 1) { + cursorRow++; + cursorCol = 0; + } else { + cursorRow = 0; + cursorCol = 0; + } + } +} + +void VirtualKeyboard::handlePress() +{ + resetTimeout(); // Reset timeout on any input activity + + const VirtualKey &key = keyboard[cursorRow][cursorCol]; + + // Don't handle press if the key is empty (but allow special keys) + if (key.character == 0 && key.type == VK_CHAR) { + return; + } + + // For character keys, insert lowercase character + if (key.type == VK_CHAR) { + insertCharacter(getCharForKey(key, false)); // false = lowercase/normal char + return; + } + + // Handle non-character keys immediately + switch (key.type) { + case VK_BACKSPACE: + deleteCharacter(); + break; + case VK_ENTER: + submitText(); + break; + case VK_SPACE: + insertCharacter(' '); + break; + case VK_ESC: + if (onTextEntered) { + std::function callback = onTextEntered; + onTextEntered = nullptr; + inputText = ""; + callback(""); + } + return; + default: + break; + } +} + +void VirtualKeyboard::handleLongPress() +{ + resetTimeout(); // Reset timeout on any input activity + + const VirtualKey &key = keyboard[cursorRow][cursorCol]; + + // Don't handle press if the key is empty (but allow special keys) + if (key.character == 0 && key.type == VK_CHAR) { + return; + } + + // For character keys, insert uppercase/alternate character + if (key.type == VK_CHAR) { + insertCharacter(getCharForKey(key, true)); // true = uppercase/alternate char + return; + } + + switch (key.type) { + case VK_BACKSPACE: + // One-shot: delete up to 5 characters on long press + for (int i = 0; i < 5; ++i) { + if (inputText.empty()) + break; + deleteCharacter(); + } + break; + case VK_ENTER: + submitText(); + break; + case VK_SPACE: + insertCharacter(' '); + break; + case VK_ESC: + if (onTextEntered) { + onTextEntered(""); + } + break; + default: + break; + } +} + +void VirtualKeyboard::insertCharacter(char c) +{ + if (inputText.length() < 160) { // Reasonable text length limit + inputText += c; + } +} + +void VirtualKeyboard::deleteCharacter() +{ + if (!inputText.empty()) { + inputText.pop_back(); + } +} + +void VirtualKeyboard::submitText() +{ + LOG_INFO("Virtual keyboard: submitting text '%s'", inputText.c_str()); + + // Only submit if text is not empty + if (!inputText.empty() && onTextEntered) { + // Store callback and text to submit before clearing callback + std::function callback = onTextEntered; + std::string textToSubmit = inputText; + onTextEntered = nullptr; + // Don't clear inputText here - let the calling module handle cleanup + // inputText = ""; // Removed: keep text visible until module cleans up + callback(textToSubmit); + } else if (inputText.empty()) { + // For empty text, just ignore the submission - don't clear callback + // This keeps the virtual keyboard responsive for further input + LOG_INFO("Virtual keyboard: empty text submitted, ignoring - keyboard remains active"); + } else { + // No callback available + if (screen) { + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + } + } +} + +void VirtualKeyboard::setInputText(const std::string &text) +{ + inputText = text; +} + +std::string VirtualKeyboard::getInputText() const +{ + return inputText; +} + +void VirtualKeyboard::setHeader(const std::string &header) +{ + headerText = header; +} + +void VirtualKeyboard::setCallback(std::function callback) +{ + onTextEntered = callback; +} + +void VirtualKeyboard::resetTimeout() +{ + lastActivityTime = millis(); +} + +bool VirtualKeyboard::isTimedOut() const +{ + return (millis() - lastActivityTime) > TIMEOUT_MS; +} + +} // namespace graphics diff --git a/src/graphics/VirtualKeyboard.h b/src/graphics/VirtualKeyboard.h new file mode 100644 index 000000000..169163b57 --- /dev/null +++ b/src/graphics/VirtualKeyboard.h @@ -0,0 +1,80 @@ +#pragma once + +#include "configuration.h" +#include +#include +#include + +namespace graphics +{ + +enum VirtualKeyType { VK_CHAR, VK_BACKSPACE, VK_ENTER, VK_SHIFT, VK_ESC, VK_SPACE }; + +struct VirtualKey { + char character; + VirtualKeyType type; + uint8_t x; + uint8_t y; + uint8_t width; + uint8_t height; +}; + +class VirtualKeyboard +{ + public: + VirtualKeyboard(); + ~VirtualKeyboard(); + + void draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY); + void setInputText(const std::string &text); + std::string getInputText() const; + void setHeader(const std::string &header); + void setCallback(std::function callback); + + // Navigation methods for encoder input + void moveCursorUp(); + void moveCursorDown(); + void moveCursorLeft(); + void moveCursorRight(); + void handlePress(); + void handleLongPress(); + + // Timeout management + void resetTimeout(); + bool isTimedOut() const; + + private: + static const uint8_t KEYBOARD_ROWS = 4; + static const uint8_t KEYBOARD_COLS = 11; + static const uint8_t KEY_WIDTH = 9; + static const uint8_t KEY_HEIGHT = 9; // Compressed to fit 4 rows on 64px displays + static const uint8_t KEYBOARD_START_Y = 26; // Start just below input box bottom + + VirtualKey keyboard[KEYBOARD_ROWS][KEYBOARD_COLS]; + + std::string inputText; + std::string headerText; + std::function onTextEntered; + + uint8_t cursorRow; + uint8_t cursorCol; + + // Timeout management for auto-exit + uint32_t lastActivityTime; + static const uint32_t TIMEOUT_MS = 60000; // 1 minute timeout + + void initializeKeyboard(); + void drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t w, uint8_t h, + bool isLastCol); + void drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY); + + // Unified cursor movement helper + void moveCursorDelta(int dRow, int dCol); + + char getCharForKey(const VirtualKey &key, bool isLongPress = false); + void insertCharacter(char c); + void deleteCharacter(); + void submitText(); +}; + +} // namespace graphics diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 512f650ec..e02948864 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -10,7 +10,10 @@ #include "graphics/Screen.h" #include "graphics/SharedUIDisplay.h" #include "graphics/draw/UIRenderer.h" +#include "input/RotaryEncoderInterruptImpl1.h" +#include "input/UpDownInterruptImpl1.h" #include "main.h" +#include "mesh/MeshTypes.h" #include "modules/AdminModule.h" #include "modules/CannedMessageModule.h" #include "modules/KeyVerificationModule.h" diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index 3d635e588..221d95075 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -38,6 +38,8 @@ bool NotificationRenderer::pauseBanner = false; notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none; uint32_t NotificationRenderer::numDigits = 0; uint32_t NotificationRenderer::currentNumber = 0; +VirtualKeyboard *NotificationRenderer::virtualKeyboard = nullptr; +std::function NotificationRenderer::textInputCallback = nullptr; uint32_t pow_of_10(uint32_t n) { @@ -89,14 +91,33 @@ void NotificationRenderer::resetBanner() void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state) { - if (!isOverlayBannerShowing() && alertBannerMessage[0] != '\0') - resetBanner(); - if (!isOverlayBannerShowing() || pauseBanner) + // Handle text_input notifications first - they have their own timeout/banner logic + if (current_notification_type == notificationTypeEnum::text_input) { + // Check for timeout and reset if needed for text input + if (millis() > alertBannerUntil && alertBannerUntil > 0) { + resetBanner(); + return; + } + drawTextInput(display, state); return; + } + + if (millis() > alertBannerUntil && alertBannerUntil > 0) { + resetBanner(); + } + + // Exit if no banner is showing or banner is paused + if (!isOverlayBannerShowing() || pauseBanner) { + return; + } + switch (current_notification_type) { case notificationTypeEnum::none: // Do nothing - no notification to display break; + case notificationTypeEnum::text_input: + // Already handled above with dedicated logic (early return). Keep a case here to satisfy -Wswitch. + break; case notificationTypeEnum::text_banner: case notificationTypeEnum::selection_picker: drawAlertBannerOverlay(display, state); @@ -575,6 +596,90 @@ void NotificationRenderer::drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUi "Please be patient and do not power off."); } +void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state) +{ + if (virtualKeyboard) { + // Check for timeout and auto-exit if needed + if (virtualKeyboard->isTimedOut()) { + LOG_INFO("Virtual keyboard timeout - auto-exiting"); + // Cancel virtual keyboard - call callback with empty string to indicate timeout + auto callback = textInputCallback; // Store callback before clearing + + // Clean up first to prevent re-entry + delete virtualKeyboard; + virtualKeyboard = nullptr; + textInputCallback = nullptr; + resetBanner(); + + // Call callback after cleanup + if (callback) { + callback(""); + } + + // Restore normal overlays + if (screen) { + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + } + return; + } + + // Handle input events for virtual keyboard navigation + if (inEvent.inputEvent != INPUT_BROKER_NONE) { + if (inEvent.inputEvent == INPUT_BROKER_UP) { + virtualKeyboard->moveCursorUp(); + } else if (inEvent.inputEvent == INPUT_BROKER_DOWN) { + virtualKeyboard->moveCursorDown(); + } else if (inEvent.inputEvent == INPUT_BROKER_LEFT) { + virtualKeyboard->moveCursorLeft(); + } else if (inEvent.inputEvent == INPUT_BROKER_RIGHT) { + virtualKeyboard->moveCursorRight(); + } else if (inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) { + // Long press UP = move left + virtualKeyboard->moveCursorLeft(); + } else if (inEvent.inputEvent == INPUT_BROKER_USER_PRESS) { + // Long press DOWN = move right + virtualKeyboard->moveCursorRight(); + } else if (inEvent.inputEvent == INPUT_BROKER_SELECT) { + virtualKeyboard->handlePress(); + } else if (inEvent.inputEvent == INPUT_BROKER_SELECT_LONG) { + virtualKeyboard->handleLongPress(); + } else if (inEvent.inputEvent == INPUT_BROKER_CANCEL) { + // Cancel virtual keyboard - call callback with empty string + auto callback = textInputCallback; // Store callback before clearing + + // Clean up first to prevent re-entry + delete virtualKeyboard; + virtualKeyboard = nullptr; + textInputCallback = nullptr; + resetBanner(); + + // Call callback after cleanup + if (callback) { + callback(""); + } + + // Restore normal overlays + if (screen) { + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); + } + return; + } + + // Reset input event after processing + inEvent.inputEvent = INPUT_BROKER_NONE; + } + + // Clear the display and draw virtual keyboard + display->setColor(BLACK); + display->fillRect(0, 0, display->getWidth(), display->getHeight()); + display->setColor(WHITE); + virtualKeyboard->draw(display, 0, 0); + } else { + // If virtualKeyboard is null, reset the banner to avoid getting stuck + resetBanner(); + } +} + bool NotificationRenderer::isOverlayBannerShowing() { return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil); diff --git a/src/graphics/draw/NotificationRenderer.h b/src/graphics/draw/NotificationRenderer.h index 9c30b329c..edb069513 100644 --- a/src/graphics/draw/NotificationRenderer.h +++ b/src/graphics/draw/NotificationRenderer.h @@ -3,6 +3,9 @@ #include "OLEDDisplay.h" #include "OLEDDisplayUi.h" #include "graphics/Screen.h" +#include "graphics/VirtualKeyboard.h" +#include +#include #define MAX_LINES 5 namespace graphics @@ -22,6 +25,8 @@ class NotificationRenderer static std::function alertBannerCallback; static uint32_t numDigits; static uint32_t currentNumber; + static VirtualKeyboard *virtualKeyboard; + static std::function textInputCallback; static bool pauseBanner; @@ -30,6 +35,7 @@ class NotificationRenderer static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state); + static void drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1], uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth = 0); diff --git a/src/input/InputBroker.h b/src/input/InputBroker.h index 4487fa662..012a403f5 100644 --- a/src/input/InputBroker.h +++ b/src/input/InputBroker.h @@ -4,6 +4,7 @@ enum input_broker_event { INPUT_BROKER_NONE = 0, INPUT_BROKER_SELECT = 10, + INPUT_BROKER_SELECT_LONG, INPUT_BROKER_UP = 17, INPUT_BROKER_DOWN = 18, INPUT_BROKER_LEFT = 19, diff --git a/src/input/TrackballInterruptBase.cpp b/src/input/TrackballInterruptBase.cpp index d41ad2fd6..4c8ce6409 100644 --- a/src/input/TrackballInterruptBase.cpp +++ b/src/input/TrackballInterruptBase.cpp @@ -1,12 +1,14 @@ #include "TrackballInterruptBase.h" #include "configuration.h" +extern bool osk_found; TrackballInterruptBase::TrackballInterruptBase(const char *name) : concurrency::OSThread(name), _originName(name) {} void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress, input_broker_event eventDown, input_broker_event eventUp, input_broker_event eventLeft, - input_broker_event eventRight, input_broker_event eventPressed, void (*onIntDown)(), - void (*onIntUp)(), void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)()) + input_broker_event eventRight, input_broker_event eventPressed, + input_broker_event eventPressedLong, void (*onIntDown)(), void (*onIntUp)(), + void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)()) { this->_pinDown = pinDown; this->_pinUp = pinUp; @@ -18,6 +20,7 @@ void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLef this->_eventLeft = eventLeft; this->_eventRight = eventRight; this->_eventPressed = eventPressed; + this->_eventPressedLong = eventPressedLong; if (pinPress != 255) { pinMode(pinPress, INPUT_PULLUP); @@ -40,9 +43,9 @@ void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLef attachInterrupt(this->_pinRight, onIntRight, TB_DIRECTION); } - LOG_DEBUG("Trackball GPIO initialized (%d, %d, %d, %d, %d)", this->_pinUp, this->_pinDown, this->_pinLeft, this->_pinRight, - pinPress); - + LOG_DEBUG("Trackball GPIO initialized - UP:%d DOWN:%d LEFT:%d RIGHT:%d PRESS:%d", this->_pinUp, this->_pinDown, + this->_pinLeft, this->_pinRight, pinPress); + osk_found = true; this->setInterval(100); } @@ -50,10 +53,47 @@ int32_t TrackballInterruptBase::runOnce() { InputEvent e; e.inputEvent = INPUT_BROKER_NONE; + + // Handle long press detection for press button + if (pressDetected && pressStartTime > 0) { + uint32_t pressDuration = millis() - pressStartTime; + bool buttonStillPressed = false; + +#if defined(T_DECK) + buttonStillPressed = (this->action == TB_ACTION_PRESSED); +#else + buttonStillPressed = !digitalRead(_pinPress); +#endif + + if (!buttonStillPressed) { + // Button released + if (pressDuration < LONG_PRESS_DURATION) { + // Short press + e.inputEvent = this->_eventPressed; + } + // Reset state + pressDetected = false; + pressStartTime = 0; + lastLongPressEventTime = 0; + this->action = TB_ACTION_NONE; + } else if (pressDuration >= LONG_PRESS_DURATION) { + // Long press detected + uint32_t currentTime = millis(); + // Only trigger long press event if enough time has passed since the last one + if (lastLongPressEventTime == 0 || (currentTime - lastLongPressEventTime) >= LONG_PRESS_REPEAT_INTERVAL) { + e.inputEvent = this->_eventPressedLong; + lastLongPressEventTime = currentTime; + } + this->action = TB_ACTION_PRESSED_LONG; + } + } + #if defined(T_DECK) // T-deck gets a super-simple debounce on trackball - if (this->action == TB_ACTION_PRESSED) { - // LOG_DEBUG("Trackball event Press"); - e.inputEvent = this->_eventPressed; + if (this->action == TB_ACTION_PRESSED && !pressDetected) { + // Start long press detection + pressDetected = true; + pressStartTime = millis(); + // Don't send event yet, wait to see if it's a long press } else if (this->action == TB_ACTION_UP && lastEvent == TB_ACTION_UP) { // LOG_DEBUG("Trackball event UP"); e.inputEvent = this->_eventUp; @@ -68,9 +108,11 @@ int32_t TrackballInterruptBase::runOnce() e.inputEvent = this->_eventRight; } #else - if (this->action == TB_ACTION_PRESSED && !digitalRead(_pinPress)) { - // LOG_DEBUG("Trackball event Press"); - e.inputEvent = this->_eventPressed; + if (this->action == TB_ACTION_PRESSED && !digitalRead(_pinPress) && !pressDetected) { + // Start long press detection + pressDetected = true; + pressStartTime = millis(); + // Don't send event yet, wait to see if it's a long press } else if (this->action == TB_ACTION_UP && !digitalRead(_pinUp)) { // LOG_DEBUG("Trackball event UP"); e.inputEvent = this->_eventUp; @@ -91,10 +133,16 @@ int32_t TrackballInterruptBase::runOnce() e.kbchar = 0x00; this->notifyObservers(&e); } - lastEvent = action; - this->action = TB_ACTION_NONE; - return 100; + // Only update lastEvent for non-press actions or completed press actions + if (this->action != TB_ACTION_PRESSED || !pressDetected) { + lastEvent = action; + if (!pressDetected) { + this->action = TB_ACTION_NONE; + } + } + + return 50; // Check more frequently for better long press detection } void TrackballInterruptBase::intPressHandler() diff --git a/src/input/TrackballInterruptBase.h b/src/input/TrackballInterruptBase.h index 92db8720e..38be22f20 100644 --- a/src/input/TrackballInterruptBase.h +++ b/src/input/TrackballInterruptBase.h @@ -18,8 +18,8 @@ class TrackballInterruptBase : public Observable, public con explicit TrackballInterruptBase(const char *name); void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress, input_broker_event eventDown, input_broker_event eventUp, input_broker_event eventLeft, input_broker_event eventRight, - input_broker_event eventPressed, void (*onIntDown)(), void (*onIntUp)(), void (*onIntLeft)(), void (*onIntRight)(), - void (*onIntPress)()); + input_broker_event eventPressed, input_broker_event eventPressedLong, void (*onIntDown)(), void (*onIntUp)(), + void (*onIntLeft)(), void (*onIntRight)(), void (*onIntPress)()); void intPressHandler(); void intDownHandler(); void intUpHandler(); @@ -33,6 +33,7 @@ class TrackballInterruptBase : public Observable, public con enum TrackballInterruptBaseActionType { TB_ACTION_NONE, TB_ACTION_PRESSED, + TB_ACTION_PRESSED_LONG, TB_ACTION_UP, TB_ACTION_DOWN, TB_ACTION_LEFT, @@ -46,12 +47,20 @@ class TrackballInterruptBase : public Observable, public con volatile TrackballInterruptBaseActionType action = TB_ACTION_NONE; + // Long press detection for press button + uint32_t pressStartTime = 0; + bool pressDetected = false; + uint32_t lastLongPressEventTime = 0; + static const uint32_t LONG_PRESS_DURATION = 500; // ms + static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 500; // ms - interval between repeated long press events + private: input_broker_event _eventDown = INPUT_BROKER_NONE; input_broker_event _eventUp = INPUT_BROKER_NONE; input_broker_event _eventLeft = INPUT_BROKER_NONE; input_broker_event _eventRight = INPUT_BROKER_NONE; input_broker_event _eventPressed = INPUT_BROKER_NONE; + input_broker_event _eventPressedLong = INPUT_BROKER_NONE; const char *_originName; TrackballInterruptBaseActionType lastEvent = TB_ACTION_NONE; }; diff --git a/src/input/TrackballInterruptImpl1.cpp b/src/input/TrackballInterruptImpl1.cpp index 896238f38..594facdeb 100644 --- a/src/input/TrackballInterruptImpl1.cpp +++ b/src/input/TrackballInterruptImpl1.cpp @@ -13,11 +13,12 @@ void TrackballInterruptImpl1::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLe input_broker_event eventLeft = INPUT_BROKER_LEFT; input_broker_event eventRight = INPUT_BROKER_RIGHT; input_broker_event eventPressed = INPUT_BROKER_SELECT; + input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG; TrackballInterruptBase::init(pinDown, pinUp, pinLeft, pinRight, pinPress, eventDown, eventUp, eventLeft, eventRight, - eventPressed, TrackballInterruptImpl1::handleIntDown, TrackballInterruptImpl1::handleIntUp, - TrackballInterruptImpl1::handleIntLeft, TrackballInterruptImpl1::handleIntRight, - TrackballInterruptImpl1::handleIntPressed); + eventPressed, eventPressedLong, TrackballInterruptImpl1::handleIntDown, + TrackballInterruptImpl1::handleIntUp, TrackballInterruptImpl1::handleIntLeft, + TrackballInterruptImpl1::handleIntRight, TrackballInterruptImpl1::handleIntPressed); inputBroker->registerSource(this); } diff --git a/src/main.cpp b/src/main.cpp index ef5f5a721..23ffa6b6d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -191,6 +191,8 @@ ScanI2C::DeviceAddress cardkb_found = ScanI2C::ADDRESS_NONE; uint8_t kb_model; // global bool to record that a kb is present bool kb_found = false; +// global bool to record that on-screen keyboard (OSK) is present +bool osk_found = false; // The I2C address of the RTC Module (if found) ScanI2C::DeviceAddress rtc_found = ScanI2C::ADDRESS_NONE; @@ -1412,6 +1414,10 @@ void setup() #endif #endif +#if defined(HAS_TRACKBALL) || (defined(INPUTDRIVER_ENCODER_TYPE) && INPUTDRIVER_ENCODER_TYPE == 2) + osk_found = true; +#endif + #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WEBSERVER // Start web server thread. webServerThread = new WebServerThread(); diff --git a/src/main.h b/src/main.h index 3568daad2..2ddd4862f 100644 --- a/src/main.h +++ b/src/main.h @@ -32,6 +32,7 @@ extern ScanI2C::DeviceAddress screen_found; extern ScanI2C::DeviceAddress cardkb_found; extern uint8_t kb_model; extern bool kb_found; +extern bool osk_found; extern ScanI2C::DeviceAddress rtc_found; extern ScanI2C::DeviceAddress accelerometer_found; extern ScanI2C::FoundDevice rgb_found; diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index d40dcd24f..76b950322 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -13,12 +13,16 @@ #include "detect/ScanI2C.h" #include "graphics/Screen.h" #include "graphics/SharedUIDisplay.h" +#include "graphics/draw/NotificationRenderer.h" #include "graphics/emotes.h" #include "graphics/images.h" #include "main.h" // for cardkb_found #include "mesh/generated/meshtastic/cannedmessages.pb.h" #include "modules/AdminModule.h" #include "modules/ExternalNotificationModule.h" // for buzzer control +#if HAS_TRACKBALL +#include "input/TrackballInterruptImpl1.h" +#endif #if !MESHTASTIC_EXCLUDE_GPS #include "GPS.h" #endif @@ -38,6 +42,7 @@ extern ScanI2C::DeviceAddress cardkb_found; extern bool graphics::isMuted; +extern bool osk_found; static const char *cannedMessagesConfigFile = "/prefs/cannedConf.proto"; static NodeNum lastDest = NODENUM_BROADCAST; @@ -151,10 +156,13 @@ int CannedMessageModule::splitConfiguredMessages() int tempCount = 0; // Insert at position 0 (top) tempMessages[tempCount++] = "[Select Destination]"; - #if defined(USE_VIRTUAL_KEYBOARD) - // Add a "Free Text" entry at the top if using a keyboard + // Add a "Free Text" entry at the top if using a touch screen virtual keyboard tempMessages[tempCount++] = "[-- Free Text --]"; +#else + if (osk_found && screen) { + tempMessages[tempCount++] = "[-- Free Text --]"; + } #endif // First message always starts at buffer start @@ -341,6 +349,8 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) case CANNED_MESSAGE_RUN_STATE_FREETEXT: return handleFreeTextInput(event); // All allowed input for this state + // Virtual keyboard mode: Show virtual keyboard and handle input + // If sending, block all input except global/system (handled above) case CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE: return 1; @@ -627,6 +637,56 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo notifyObservers(&e); return true; } +#else + if (strcmp(current, "[-- Free Text --]") == 0) { + if (osk_found && screen) { + char headerBuffer[64]; + if (this->dest == NODENUM_BROADCAST) { + snprintf(headerBuffer, sizeof(headerBuffer), "To: Broadcast@%s", channels.getName(this->channel)); + } else { + snprintf(headerBuffer, sizeof(headerBuffer), "To: %s", getNodeName(this->dest)); + } + screen->showTextInput(headerBuffer, "", 300000, [this](const std::string &text) { + if (!text.empty()) { + this->freetext = text.c_str(); + this->payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; + runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; + currentMessageIndex = -1; + + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + this->notifyObservers(&e); + screen->forceDisplay(); + + setIntervalFromNow(500); + return; + } else { + // Don't delete virtual keyboard immediately - it might still be executing + // Instead, just clear the callback and reset banner to stop input processing + graphics::NotificationRenderer::textInputCallback = nullptr; + graphics::NotificationRenderer::resetBanner(); + + // Return to inactive state + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + this->currentMessageIndex = -1; + this->freetext = ""; + this->cursor = 0; + + // Force display update to show normal screen + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + this->notifyObservers(&e); + screen->forceDisplay(); + + // Schedule cleanup for next loop iteration to ensure safe deletion + setIntervalFromNow(50); + return; + } + }); + + return true; + } + } #endif // Normal canned message selection @@ -943,12 +1003,54 @@ int32_t CannedMessageModule::runOnce() // Normal module disable/idle handling if ((this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) || (this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE)) { + // Clean up virtual keyboard if needed when going inactive + if (graphics::NotificationRenderer::virtualKeyboard && graphics::NotificationRenderer::textInputCallback == nullptr) { + LOG_INFO("Performing delayed virtual keyboard cleanup"); + delete graphics::NotificationRenderer::virtualKeyboard; + graphics::NotificationRenderer::virtualKeyboard = nullptr; + } + temporaryMessage = ""; return INT32_MAX; } + // Handle delayed virtual keyboard message sending + if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { + // Virtual keyboard message sending case - text was not empty + if (this->freetext.length() > 0) { + LOG_INFO("Processing delayed virtual keyboard send: '%s'", this->freetext.c_str()); + sendText(this->dest, this->channel, this->freetext.c_str(), true); + + // Clean up virtual keyboard after sending + if (graphics::NotificationRenderer::virtualKeyboard) { + LOG_INFO("Cleaning up virtual keyboard after message send"); + delete graphics::NotificationRenderer::virtualKeyboard; + graphics::NotificationRenderer::virtualKeyboard = nullptr; + graphics::NotificationRenderer::textInputCallback = nullptr; + graphics::NotificationRenderer::resetBanner(); + } + + // Clear payload to indicate virtual keyboard processing is complete + // But keep SENDING_ACTIVE state to show "Sending..." screen for 2 seconds + this->payload = 0; + } else { + // Empty message, just go inactive + LOG_INFO("Empty freetext detected in delayed processing, returning to inactive state"); + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + } + + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + this->currentMessageIndex = -1; + this->freetext = ""; + this->cursor = 0; + this->notifyObservers(&e); + return 2000; + } + UIFrameEvent e; - if ((this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) || + if ((this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload != 0 && + this->payload != CANNED_MESSAGE_RUN_STATE_FREETEXT) || (this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) || (this->runState == CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION)) { this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; @@ -958,6 +1060,18 @@ int32_t CannedMessageModule::runOnce() this->freetext = ""; this->cursor = 0; this->notifyObservers(&e); + } + // Handle SENDING_ACTIVE state transition after virtual keyboard message + else if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload == 0) { + // This happens after virtual keyboard message sending is complete + LOG_INFO("Virtual keyboard message sending completed, returning to inactive state"); + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + temporaryMessage = ""; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + this->currentMessageIndex = -1; + this->freetext = ""; + this->cursor = 0; + this->notifyObservers(&e); } else if (((this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT)) && !Throttle::isWithinTimespanMs(this->lastTouchMillis, INACTIVATE_AFTER_MS)) { // Reset module on inactivity @@ -966,9 +1080,23 @@ int32_t CannedMessageModule::runOnce() this->freetext = ""; this->cursor = 0; this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + + // Clean up virtual keyboard if it exists during timeout + if (graphics::NotificationRenderer::virtualKeyboard) { + LOG_INFO("Cleaning up virtual keyboard due to module timeout"); + delete graphics::NotificationRenderer::virtualKeyboard; + graphics::NotificationRenderer::virtualKeyboard = nullptr; + graphics::NotificationRenderer::textInputCallback = nullptr; + graphics::NotificationRenderer::resetBanner(); + } + this->notifyObservers(&e); } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) { - if (this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { + if (this->payload == 0) { + // [Exit] button pressed - return to inactive state + LOG_INFO("Processing [Exit] action - returning to inactive state"); + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + } else if (this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { if (this->freetext.length() > 0) { sendText(this->dest, this->channel, this->freetext.c_str(), true); this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; From 1c329d9ffa7c8c930c712bece9e467ac72f54018 Mon Sep 17 00:00:00 2001 From: notmasteryet <146979+notmasteryet@users.noreply.github.com> Date: Thu, 21 Aug 2025 22:12:21 -0500 Subject: [PATCH 03/54] Log more information about ignored packet --- src/mesh/RadioLibInterface.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/mesh/RadioLibInterface.cpp b/src/mesh/RadioLibInterface.cpp index e3ef58f14..946b1982c 100644 --- a/src/mesh/RadioLibInterface.cpp +++ b/src/mesh/RadioLibInterface.cpp @@ -422,7 +422,8 @@ void RadioLibInterface::handleReceiveInterrupt() } #endif if (state != RADIOLIB_ERR_NONE) { - LOG_ERROR("Ignore received packet due to error=%d", state); + LOG_ERROR("Ignore received packet due to error=%d (maybe to=0x%08x, from=0x%08x, flags=0x%02x)", state, + radioBuffer.header.to, radioBuffer.header.from, radioBuffer.header.flags); rxBad++; airTime->logAirtime(RX_ALL_LOG, xmitMsec); From 8b42bf7a957f5ff8018307ec0dec87c4e2fce286 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 23 Aug 2025 05:47:51 -0500 Subject: [PATCH 04/54] Don't reboot when setting lora config with portduino sim radio (#7716) --- src/modules/AdminModule.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 4014e1c36..4c893e462 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -718,6 +718,13 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) requiresReboot = false; } +#if defined(ARCH_PORTDUINO) + // If running on portduino and using SimRadio, do not require reboot + if (SimRadio::instance) { + requiresReboot = false; + } +#endif + #ifdef RF95_FAN_EN // Turn PA off if disabled by config if (c.payload_variant.lora.pa_fan_disabled) { From 1037fa562221590d5ebaf43e3908e472eaa18380 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 23 Aug 2025 06:31:30 -0500 Subject: [PATCH 05/54] Update meshtastic/device-ui digest to 0f32b64 (#7723) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index cce4d2dcf..543205996 100644 --- a/platformio.ini +++ b/platformio.ini @@ -118,7 +118,7 @@ lib_deps = [device-ui_base] lib_deps = # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master - https://github.com/meshtastic/device-ui/archive/3dc7cf3e233aaa8cc23492cca50541fc099ebfa1.zip + https://github.com/meshtastic/device-ui/archive/0f32b64dca418c6465763ec576509a6a2bfbc50a.zip ; Common libs for environmental measurements in telemetry module [environmental_base] From 5136c8ba24bccea7de83ad7cc35e69a3a28f5b2b Mon Sep 17 00:00:00 2001 From: Lewis He Date: Sat, 23 Aug 2025 19:46:59 +0800 Subject: [PATCH 06/54] The T-Deck-Pro 4G version sets the modem to be disabled by default. (#7715) Co-authored-by: Ben Meadors --- src/main.cpp | 10 ++++++++++ variants/esp32s3/t-deck-pro/variant.h | 9 +++++++++ 2 files changed, 19 insertions(+) diff --git a/src/main.cpp b/src/main.cpp index ef5f5a721..0260cbc07 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -404,6 +404,16 @@ void setup() initDeepSleep(); +#if defined(MODEM_POWER_EN) + pinMode(MODEM_POWER_EN, OUTPUT); + digitalWrite(MODEM_POWER_EN, LOW); +#endif + +#if defined(MODEM_PWRKEY) + pinMode(MODEM_PWRKEY, OUTPUT); + digitalWrite(MODEM_PWRKEY, LOW); +#endif + #if defined(LORA_TCXO_GPIO) pinMode(LORA_TCXO_GPIO, OUTPUT); digitalWrite(LORA_TCXO_GPIO, HIGH); diff --git a/variants/esp32s3/t-deck-pro/variant.h b/variants/esp32s3/t-deck-pro/variant.h index b08d3f65f..abe0a772a 100644 --- a/variants/esp32s3/t-deck-pro/variant.h +++ b/variants/esp32s3/t-deck-pro/variant.h @@ -92,3 +92,12 @@ #define SX126X_DIO3_TCXO_VOLTAGE 2.4 // Internally the TTGO module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the sx1262interface // code) + +#define MODEM_POWER_EN 41 +#define MODEM_PWRKEY 40 +#define MODEM_RST 9 +#define MODEM_RI 7 +#define MODEM_DTR 8 +#define MODEM_RX 10 +#define MODEM_TX 11 + From 35f5b7ec03536875dd9810431139764ee686c4d9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 23 Aug 2025 06:48:57 -0500 Subject: [PATCH 07/54] Update caveman99-stm32-Crypto digest to 1aa30eb (#7725) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- arch/stm32/stm32.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arch/stm32/stm32.ini b/arch/stm32/stm32.ini index d91607a7d..8b7d256b3 100644 --- a/arch/stm32/stm32.ini +++ b/arch/stm32/stm32.ini @@ -50,7 +50,7 @@ lib_deps = ${radiolib_base.lib_deps} # renovate: datasource=git-refs depName=caveman99-stm32-Crypto packageName=https://github.com/caveman99/Crypto gitBranch=main - https://github.com/caveman99/Crypto/archive/eae9c768054118a9399690f8af202853d1ae8516.zip + https://github.com/caveman99/Crypto/archive/1aa30eb536bd52a576fde6dfa393bf7349cf102d.zip lib_ignore = OneButton From 4fef890466f025e5013b45f0f1c0f2d2ea6f714d Mon Sep 17 00:00:00 2001 From: Austin Date: Sat, 23 Aug 2025 11:41:57 -0400 Subject: [PATCH 08/54] Renovate: Always use `master` as the base. (#7726) --- renovate.json | 1 + 1 file changed, 1 insertion(+) diff --git a/renovate.json b/renovate.json index e90462cc3..187cdc600 100644 --- a/renovate.json +++ b/renovate.json @@ -8,6 +8,7 @@ "replacements:all", "workarounds:all" ], + "baseBranchPatterns": ["master"], "forkProcessing": "enabled", "ignoreDeps": [ "protobufs" From 103ea2f1681ee4f9b36da4f6e2888ef8c9774a54 Mon Sep 17 00:00:00 2001 From: TN <44137240+TN666@users.noreply.github.com> Date: Sun, 24 Aug 2025 20:39:50 +0800 Subject: [PATCH 09/54] Add more text message test cases for meshpacket serializer (#7709) * Add more text message test cases for meshpacket serializer * fix the trunk issue --- .../ports/test_text_message.cpp | 127 +++++++++++++----- .../test_serializer.cpp | 8 ++ 2 files changed, 103 insertions(+), 32 deletions(-) diff --git a/test/test_meshpacket_serializer/ports/test_text_message.cpp b/test/test_meshpacket_serializer/ports/test_text_message.cpp index de3f34541..6213b08d5 100644 --- a/test/test_meshpacket_serializer/ports/test_text_message.cpp +++ b/test/test_meshpacket_serializer/ports/test_text_message.cpp @@ -1,42 +1,105 @@ #include "../test_helpers.h" +#include + +// Helper function to test common packet fields and structure +void verify_text_message_packet_structure(const std::string &json, const char *expected_text) +{ + TEST_ASSERT_TRUE(json.length() > 0); + + // Use smart pointer for automatic memory management + std::unique_ptr root(JSON::Parse(json.c_str())); + TEST_ASSERT_NOT_NULL(root.get()); + TEST_ASSERT_TRUE(root->IsObject()); + + JSONObject jsonObj = root->AsObject(); + + // Check basic packet fields - use helper function to reduce duplication + auto check_field = [&](const char *field, uint32_t expected_value) { + auto it = jsonObj.find(field); + TEST_ASSERT_TRUE(it != jsonObj.end()); + TEST_ASSERT_EQUAL(expected_value, (uint32_t)it->second->AsNumber()); + }; + + check_field("from", 0x11223344); + check_field("to", 0x55667788); + check_field("id", 0x9999); + + // Check message type + auto type_it = jsonObj.find("type"); + TEST_ASSERT_TRUE(type_it != jsonObj.end()); + TEST_ASSERT_EQUAL_STRING("text", type_it->second->AsString().c_str()); + + // Check payload + auto payload_it = jsonObj.find("payload"); + TEST_ASSERT_TRUE(payload_it != jsonObj.end()); + TEST_ASSERT_TRUE(payload_it->second->IsObject()); + + JSONObject payload = payload_it->second->AsObject(); + auto text_it = payload.find("text"); + TEST_ASSERT_TRUE(text_it != payload.end()); + TEST_ASSERT_EQUAL_STRING(expected_text, text_it->second->AsString().c_str()); + + // No need for manual delete with smart pointer +} // Test TEXT_MESSAGE_APP port void test_text_message_serialization() { const char *test_text = "Hello Meshtastic!"; meshtastic_MeshPacket packet = - create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, (const uint8_t *)test_text, strlen(test_text)); + create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, reinterpret_cast(test_text), strlen(test_text)); std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); - TEST_ASSERT_TRUE(json.length() > 0); - - JSONValue *root = JSON::Parse(json.c_str()); - TEST_ASSERT_NOT_NULL(root); - TEST_ASSERT_TRUE(root->IsObject()); - - JSONObject jsonObj = root->AsObject(); - - // Check basic packet fields - TEST_ASSERT_TRUE(jsonObj.find("from") != jsonObj.end()); - TEST_ASSERT_EQUAL(0x11223344, (uint32_t)jsonObj["from"]->AsNumber()); - - TEST_ASSERT_TRUE(jsonObj.find("to") != jsonObj.end()); - TEST_ASSERT_EQUAL(0x55667788, (uint32_t)jsonObj["to"]->AsNumber()); - - TEST_ASSERT_TRUE(jsonObj.find("id") != jsonObj.end()); - TEST_ASSERT_EQUAL(0x9999, (uint32_t)jsonObj["id"]->AsNumber()); - - // Check message type - TEST_ASSERT_TRUE(jsonObj.find("type") != jsonObj.end()); - TEST_ASSERT_EQUAL_STRING("text", jsonObj["type"]->AsString().c_str()); - - // Check payload - TEST_ASSERT_TRUE(jsonObj.find("payload") != jsonObj.end()); - TEST_ASSERT_TRUE(jsonObj["payload"]->IsObject()); - - JSONObject payload = jsonObj["payload"]->AsObject(); - TEST_ASSERT_TRUE(payload.find("text") != payload.end()); - TEST_ASSERT_EQUAL_STRING("Hello Meshtastic!", payload["text"]->AsString().c_str()); - - delete root; + verify_text_message_packet_structure(json, test_text); +} + +// Test with nullptr to check robustness +void test_text_message_serialization_null() +{ + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, nullptr, 0); + + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + verify_text_message_packet_structure(json, ""); +} + +// Test TEXT_MESSAGE_APP port with very long message (boundary testing) +void test_text_message_serialization_long_text() +{ + // Test with actual message size limits + constexpr size_t MAX_MESSAGE_SIZE = 200; // Typical LoRa payload limit + std::string long_text(MAX_MESSAGE_SIZE, 'A'); + + meshtastic_MeshPacket packet = create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, + reinterpret_cast(long_text.c_str()), long_text.length()); + + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + verify_text_message_packet_structure(json, long_text.c_str()); +} + +// Test with message over size limit (should fail) +void test_text_message_serialization_oversized() +{ + constexpr size_t OVERSIZED_MESSAGE = 250; // Over the limit + std::string oversized_text(OVERSIZED_MESSAGE, 'B'); + + meshtastic_MeshPacket packet = create_test_packet( + meshtastic_PortNum_TEXT_MESSAGE_APP, reinterpret_cast(oversized_text.c_str()), oversized_text.length()); + + // Should fail or return empty/error + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + // Should only verify first 234 characters for oversized messages + std::string expected_text = oversized_text.substr(0, 234); + verify_text_message_packet_structure(json, expected_text.c_str()); +} + +// Add test for malformed UTF-8 sequences +void test_text_message_serialization_invalid_utf8() +{ + const uint8_t invalid_utf8[] = {0xFF, 0xFE, 0xFD, 0x00}; // Invalid UTF-8 + meshtastic_MeshPacket packet = + create_test_packet(meshtastic_PortNum_TEXT_MESSAGE_APP, invalid_utf8, sizeof(invalid_utf8) - 1); + + // Should not crash, may produce replacement characters + std::string json = MeshPacketSerializer::JsonSerialize(&packet, false); + TEST_ASSERT_TRUE(json.length() > 0); } diff --git a/test/test_meshpacket_serializer/test_serializer.cpp b/test/test_meshpacket_serializer/test_serializer.cpp index d74031fa4..7f51a2e70 100644 --- a/test/test_meshpacket_serializer/test_serializer.cpp +++ b/test/test_meshpacket_serializer/test_serializer.cpp @@ -4,6 +4,10 @@ // Forward declarations for test functions void test_text_message_serialization(); +void test_text_message_serialization_null(); +void test_text_message_serialization_long_text(); +void test_text_message_serialization_oversized(); +void test_text_message_serialization_invalid_utf8(); void test_position_serialization(); void test_nodeinfo_serialization(); void test_waypoint_serialization(); @@ -21,6 +25,10 @@ void setup() // Text message tests RUN_TEST(test_text_message_serialization); + RUN_TEST(test_text_message_serialization_null); + RUN_TEST(test_text_message_serialization_long_text); + RUN_TEST(test_text_message_serialization_oversized); + RUN_TEST(test_text_message_serialization_invalid_utf8); // Position tests RUN_TEST(test_position_serialization); From 915f882e1f6b741b64f3a4a1ded2445412909b74 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sun, 24 Aug 2025 10:13:18 -0500 Subject: [PATCH 10/54] Pkc fix (#7722) --- src/mesh/Router.cpp | 5 +++-- src/modules/AdminModule.cpp | 28 +++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index cceacfe9e..1f835bca7 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -529,8 +529,9 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p) #endif // Don't use PKC with Ham mode !owner.is_licensed && - // Don't use PKC if it's not explicitly requested and a non-primary channel is requested - !(p->pki_encrypted != true && p->channel > 0) && + // Don't use PKC on 'serial' or 'gpio' channels unless explicitly requested + !(p->pki_encrypted != true && (strcasecmp(channels.getName(chIndex), Channels::serialChannel) == 0 || + strcasecmp(channels.getName(chIndex), Channels::gpioChannel) == 0)) && // Check for valid keys and single node destination config.security.private_key.size == 32 && !isBroadcast(p->to) && node != nullptr && // Check for a known public key for the destination diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 4c893e462..9e8ce2e6b 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -505,7 +505,9 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta if (mp.decoded.want_response && !myReply) { myReply = allocErrorResponse(meshtastic_Routing_Error_NONE, &mp); } - + if (mp.pki_encrypted) { + myReply->pki_encrypted = true; + } return handled; } @@ -941,6 +943,9 @@ void AdminModule::handleGetOwner(const meshtastic_MeshPacket &req) res.which_payload_variant = meshtastic_AdminMessage_get_owner_response_tag; setPassKey(&res); myReply = allocDataProtobuf(res); + if (req.pki_encrypted) { + myReply->pki_encrypted = true; + } } } @@ -1012,6 +1017,9 @@ void AdminModule::handleGetConfig(const meshtastic_MeshPacket &req, const uint32 res.which_payload_variant = meshtastic_AdminMessage_get_config_response_tag; setPassKey(&res); myReply = allocDataProtobuf(res); + if (req.pki_encrypted) { + myReply->pki_encrypted = true; + } } } @@ -1099,6 +1107,9 @@ void AdminModule::handleGetModuleConfig(const meshtastic_MeshPacket &req, const res.which_payload_variant = meshtastic_AdminMessage_get_module_config_response_tag; setPassKey(&res); myReply = allocDataProtobuf(res); + if (req.pki_encrypted) { + myReply->pki_encrypted = true; + } } } @@ -1123,6 +1134,9 @@ void AdminModule::handleGetNodeRemoteHardwarePins(const meshtastic_MeshPacket &r } setPassKey(&r); myReply = allocDataProtobuf(r); + if (req.pki_encrypted) { + myReply->pki_encrypted = true; + } } void AdminModule::handleGetDeviceMetadata(const meshtastic_MeshPacket &req) @@ -1132,6 +1146,9 @@ void AdminModule::handleGetDeviceMetadata(const meshtastic_MeshPacket &req) r.which_payload_variant = meshtastic_AdminMessage_get_device_metadata_response_tag; setPassKey(&r); myReply = allocDataProtobuf(r); + if (req.pki_encrypted) { + myReply->pki_encrypted = true; + } } void AdminModule::handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &req) @@ -1200,6 +1217,9 @@ void AdminModule::handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &r r.which_payload_variant = meshtastic_AdminMessage_get_device_connection_status_response_tag; setPassKey(&r); myReply = allocDataProtobuf(r); + if (req.pki_encrypted) { + myReply->pki_encrypted = true; + } } void AdminModule::handleGetChannel(const meshtastic_MeshPacket &req, uint32_t channelIndex) @@ -1211,6 +1231,9 @@ void AdminModule::handleGetChannel(const meshtastic_MeshPacket &req, uint32_t ch r.which_payload_variant = meshtastic_AdminMessage_get_channel_response_tag; setPassKey(&r); myReply = allocDataProtobuf(r); + if (req.pki_encrypted) { + myReply->pki_encrypted = true; + } } } @@ -1220,6 +1243,9 @@ void AdminModule::handleGetDeviceUIConfig(const meshtastic_MeshPacket &req) r.which_payload_variant = meshtastic_AdminMessage_get_ui_config_response_tag; r.get_ui_config_response = uiconfig; myReply = allocDataProtobuf(r); + if (req.pki_encrypted) { + myReply->pki_encrypted = true; + } } void AdminModule::reboot(int32_t seconds) From 3d825c51dd7bd837ea58f2e148c2b63c698e67c6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 24 Aug 2025 14:44:51 -0500 Subject: [PATCH 11/54] Update meshtastic/device-ui digest to 0f32b64 (#7728) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index cce4d2dcf..543205996 100644 --- a/platformio.ini +++ b/platformio.ini @@ -118,7 +118,7 @@ lib_deps = [device-ui_base] lib_deps = # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master - https://github.com/meshtastic/device-ui/archive/3dc7cf3e233aaa8cc23492cca50541fc099ebfa1.zip + https://github.com/meshtastic/device-ui/archive/0f32b64dca418c6465763ec576509a6a2bfbc50a.zip ; Common libs for environmental measurements in telemetry module [environmental_base] From 1eafdfcbc88f32a657c1e0d76b4cb36d15072578 Mon Sep 17 00:00:00 2001 From: Tom Fifield Date: Mon, 25 Aug 2025 05:45:29 +1000 Subject: [PATCH 12/54] Reduce power of EU433 to 10dBm (#7733) We are currently blocked from making the breaking change to fix EU_433 channel centres until 3.0 (https://github.com/meshtastic/firmware/issues/3371 ) However, as already updated in https://github.com/meshtastic/meshtastic/pull/919 the documentation, the power limit for EU_433 is 10dBm. We can change the power limit without breaking anything, so this patch sets the power limit to match the ETSI spec without changing any other settings. --- src/mesh/RadioInterface.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index c210d5d48..e721431b1 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -32,9 +32,12 @@ const RegionInfo regions[] = { RDEF(US, 902.0f, 928.0f, 100, 0, 30, true, false, false), /* - https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf + EN300220 ETSI V3.2.1 [Table B.1, Item H, p. 21] + + https://www.etsi.org/deliver/etsi_en/300200_300299/30022002/03.02.01_60/en_30022002v030201p.pdf + FIXME: https://github.com/meshtastic/firmware/issues/3371 */ - RDEF(EU_433, 433.0f, 434.0f, 10, 0, 12, true, false, false), + RDEF(EU_433, 433.0f, 434.0f, 10, 0, 10, true, false, false), /* https://www.thethingsnetwork.org/docs/lorawan/duty-cycle/ From 5b9db81819f45b625683047c3b78bfece8d23b2e Mon Sep 17 00:00:00 2001 From: TN666 Date: Mon, 25 Aug 2025 23:35:03 +0800 Subject: [PATCH 13/54] Add more test case for encrypted packet test --- .../ports/test_encrypted.cpp | 74 +++++++++++++------ .../test_serializer.cpp | 2 + 2 files changed, 55 insertions(+), 21 deletions(-) diff --git a/test/test_meshpacket_serializer/ports/test_encrypted.cpp b/test/test_meshpacket_serializer/ports/test_encrypted.cpp index 557ee7a49..24866654a 100644 --- a/test/test_meshpacket_serializer/ports/test_encrypted.cpp +++ b/test/test_meshpacket_serializer/ports/test_encrypted.cpp @@ -1,20 +1,32 @@ #include "../test_helpers.h" -// Test encrypted packet serialization -void test_encrypted_packet_serialization() +// test data initialization +const int from = 0x11223344; +const int to = 0x55667788; +const int id = 0x9999; + +// Helper function to create a test encrypted packet +meshtastic_MeshPacket create_test_encrypted_packet(uint32_t from, uint32_t to, uint32_t id, const char *data) { meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_zero; - packet.from = 0x11223344; - packet.to = 0x55667788; - packet.id = 0x9999; + packet.from = from; + packet.to = to; + packet.id = id; packet.which_payload_variant = meshtastic_MeshPacket_encrypted_tag; - // Add some dummy encrypted data - const char *encrypted_data = "encrypted_payload_data"; - packet.encrypted.size = strlen(encrypted_data); - memcpy(packet.encrypted.bytes, encrypted_data, packet.encrypted.size); + if (data) { + packet.encrypted.size = strlen(data); + memcpy(packet.encrypted.bytes, data, packet.encrypted.size); + } - std::string json = MeshPacketSerializer::JsonSerializeEncrypted(&packet); + return packet; +} + +// Comprehensive helper function for all encrypted packet assertions +void assert_encrypted_packet(const std::string &json, uint32_t expected_from, uint32_t expected_to, uint32_t expected_id, + size_t expected_size) +{ + // Parse and validate JSON TEST_ASSERT_TRUE(json.length() > 0); JSONValue *root = JSON::Parse(json.c_str()); @@ -23,28 +35,48 @@ void test_encrypted_packet_serialization() JSONObject jsonObj = root->AsObject(); - // Check basic packet fields + // Assert basic packet fields TEST_ASSERT_TRUE(jsonObj.find("from") != jsonObj.end()); - TEST_ASSERT_EQUAL(0x11223344, (uint32_t)jsonObj["from"]->AsNumber()); + TEST_ASSERT_EQUAL(expected_from, (uint32_t)jsonObj.at("from")->AsNumber()); TEST_ASSERT_TRUE(jsonObj.find("to") != jsonObj.end()); - TEST_ASSERT_EQUAL(0x55667788, (uint32_t)jsonObj["to"]->AsNumber()); + TEST_ASSERT_EQUAL(expected_to, (uint32_t)jsonObj.at("to")->AsNumber()); TEST_ASSERT_TRUE(jsonObj.find("id") != jsonObj.end()); - TEST_ASSERT_EQUAL(0x9999, (uint32_t)jsonObj["id"]->AsNumber()); + TEST_ASSERT_EQUAL(expected_id, (uint32_t)jsonObj.at("id")->AsNumber()); - // Check that it has encrypted data fields (not "payload" but "bytes" and "size") + // Assert encrypted data fields TEST_ASSERT_TRUE(jsonObj.find("bytes") != jsonObj.end()); - TEST_ASSERT_TRUE(jsonObj["bytes"]->IsString()); + TEST_ASSERT_TRUE(jsonObj.at("bytes")->IsString()); TEST_ASSERT_TRUE(jsonObj.find("size") != jsonObj.end()); - TEST_ASSERT_EQUAL(22, (int)jsonObj["size"]->AsNumber()); // strlen("encrypted_payload_data") = 22 + TEST_ASSERT_EQUAL(expected_size, (int)jsonObj.at("size")->AsNumber()); - // The encrypted data should be hex-encoded + // Assert hex encoding std::string encrypted_hex = jsonObj["bytes"]->AsString(); - TEST_ASSERT_TRUE(encrypted_hex.length() > 0); - // Should be twice the size of the original data (hex encoding) - TEST_ASSERT_EQUAL(44, encrypted_hex.length()); // 22 * 2 = 44 + TEST_ASSERT_EQUAL(expected_size * 2, encrypted_hex.length()); delete root; } + +// Test encrypted packet serialization +void test_encrypted_packet_serialization() +{ + const char *data = "encrypted_payload_data"; + + meshtastic_MeshPacket packet = create_test_encrypted_packet(from, to, id, data); + std::string json = MeshPacketSerializer::JsonSerializeEncrypted(&packet); + + assert_encrypted_packet(json, from, to, id, strlen(data)); +} + +// Test empty encrypted packet +void test_empty_encrypted_packet() +{ + const char *data = ""; + + meshtastic_MeshPacket packet = create_test_encrypted_packet(from, to, id, data); + std::string json = MeshPacketSerializer::JsonSerializeEncrypted(&packet); + + assert_encrypted_packet(json, from, to, id, strlen(data)); +} diff --git a/test/test_meshpacket_serializer/test_serializer.cpp b/test/test_meshpacket_serializer/test_serializer.cpp index 7f51a2e70..484db8d74 100644 --- a/test/test_meshpacket_serializer/test_serializer.cpp +++ b/test/test_meshpacket_serializer/test_serializer.cpp @@ -18,6 +18,7 @@ void test_telemetry_environment_metrics_missing_fields(); void test_telemetry_environment_metrics_complete_coverage(); void test_telemetry_environment_metrics_unset_fields(); void test_encrypted_packet_serialization(); +void test_empty_encrypted_packet(); void setup() { @@ -49,6 +50,7 @@ void setup() // Encrypted packet test RUN_TEST(test_encrypted_packet_serialization); + RUN_TEST(test_empty_encrypted_packet); UNITY_END(); } From 9a1c2c9b61fd06c03809adb860d249c2f715a712 Mon Sep 17 00:00:00 2001 From: m1nl Date: Mon, 25 Aug 2025 19:42:13 +0200 Subject: [PATCH 14/54] setup flags which describe framework / device PM capabilties --- src/platform/esp32/architecture.h | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/platform/esp32/architecture.h b/src/platform/esp32/architecture.h index 522e862ac..4373b2cf0 100644 --- a/src/platform/esp32/architecture.h +++ b/src/platform/esp32/architecture.h @@ -215,3 +215,13 @@ #endif #define SERIAL0_RX_GPIO 3 // Always GPIO3 on ESP32 // FIXME: may be different on ESP32-S3, etc. + +// Setup flag, which indicates if our device supports power management +#ifdef CONFIG_PM_ENABLE +#define HAS_ESP32_PM_SUPPORT 1 +#endif + +// Setup flag, which indicates if our device supports dynamic light sleep +#if defined(HAS_ESP32_PM_SUPPORT) && defined(CONFIG_FREERTOS_USE_TICKLESS_IDLE) +#define HAS_ESP32_DYNAMIC_LIGHT_SLEEP 1 +#endif \ No newline at end of file From ba26d03b1b941c57998d29ea448931b4f5aeb8f0 Mon Sep 17 00:00:00 2001 From: m1nl Date: Mon, 25 Aug 2025 19:44:13 +0200 Subject: [PATCH 15/54] standarize values of HAS_32768HZ capability flag --- src/platform/esp32/architecture.h | 3 +++ src/platform/esp32/main-esp32.cpp | 10 +++++----- variants/esp32s3/tbeam-s3-core/variant.h | 4 ++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/platform/esp32/architecture.h b/src/platform/esp32/architecture.h index 4373b2cf0..80749ee6b 100644 --- a/src/platform/esp32/architecture.h +++ b/src/platform/esp32/architecture.h @@ -45,6 +45,9 @@ #ifndef HAS_CUSTOM_CRYPTO_ENGINE #define HAS_CUSTOM_CRYPTO_ENGINE 1 #endif +#ifndef HAS_32768HZ +#define HAS_32768HZ 0 +#endif #if defined(HAS_AXP192) || defined(HAS_AXP2101) #define HAS_PMU diff --git a/src/platform/esp32/main-esp32.cpp b/src/platform/esp32/main-esp32.cpp index cdea53c9a..760964119 100644 --- a/src/platform/esp32/main-esp32.cpp +++ b/src/platform/esp32/main-esp32.cpp @@ -64,7 +64,7 @@ void getMacAddr(uint8_t *dmac) #endif } -#ifdef HAS_32768HZ +#if HAS_32768HZ #define CALIBRATE_ONE(cali_clk) calibrate_one(cali_clk, #cali_clk) static uint32_t calibrate_one(rtc_cal_sel_t cal_clk, const char *name) @@ -86,17 +86,17 @@ void enableSlowCLK() uint32_t cal_32k = CALIBRATE_ONE(RTC_CAL_32K_XTAL); if (cal_32k == 0) { - LOG_DEBUG("32K XTAL OSC has not started up"); + LOG_DEBUG("32k XTAL OSC has not started up"); } else { rtc_clk_slow_freq_set(RTC_SLOW_FREQ_32K_XTAL); - LOG_DEBUG("Switch RTC Source to 32.768Khz succeeded, using 32K XTAL"); + LOG_DEBUG("Switch RTC Source to 32.768kHz succeeded, using 32k XTAL"); CALIBRATE_ONE(RTC_CAL_RTC_MUX); CALIBRATE_ONE(RTC_CAL_32K_XTAL); } CALIBRATE_ONE(RTC_CAL_RTC_MUX); CALIBRATE_ONE(RTC_CAL_32K_XTAL); if (rtc_clk_slow_freq_get() != RTC_SLOW_FREQ_32K_XTAL) { - LOG_WARN("Failed to switch 32K XTAL RTC source to 32.768Khz !!! "); + LOG_WARN("Failed to switch 32K XTAL RTC source to 32.768kHz !!! "); return; } } @@ -182,7 +182,7 @@ void esp32Setup() res = esp_task_wdt_add(NULL); assert(res == ESP_OK); -#ifdef HAS_32768HZ +#if HAS_32768HZ enableSlowCLK(); #endif } diff --git a/variants/esp32s3/tbeam-s3-core/variant.h b/variants/esp32s3/tbeam-s3-core/variant.h index dabd52980..40ba0307a 100644 --- a/variants/esp32s3/tbeam-s3-core/variant.h +++ b/variants/esp32s3/tbeam-s3-core/variant.h @@ -62,6 +62,6 @@ // #define PCF8563_RTC 0x51 //Putting definitions in variant. h does not compile correctly // has 32768 Hz crystal -#define HAS_32768HZ +#define HAS_32768HZ 1 -#define USE_SH1106 \ No newline at end of file +#define USE_SH1106 From 5aa486d6c2e5f284b6530a4bbaf95902738e71cc Mon Sep 17 00:00:00 2001 From: m1nl Date: Mon, 25 Aug 2025 19:56:15 +0200 Subject: [PATCH 16/54] set HAS_32768HZ for Heltec V3 board --- variants/esp32s3/heltec_v3/variant.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/variants/esp32s3/heltec_v3/variant.h b/variants/esp32s3/heltec_v3/variant.h index 4f1d91db8..d760c3b7f 100644 --- a/variants/esp32s3/heltec_v3/variant.h +++ b/variants/esp32s3/heltec_v3/variant.h @@ -40,3 +40,5 @@ #define SX126X_DIO2_AS_RF_SWITCH #define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +#define HAS_32768HZ 1 \ No newline at end of file From 1a279c6053f485fdfb606145767124b208fb20a4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 26 Aug 2025 06:31:38 -0500 Subject: [PATCH 17/54] Upgrade trunk (#7677) Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com> --- .trunk/trunk.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index de38e3ec0..a0dcf2ff5 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -4,19 +4,19 @@ cli: plugins: sources: - id: trunk - ref: v1.7.1 + ref: v1.7.2 uri: https://github.com/trunk-io/plugins lint: enabled: - - checkov@3.2.461 - - renovate@41.74.0 + - checkov@3.2.465 + - renovate@41.82.10 - prettier@3.6.2 - trufflehog@3.90.5 - yamllint@1.37.1 - bandit@1.8.6 - - trivy@0.64.1 - - taplo@0.9.3 - - ruff@0.12.7 + - trivy@0.65.0 + - taplo@0.10.0 + - ruff@0.12.10 - isort@6.0.1 - markdownlint@0.45.0 - oxipng@9.1.5 @@ -25,7 +25,7 @@ lint: - flake8@7.3.0 - hadolint@2.12.1-beta - shfmt@3.6.0 - - shellcheck@0.10.0 + - shellcheck@0.11.0 - black@25.1.0 - git-diff-check - gitleaks@8.28.0 From 3f5c30e3b35c5a8c779ff3542c52cfe7b107c871 Mon Sep 17 00:00:00 2001 From: Manuel <71137295+mverch67@users.noreply.github.com> Date: Tue, 26 Aug 2025 16:35:25 +0200 Subject: [PATCH 18/54] T-Lora Pager (#7613) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * initial commit * preset rotary1 encoder * define TAB+ESC * haptic feedback * allow switch off haptic feedback * enable audio amplifier * include PR4684 * fix for tft target * add ES8311 audio codec * fix KB scan duplicate * display workaround to avoid debris * fix debris on display * keyboard backlight * enable screen options * fsm based bounce-free rotary encoder implementation * use fsm RotaryEncoder only for T-Lora Pager * change inputbroker default config to allow using rotary wheel for screens AND menues --------- Co-authored-by: Thomas Göttgens Co-authored-by: Ben Meadors --- src/detect/ScanI2C.h | 3 +- src/detect/ScanI2CTwoWire.cpp | 10 +- src/graphics/Screen.cpp | 4 +- src/graphics/ScreenFonts.h | 2 +- src/graphics/TFTDisplay.cpp | 199 +++++++++++++-- src/graphics/TFTDisplay.h | 2 + src/graphics/draw/DebugRenderer.cpp | 8 +- src/graphics/draw/MenuHandler.cpp | 10 +- src/graphics/draw/UIRenderer.cpp | 2 +- src/graphics/images.h | 3 +- src/input/RotaryEncoderImpl.cpp | 73 ++++++ src/input/RotaryEncoderImpl.h | 28 +++ src/input/TLoraPagerKeyboard.cpp | 230 ++++++++++++++++++ src/input/TLoraPagerKeyboard.h | 23 +- src/input/cardKbI2cImpl.cpp | 4 +- src/main.cpp | 37 ++- src/main.h | 2 +- src/mesh/NodeDB.cpp | 11 +- src/modules/Modules.cpp | 10 + src/platform/esp32/architecture.h | 2 + .../extra_variants/t_lora_pager/variant.cpp | 27 ++ variants/esp32s3/tlora-pager/pins_arduino.h | 19 ++ variants/esp32s3/tlora-pager/platformio.ini | 70 ++++++ variants/esp32s3/tlora-pager/variant.h | 125 ++++++++++ 24 files changed, 855 insertions(+), 49 deletions(-) create mode 100644 src/input/RotaryEncoderImpl.cpp create mode 100644 src/input/RotaryEncoderImpl.h create mode 100644 src/input/TLoraPagerKeyboard.cpp create mode 100644 src/platform/extra_variants/t_lora_pager/variant.cpp create mode 100644 variants/esp32s3/tlora-pager/pins_arduino.h create mode 100644 variants/esp32s3/tlora-pager/platformio.ini create mode 100644 variants/esp32s3/tlora-pager/variant.h diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h index c1358861b..e46c6f623 100644 --- a/src/detect/ScanI2C.h +++ b/src/detect/ScanI2C.h @@ -79,7 +79,8 @@ class ScanI2C BQ27220, LTR553ALS, BHI260AP, - BMM150 + BMM150, + DRV2605 } DeviceType; // typedef uint8_t DeviceAddress; diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 8b3670cd9..9aef9defe 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -483,8 +483,14 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) type = MLX90614; logFoundDevice("MLX90614", (uint8_t)addr.address); } else { - type = MPR121KB; - logFoundDevice("MPR121KB", (uint8_t)addr.address); + registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1); // DRV2605_REG_STATUS + if (registerValue == 0xe0) { + type = DRV2605; + logFoundDevice("DRV2605", (uint8_t)addr.address); + } else { + type = MPR121KB; + logFoundDevice("MPR121KB", (uint8_t)addr.address); + } } break; diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index fa71e17d8..dea08d5ba 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -318,7 +318,7 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O dispdev = new SSD1306Wire(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); #elif defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7789_CS) || \ - defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) + defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) dispdev = new TFTDisplay(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); #elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY) @@ -550,7 +550,7 @@ void Screen::setup() #else if (!config.display.flip_screen) { #if defined(ST7701_CS) || defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \ - defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) + defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) static_cast(dispdev)->flipScreenVertically(); #elif defined(USE_ST7789) static_cast(dispdev)->flipScreenVertically(); diff --git a/src/graphics/ScreenFonts.h b/src/graphics/ScreenFonts.h index 84ec45977..a25417b05 100644 --- a/src/graphics/ScreenFonts.h +++ b/src/graphics/ScreenFonts.h @@ -73,7 +73,7 @@ #endif #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)) && \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS)) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) // The screen is bigger so use bigger fonts #define FONT_SMALL FONT_MEDIUM_LOCAL // Height: 19 diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp index f8787612f..b1814005e 100644 --- a/src/graphics/TFTDisplay.cpp +++ b/src/graphics/TFTDisplay.cpp @@ -562,6 +562,91 @@ class LGFX : public lgfx::LGFX_Device static LGFX *tft = nullptr; +#elif defined(ST7796_CS) +#include // Graphics and font library for ST7796 driver chip + +class LGFX : public lgfx::LGFX_Device +{ + lgfx::Panel_ST7796 _panel_instance; + lgfx::Bus_SPI _bus_instance; + lgfx::Light_PWM _light_instance; + + public: + LGFX(void) + { + { + auto cfg = _bus_instance.config(); + + // SPI + cfg.spi_host = ST7796_SPI_HOST; + cfg.spi_mode = 0; + cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by dividing + // 80MHz by an integer) + cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving + cfg.spi_3wire = false; + cfg.use_lock = true; // Set to true to use transaction locking + cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch / + // SPI_DMA_CH_AUTO=auto setting) + cfg.pin_sclk = ST7796_SCK; // Set SPI SCLK pin number + cfg.pin_mosi = ST7796_SDA; // Set SPI MOSI pin number + cfg.pin_miso = ST7796_MISO; // Set SPI MISO pin number (-1 = disable) + cfg.pin_dc = ST7796_RS; // Set SPI DC pin number (-1 = disable) + + _bus_instance.config(cfg); // applies the set value to the bus. + _panel_instance.setBus(&_bus_instance); // set the bus on the panel. + } + + { // Set the display panel control. + auto cfg = _panel_instance.config(); // Gets a structure for display panel settings. + + cfg.pin_cs = ST7796_CS; // Pin number where CS is connected (-1 = disable) + cfg.pin_rst = ST7796_RESET; // Pin number where RST is connected (-1 = disable) + cfg.pin_busy = ST7796_BUSY; // Pin number where BUSY is connected (-1 = disable) + + // cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC + // cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC + cfg.panel_width = TFT_WIDTH; // actual displayable width + cfg.panel_height = TFT_HEIGHT; // actual displayable height + cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction + cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction + cfg.offset_rotation = TFT_OFFSET_ROTATION; // Rotation direction value offset 0~7 (4~7 is mirrored) +#ifdef TFT_DUMMY_READ_PIXELS + cfg.dummy_read_pixel = TFT_DUMMY_READ_PIXELS; // Number of bits for dummy read before pixel readout +#else + cfg.dummy_read_pixel = 8; // Number of bits for dummy read before pixel readout +#endif + cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read + cfg.readable = true; // Set to true if data can be read + cfg.invert = true; // Set to true if the light/darkness of the panel is reversed + cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped + cfg.dlen_16bit = + false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI + cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) + + _panel_instance.config(cfg); + } + +#ifdef ST7796_BL + // Set the backlight control. (delete if not necessary) + { + auto cfg = _light_instance.config(); // Gets a structure for backlight settings. + + cfg.pin_bl = ST7796_BL; // Pin number to which the backlight is connected + cfg.invert = false; // true to invert the brightness of the backlight + cfg.freq = 44100; + cfg.pwm_channel = 7; + + _light_instance.config(cfg); + _panel_instance.setLight(&_light_instance); // Set the backlight on the panel. + } +#endif + + setPanel(&_panel_instance); // Sets the panel to use. + } +}; + +static LGFX *tft = nullptr; + #elif defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) #include // Graphics and font library for ILI9341/ILI9342 driver chip @@ -997,8 +1082,9 @@ static LGFX *tft = nullptr; #endif -#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ST7789_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \ - defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST72xx_DE) || (ARCH_PORTDUINO && HAS_SCREEN != 0) +#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ST7789_CS) || defined(ST7796_CS) || defined(ILI9341_DRIVER) || \ + defined(ILI9342_DRIVER) || defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST72xx_DE) || \ + (ARCH_PORTDUINO && HAS_SCREEN != 0) #include "SPILock.h" #include "TFTDisplay.h" #include @@ -1047,32 +1133,97 @@ void TFTDisplay::display(bool fromBlank) { if (fromBlank) tft->fillScreen(TFT_BLACK); - // tft->clear(); + concurrency::LockGuard g(spiLock); - uint16_t x, y; + uint32_t x, y; + uint32_t y_byteIndex; + uint8_t y_byteMask; + uint32_t x_FirstPixelUpdate; + uint32_t x_LastPixelUpdate; + bool isset, dblbuf_isset; + uint16_t colorTftMesh, colorTftBlack; + bool somethingChanged = false; - for (y = 0; y < displayHeight; y++) { - for (x = 0; x < displayWidth; x++) { - auto isset = buffer[x + (y / 8) * displayWidth] & (1 << (y & 7)); + // Store colors byte-reversed so that TFT_eSPI doesn't have to swap bytes in a separate step + colorTftMesh = (TFT_MESH >> 8) | ((TFT_MESH & 0xFF) << 8); + colorTftBlack = (TFT_BLACK >> 8) | ((TFT_BLACK & 0xFF) << 8); + + y = 0; + while (y < displayHeight) { + y_byteIndex = (y / 8) * displayWidth; + y_byteMask = (1 << (y & 7)); + + // Step 1: Do a quick scan of 8 rows together. This allows fast-forwarding over unchanged screen areas. + if (y_byteMask == 1) { if (!fromBlank) { - // get src pixel in the page based ordering the OLED lib uses FIXME, super inefficent - auto dblbuf_isset = buffer_back[x + (y / 8) * displayWidth] & (1 << (y & 7)); - if (isset != dblbuf_isset) { - tft->drawPixel(x, y, isset ? TFT_MESH : TFT_BLACK); + for (x = 0; x < displayWidth; x++) { + if (buffer[x + y_byteIndex] != buffer_back[x + y_byteIndex]) + break; } - } else if (isset) { - tft->drawPixel(x, y, TFT_MESH); + } else { + for (x = 0; x < displayWidth; x++) { + if (buffer[x + y_byteIndex] != 0) + break; + } + } + if (x >= displayWidth) { + // No changed pixels found in these 8 rows, fast-forward to the next 8 + y = y + 8; + continue; } } + + // Step 2: Scan each of the 8 rows individually. Find the first pixel in each row that needs updating + for (x_FirstPixelUpdate = 0; x_FirstPixelUpdate < displayWidth; x_FirstPixelUpdate++) { + isset = buffer[x_FirstPixelUpdate + y_byteIndex] & y_byteMask; + + if (!fromBlank) { + // get src pixel in the page based ordering the OLED lib uses + dblbuf_isset = buffer_back[x_FirstPixelUpdate + y_byteIndex] & y_byteMask; + if (isset != dblbuf_isset) { + break; + } + } else if (isset) { + break; + } + } + + // Did we find a pixel that needs updating on this row? + if (x_FirstPixelUpdate < displayWidth) { + + // Quickly write out the first changed pixel (saves another array lookup) + linePixelBuffer[x_FirstPixelUpdate] = isset ? colorTftMesh : colorTftBlack; + x_LastPixelUpdate = x_FirstPixelUpdate; + + // Step 3: copy all remaining pixels in this row into the pixel line buffer, + // while also recording the last pixel in the row that needs updating + for (x = x_FirstPixelUpdate + 1; x < displayWidth; x++) { + isset = buffer[x + y_byteIndex] & y_byteMask; + linePixelBuffer[x] = isset ? colorTftMesh : colorTftBlack; + + if (!fromBlank) { + dblbuf_isset = buffer_back[x + y_byteIndex] & y_byteMask; + if (isset != dblbuf_isset) { + x_LastPixelUpdate = x; + } + } else if (isset) { + x_LastPixelUpdate = x; + } + } + + // Step 4: Send the changed pixels on this line to the screen as a single block transfer. + // This function accepts pixel data MSB first so it can dump the memory straight out the SPI port. + tft->pushRect(x_FirstPixelUpdate, y, (x_LastPixelUpdate - x_FirstPixelUpdate + 1), 1, + &linePixelBuffer[x_FirstPixelUpdate]); + + somethingChanged = true; + } + y++; } // Copy the Buffer to the Back Buffer - for (y = 0; y < (displayHeight / 8); y++) { - for (x = 0; x < displayWidth; x++) { - uint16_t pos = x + y * displayWidth; - buffer_back[pos] = buffer[pos]; - } - } + if (somethingChanged) + memcpy(buffer_back, buffer, displayBufferSize); } void TFTDisplay::sdlLoop() @@ -1264,13 +1415,21 @@ bool TFTDisplay::connect() tft->setRotation(1); // T-Deck has the TFT in landscape #elif defined(T_WATCH_S3) tft->setRotation(2); // T-Watch S3 left-handed orientation -#elif ARCH_PORTDUINO || defined(SENSECAP_INDICATOR) +#elif ARCH_PORTDUINO || defined(SENSECAP_INDICATOR) || defined(T_LORA_PAGER) tft->setRotation(0); // use config.yaml to set rotation #else tft->setRotation(3); // Orient horizontal and wide underneath the silkscreen name label #endif tft->fillScreen(TFT_BLACK); + if (this->linePixelBuffer == NULL) { + this->linePixelBuffer = (uint16_t *)malloc(sizeof(uint16_t) * displayWidth); + + if (!this->linePixelBuffer) { + LOG_ERROR("Not enough memory to create TFT line buffer\n"); + return false; + } + } return true; } diff --git a/src/graphics/TFTDisplay.h b/src/graphics/TFTDisplay.h index 60adfdf7c..27672ad29 100644 --- a/src/graphics/TFTDisplay.h +++ b/src/graphics/TFTDisplay.h @@ -58,4 +58,6 @@ class TFTDisplay : public OLEDDisplay // Connect to the display virtual bool connect() override; + + uint16_t *linePixelBuffer = nullptr; }; \ No newline at end of file diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index 5d9b5a33b..a0f29f10d 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -94,7 +94,8 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16 if (!Throttle::isWithinTimespanMs(storeForwardModule->lastHeartbeat, (storeForwardModule->heartbeatInterval * 1200))) { // no heartbeat, overlap a bit #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || ARCH_PORTDUINO) && \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \ + ARCH_PORTDUINO) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8, imgQuestionL1); @@ -106,7 +107,7 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16 #endif } else { #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS)) && \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS)) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 16, 8, imgSFL1); @@ -121,7 +122,8 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16 } else { // TODO: Raspberry Pi supports more than just the one screen size #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || ARCH_PORTDUINO) && \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS) || \ + ARCH_PORTDUINO) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8, imgInfoL1); diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 512f650ec..bcd8d8ee8 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -434,8 +434,8 @@ void menuHandler::systemBaseMenu() optionsArray[options] = "Notifications"; optionsEnumArray[options++] = Notifications; -#if defined(ST7789_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) || \ - defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || HAS_TFT +#if defined(ST7789_CS) || defined(ST7796_CS) || defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || \ + defined(USE_SH1107) || defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || HAS_TFT optionsArray[options] = "Screen Options"; optionsEnumArray[options++] = ScreenOptions; #endif @@ -725,7 +725,7 @@ void menuHandler::BrightnessPickerMenu() #if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) // For HELTEC devices, use analogWrite to control backlight analogWrite(VTFT_LEDA, uiconfig.screen_brightness); -#elif defined(ST7789_CS) +#elif defined(ST7789_CS) || defined(ST7796_CS) static_cast(screen->getDisplayDevice())->setDisplayBrightness(uiconfig.screen_brightness); #elif defined(USE_OLED) || defined(USE_SSD1306) || defined(USE_SH1106) || defined(USE_SH1107) screen->getDisplayDevice()->setBrightness(uiconfig.screen_brightness); @@ -768,7 +768,7 @@ void menuHandler::TFTColorPickerMenu(OLEDDisplay *display) bannerOptions.optionsArrayPtr = optionsArray; bannerOptions.optionsCount = 10; bannerOptions.bannerCallback = [display](int selected) -> void { -#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || HAS_TFT +#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || HAS_TFT uint8_t TFT_MESH_r = 0; uint8_t TFT_MESH_g = 0; uint8_t TFT_MESH_b = 0; @@ -1045,7 +1045,7 @@ void menuHandler::screenOptionsMenu() } // Only show screen color for TFT displays -#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || HAS_TFT +#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_VISION_MASTER_T190) || defined(T_DECK) || defined(T_LORA_PAGER) || HAS_TFT optionsArray[options] = "Screen Color"; optionsEnumArray[options++] = ScreenColor; #endif diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 71d92616f..049722df8 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -194,7 +194,7 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes } #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS)) && \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || defined(ST7796_CS)) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) if (isHighResolution) { diff --git a/src/graphics/images.h b/src/graphics/images.h index beef3a1b2..c66e4b992 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -27,7 +27,8 @@ const uint8_t bluetoothConnectedIcon[36] PROGMEM = {0xfe, 0x01, 0xff, 0x03, 0x03 0xfe, 0x31, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0xf0, 0x3f, 0xe0, 0x1f}; #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) && \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || \ + ARCH_PORTDUINO) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) const uint8_t imgQuestionL1[] PROGMEM = {0xff, 0x01, 0x01, 0x32, 0x7b, 0x49, 0x49, 0x6f, 0x26, 0x01, 0x01, 0xff}; const uint8_t imgQuestionL2[] PROGMEM = {0x0f, 0x08, 0x08, 0x08, 0x06, 0x0f, 0x0f, 0x06, 0x08, 0x08, 0x08, 0x0f}; diff --git a/src/input/RotaryEncoderImpl.cpp b/src/input/RotaryEncoderImpl.cpp new file mode 100644 index 000000000..b71e800e0 --- /dev/null +++ b/src/input/RotaryEncoderImpl.cpp @@ -0,0 +1,73 @@ +#ifdef T_LORA_PAGER + +#include "RotaryEncoderImpl.h" +#include "InputBroker.h" +#include "RotaryEncoder.h" + +#define ORIGIN_NAME "RotaryEncoder" + +RotaryEncoderImpl *rotaryEncoderImpl; + +RotaryEncoderImpl::RotaryEncoderImpl() : concurrency::OSThread(ORIGIN_NAME), originName(ORIGIN_NAME) {} + +bool RotaryEncoderImpl::init() +{ + if (!moduleConfig.canned_message.updown1_enabled || moduleConfig.canned_message.inputbroker_pin_a == 0 || + moduleConfig.canned_message.inputbroker_pin_b == 0) { + // Input device is disabled. + disable(); + return false; + } + + eventCw = static_cast(moduleConfig.canned_message.inputbroker_event_cw); + eventCcw = static_cast(moduleConfig.canned_message.inputbroker_event_ccw); + eventPressed = static_cast(moduleConfig.canned_message.inputbroker_event_press); + + rotary = new RotaryEncoder(moduleConfig.canned_message.inputbroker_pin_a, moduleConfig.canned_message.inputbroker_pin_b, + moduleConfig.canned_message.inputbroker_pin_press); + rotary->resetButton(); + + inputBroker->registerSource(this); + + LOG_INFO("RotaryEncoder initialized pins(%d, %d, %d), events(%d, %d, %d)", moduleConfig.canned_message.inputbroker_pin_a, + moduleConfig.canned_message.inputbroker_pin_b, moduleConfig.canned_message.inputbroker_pin_press, eventCw, eventCcw, + eventPressed); + return true; +} + +int32_t RotaryEncoderImpl::runOnce() +{ + InputEvent e; + e.inputEvent = INPUT_BROKER_NONE; + e.source = this->originName; + + static uint32_t lastPressed = millis(); + if (rotary->readButton() == RotaryEncoder::ButtonState::BUTTON_PRESSED) { + if (lastPressed + 200 < millis()) { + LOG_DEBUG("Rotary event Press"); + lastPressed = millis(); + e.inputEvent = this->eventPressed; + } + } else { + switch (rotary->process()) { + case RotaryEncoder::DIRECTION_CW: + LOG_DEBUG("Rotary event CW"); + e.inputEvent = this->eventCw; + break; + case RotaryEncoder::DIRECTION_CCW: + LOG_DEBUG("Rotary event CCW"); + e.inputEvent = this->eventCcw; + break; + default: + break; + } + } + + if (e.inputEvent != INPUT_BROKER_NONE) { + this->notifyObservers(&e); + } + + return 20; +} + +#endif \ No newline at end of file diff --git a/src/input/RotaryEncoderImpl.h b/src/input/RotaryEncoderImpl.h new file mode 100644 index 000000000..ae2a7c6fd --- /dev/null +++ b/src/input/RotaryEncoderImpl.h @@ -0,0 +1,28 @@ +#pragma once + +// This is a non-interrupt version of RotaryEncoder which is based on a debounce inherent FSM table (see RotaryEncoder library) + +#include "InputBroker.h" +#include "concurrency/OSThread.h" +#include "mesh/NodeDB.h" + +class RotaryEncoder; + +class RotaryEncoderImpl : public Observable, public concurrency::OSThread +{ + public: + RotaryEncoderImpl(); + bool init(void); + + protected: + virtual int32_t runOnce() override; + + input_broker_event eventCw = INPUT_BROKER_NONE; + input_broker_event eventCcw = INPUT_BROKER_NONE; + input_broker_event eventPressed = INPUT_BROKER_NONE; + + RotaryEncoder *rotary; + const char *originName; +}; + +extern RotaryEncoderImpl *rotaryEncoderImpl; diff --git a/src/input/TLoraPagerKeyboard.cpp b/src/input/TLoraPagerKeyboard.cpp new file mode 100644 index 000000000..b3f62a36c --- /dev/null +++ b/src/input/TLoraPagerKeyboard.cpp @@ -0,0 +1,230 @@ +#if defined(T_LORA_PAGER) + +#include "TLoraPagerKeyboard.h" +#include "main.h" + +#ifndef LEDC_BACKLIGHT_CHANNEL +#define LEDC_BACKLIGHT_CHANNEL 4 +#endif + +#ifndef LEDC_BACKLIGHT_BIT_WIDTH +#define LEDC_BACKLIGHT_BIT_WIDTH 8 +#endif + +#ifndef LEDC_BACKLIGHT_FREQ +#define LEDC_BACKLIGHT_FREQ 1000 // Hz +#endif + +#define _TCA8418_COLS 10 +#define _TCA8418_ROWS 4 +#define _TCA8418_NUM_KEYS 31 + +#define _TCA8418_MULTI_TAP_THRESHOLD 1500 + +using Key = TCA8418KeyboardBase::TCA8418Key; + +constexpr uint8_t modifierRightShiftKey = 29 - 1; // keynum -1 +constexpr uint8_t modifierRightShift = 0b0001; +constexpr uint8_t modifierSymKey = 21 - 1; +constexpr uint8_t modifierSym = 0b0010; + +// Num chars per key, Modulus for rotating through characters +static uint8_t TLoraPagerTapMod[_TCA8418_NUM_KEYS] = {3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3}; + +static unsigned char TLoraPagerTapMap[_TCA8418_NUM_KEYS][3] = {{'q', 'Q', '1'}, + {'w', 'W', '2'}, + {'e', 'E', '3'}, + {'r', 'R', '4'}, + {'t', 'T', '5'}, + {'y', 'Y', '6'}, + {'u', 'U', '7'}, + {'i', 'I', '8'}, + {'o', 'O', '9'}, + {'p', 'P', '0'}, + {'a', 'A', '*'}, + {'s', 'S', '/'}, + {'d', 'D', '+'}, + {'f', 'F', '-'}, + {'g', 'G', '='}, + {'h', 'H', ':'}, + {'j', 'J', '\''}, + {'k', 'K', '"'}, + {'l', 'L', '@'}, + {Key::SELECT, 0x00, Key::TAB}, + {0x00, 0x00, 0x00}, + {'z', 'Z', '_'}, + {'x', 'X', '$'}, + {'c', 'C', ';'}, + {'v', 'V', '?'}, + {'b', 'B', '!'}, + {'n', 'N', ','}, + {'m', 'M', '.'}, + {0x00, 0x00, 0x00}, + {Key::BSP, 0x00, Key::ESC}, + {' ', 0x00, Key::BL_TOGGLE}}; + +TLoraPagerKeyboard::TLoraPagerKeyboard() + : TCA8418KeyboardBase(_TCA8418_ROWS, _TCA8418_COLS), modifierFlag(0), last_modifier_time(0), last_key(-1), next_key(-1), + last_tap(0L), char_idx(0), tap_interval(0) +{ +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + ledcAttach(KB_BL_PIN, LEDC_BACKLIGHT_FREQ, LEDC_BACKLIGHT_BIT_WIDTH); +#else + ledcSetup(LEDC_BACKLIGHT_CHANNEL, LEDC_BACKLIGHT_FREQ, LEDC_BACKLIGHT_BIT_WIDTH); + ledcAttachPin(KB_BL_PIN, LEDC_BACKLIGHT_CHANNEL); +#endif + reset(); +} + +void TLoraPagerKeyboard::reset(void) +{ + TCA8418KeyboardBase::reset(); + pinMode(KB_BL_PIN, OUTPUT); + digitalWrite(KB_BL_PIN, LOW); + setBacklight(false); +} + +// handle multi-key presses (shift and alt) +void TLoraPagerKeyboard::trigger() +{ + uint8_t count = keyCount(); + if (count == 0) + return; + for (uint8_t i = 0; i < count; ++i) { + uint8_t k = readRegister(TCA8418_REG_KEY_EVENT_A + i); + uint8_t key = k & 0x7F; + if (k & 0x80) { + pressed(key); + } else { + released(); + state = Idle; + } + } +} + +void TLoraPagerKeyboard::setBacklight(bool on) +{ + toggleBacklight(!on); +} + +void TLoraPagerKeyboard::pressed(uint8_t key) +{ + if (state == Init || state == Busy) { + return; + } + if (config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_ALL_ENABLED || + config.device.buzzer_mode == meshtastic_Config_DeviceConfig_BuzzerMode_SYSTEM_ONLY) { + hapticFeedback(); + } + + if (modifierFlag && (millis() - last_modifier_time > _TCA8418_MULTI_TAP_THRESHOLD)) { + modifierFlag = 0; + } + + uint8_t next_key = 0; + int row = (key - 1) / 10; + int col = (key - 1) % 10; + + if (row >= _TCA8418_ROWS || col >= _TCA8418_COLS) { + return; // Invalid key + } + + next_key = row * _TCA8418_COLS + col; + state = Held; + + uint32_t now = millis(); + tap_interval = now - last_tap; + + updateModifierFlag(next_key); + if (isModifierKey(next_key)) { + last_modifier_time = now; + } + + if (tap_interval < 0) { + last_tap = 0; + state = Busy; + return; + } + + if (next_key != last_key || tap_interval > _TCA8418_MULTI_TAP_THRESHOLD) { + char_idx = 0; + } else { + char_idx += 1; + } + + last_key = next_key; + last_tap = now; +} + +void TLoraPagerKeyboard::released() +{ + if (state != Held) { + return; + } + + if (last_key < 0 || last_key >= _TCA8418_NUM_KEYS) { + last_key = -1; + state = Idle; + return; + } + + uint32_t now = millis(); + last_tap = now; + + if (TLoraPagerTapMap[last_key][modifierFlag % TLoraPagerTapMod[last_key]] == Key::BL_TOGGLE) { + toggleBacklight(); + return; + } + + queueEvent(TLoraPagerTapMap[last_key][modifierFlag % TLoraPagerTapMod[last_key]]); + if (isModifierKey(last_key) == false) + modifierFlag = 0; +} + +void TLoraPagerKeyboard::hapticFeedback() +{ + drv.setWaveform(0, 14); // strong buzz 100% + drv.setWaveform(1, 0); // end waveform + drv.go(); +} + +// toggle brightness of the backlight in three steps +void TLoraPagerKeyboard::toggleBacklight(bool off) +{ + static uint32_t brightness = 0; + if (off) { + brightness = 0; + } else { + if (brightness == 0) { + brightness = 40; + } else if (brightness == 40) { + brightness = 127; + } else if (brightness >= 127) { + brightness = 0; + } + } + LOG_DEBUG("Toggle backlight: %d", brightness); + +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + ledcWrite(KB_BL_PIN, brightness); +#else + ledcWrite(LEDC_BACKLIGHT_CHANNEL, brightness); +#endif +} + +void TLoraPagerKeyboard::updateModifierFlag(uint8_t key) +{ + if (key == modifierRightShiftKey) { + modifierFlag ^= modifierRightShift; + } else if (key == modifierSymKey) { + modifierFlag ^= modifierSym; + } +} + +bool TLoraPagerKeyboard::isModifierKey(uint8_t key) +{ + return (key == modifierRightShiftKey || key == modifierSymKey); +} + +#endif \ No newline at end of file diff --git a/src/input/TLoraPagerKeyboard.h b/src/input/TLoraPagerKeyboard.h index d31b05978..4dabbac64 100644 --- a/src/input/TLoraPagerKeyboard.h +++ b/src/input/TLoraPagerKeyboard.h @@ -4,9 +4,26 @@ class TLoraPagerKeyboard : public TCA8418KeyboardBase { public: TLoraPagerKeyboard(); - void setBacklight(bool on) override{}; + void reset(void); + void trigger(void) override; + void setBacklight(bool on) override; + virtual ~TLoraPagerKeyboard() {} protected: - void pressed(uint8_t key) override{}; - void released(void) override{}; + void pressed(uint8_t key) override; + void released(void) override; + void hapticFeedback(void); + + void updateModifierFlag(uint8_t key); + bool isModifierKey(uint8_t key); + void toggleBacklight(bool off = false); + + private: + uint8_t modifierFlag; // Flag to indicate if a modifier key is pressed + uint32_t last_modifier_time; // Timestamp of the last modifier key press + int8_t last_key; + int8_t next_key; + uint32_t last_tap; + uint8_t char_idx; + int32_t tap_interval; }; diff --git a/src/input/cardKbI2cImpl.cpp b/src/input/cardKbI2cImpl.cpp index fcbdd0a3f..9b0926a1d 100644 --- a/src/input/cardKbI2cImpl.cpp +++ b/src/input/cardKbI2cImpl.cpp @@ -12,8 +12,8 @@ void CardKbI2cImpl::init() #if !MESHTASTIC_EXCLUDE_I2C && !defined(ARCH_PORTDUINO) && !defined(I2C_NO_RESCAN) if (cardkb_found.address == 0x00) { LOG_DEBUG("Rescan for I2C keyboard"); - uint8_t i2caddr_scan[] = {CARDKB_ADDR, TDECK_KB_ADDR, BBQ10_KB_ADDR, MPR121_KB_ADDR, XPOWERS_AXP192_AXP2101_ADDRESS}; - uint8_t i2caddr_asize = 5; + uint8_t i2caddr_scan[] = {CARDKB_ADDR, TDECK_KB_ADDR, BBQ10_KB_ADDR, MPR121_KB_ADDR, TCA8418_KB_ADDR}; + uint8_t i2caddr_asize = sizeof(i2caddr_scan) / sizeof(i2caddr_scan[0]); auto i2cScanner = std::unique_ptr(new ScanI2CTwoWire()); #if WIRE_INTERFACES_COUNT == 2 diff --git a/src/main.cpp b/src/main.cpp index 0260cbc07..338487914 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -135,8 +135,9 @@ AccelerometerThread *accelerometerThread = nullptr; AudioThread *audioThread = nullptr; #endif -#ifdef USE_PCA9557 -PCA9557 IOEXP; +#ifdef USE_XL9555 +#include "ExtensionIOXL9555.hpp" +ExtensionIOXL9555 io; #endif #if HAS_TFT @@ -201,7 +202,7 @@ ScanI2C::FoundDevice rgb_found = ScanI2C::FoundDevice(ScanI2C::DeviceType::NONE, /// The I2C address of our Air Quality Indicator (if found) ScanI2C::DeviceAddress aqi_found = ScanI2C::ADDRESS_NONE; -#ifdef T_WATCH_S3 +#if defined(T_WATCH_S3) || defined(T_LORA_PAGER) Adafruit_DRV2605 drv; #endif @@ -359,6 +360,30 @@ void setup() digitalWrite(SDCARD_CS, HIGH); pinMode(PIN_EINK_CS, OUTPUT); digitalWrite(PIN_EINK_CS, HIGH); +#elif defined(T_LORA_PAGER) + pinMode(LORA_CS, OUTPUT); + digitalWrite(LORA_CS, HIGH); + pinMode(SDCARD_CS, OUTPUT); + digitalWrite(SDCARD_CS, HIGH); + pinMode(TFT_CS, OUTPUT); + digitalWrite(TFT_CS, HIGH); + // io expander + io.begin(Wire, XL9555_SLAVE_ADDRESS0, SDA, SCL); + io.pinMode(EXPANDS_DRV_EN, OUTPUT); + io.digitalWrite(EXPANDS_DRV_EN, HIGH); + io.pinMode(EXPANDS_AMP_EN, OUTPUT); + io.digitalWrite(EXPANDS_AMP_EN, HIGH); + io.pinMode(EXPANDS_LORA_EN, OUTPUT); + io.digitalWrite(EXPANDS_LORA_EN, HIGH); + io.pinMode(EXPANDS_GPS_EN, OUTPUT); + io.digitalWrite(EXPANDS_GPS_EN, HIGH); + io.pinMode(EXPANDS_KB_EN, OUTPUT); + io.digitalWrite(EXPANDS_KB_EN, HIGH); + io.pinMode(EXPANDS_SD_EN, OUTPUT); + io.digitalWrite(EXPANDS_SD_EN, HIGH); + io.pinMode(EXPANDS_GPIO_EN, OUTPUT); + io.digitalWrite(EXPANDS_GPIO_EN, HIGH); + io.pinMode(EXPANDS_SD_PULLEN, INPUT); #endif concurrency::hasBeenSetup = true; @@ -805,7 +830,7 @@ void setup() #endif #endif -#ifdef T_WATCH_S3 +#if defined(T_WATCH_S3) || defined(T_LORA_PAGER) drv.begin(); drv.selectLibrary(1); // I2C trigger by sending 'go' command @@ -851,7 +876,7 @@ void setup() if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { #if defined(ST7701_CS) || defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \ - defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) + defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) screen = new graphics::Screen(screen_found, screen_model, screen_geometry); #elif defined(ARCH_PORTDUINO) if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || settingsMap[displayPanel]) && @@ -1114,7 +1139,7 @@ void setup() // Don't call screen setup until after nodedb is setup (because we need // the current region name) #if defined(ST7701_CS) || defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \ - defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) + defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) if (screen) screen->setup(); #elif defined(ARCH_PORTDUINO) diff --git a/src/main.h b/src/main.h index 3568daad2..ef1f241ef 100644 --- a/src/main.h +++ b/src/main.h @@ -41,7 +41,7 @@ extern bool eink_found; extern bool pmu_found; extern bool isUSBPowered; -#ifdef T_WATCH_S3 +#if defined(T_WATCH_S3) || defined(T_LORA_PAGER) #include extern Adafruit_DRV2605 drv; #endif diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 18014eb02..d544d0174 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -663,7 +663,7 @@ void NodeDB::installDefaultConfig(bool preserveKey = false) config.bluetooth.fixed_pin = defaultBLEPin; #if defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \ - defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) + defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) bool hasScreen = true; #ifdef HELTEC_MESH_NODE_T114 uint32_t st7789_id = get_st7789_id(ST7789_NSS, ST7789_SCK, ST7789_SDA, ST7789_RS, ST7789_RESET); @@ -830,6 +830,15 @@ void NodeDB::installDefaultModuleConfig() moduleConfig.external_notification.alert_message = true; moduleConfig.external_notification.output_ms = 1000; moduleConfig.external_notification.nag_timeout = 60; +#endif +#ifdef T_LORA_PAGER + moduleConfig.canned_message.updown1_enabled = true; + moduleConfig.canned_message.inputbroker_pin_a = ROTARY_A; + moduleConfig.canned_message.inputbroker_pin_b = ROTARY_B; + moduleConfig.canned_message.inputbroker_pin_press = ROTARY_PRESS; + moduleConfig.canned_message.inputbroker_event_cw = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar(28); + moduleConfig.canned_message.inputbroker_event_ccw = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar(29); + moduleConfig.canned_message.inputbroker_event_press = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT; #endif moduleConfig.has_canned_message = true; #if USERPREFS_MQTT_ENABLED && !MESHTASTIC_EXCLUDE_MQTT diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index 3528f57f5..0d405fa81 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -3,6 +3,7 @@ #include "buzz/BuzzerFeedbackThread.h" #include "input/ExpressLRSFiveWay.h" #include "input/InputBroker.h" +#include "input/RotaryEncoderImpl.h" #include "input/RotaryEncoderInterruptImpl1.h" #include "input/SerialKeyboardImpl.h" #include "input/TrackballInterruptImpl1.h" @@ -170,11 +171,20 @@ void setupModules() delete rotaryEncoderInterruptImpl1; rotaryEncoderInterruptImpl1 = nullptr; } +#ifdef T_LORA_PAGER + // use a special FSM based rotary encoder version for T-LoRa Pager + rotaryEncoderImpl = new RotaryEncoderImpl(); + if (!rotaryEncoderImpl->init()) { + delete rotaryEncoderImpl; + rotaryEncoderImpl = nullptr; + } +#else upDownInterruptImpl1 = new UpDownInterruptImpl1(); if (!upDownInterruptImpl1->init()) { delete upDownInterruptImpl1; upDownInterruptImpl1 = nullptr; } +#endif cardKbI2cImpl = new CardKbI2cImpl(); cardKbI2cImpl->init(); #ifdef INPUTBROKER_MATRIX_TYPE diff --git a/src/platform/esp32/architecture.h b/src/platform/esp32/architecture.h index 522e862ac..cb0f0dab3 100644 --- a/src/platform/esp32/architecture.h +++ b/src/platform/esp32/architecture.h @@ -192,6 +192,8 @@ #define HW_VENDOR meshtastic_HardwareModel_LINK_32 #elif defined(T_DECK_PRO) #define HW_VENDOR meshtastic_HardwareModel_T_DECK_PRO +#elif defined(T_LORA_PAGER) +#define HW_VENDOR meshtastic_HardwareModel_T_LORA_PAGER #endif // ----------------------------------------------------------------------------- diff --git a/src/platform/extra_variants/t_lora_pager/variant.cpp b/src/platform/extra_variants/t_lora_pager/variant.cpp new file mode 100644 index 000000000..ea5773d30 --- /dev/null +++ b/src/platform/extra_variants/t_lora_pager/variant.cpp @@ -0,0 +1,27 @@ +#include "configuration.h" + +#ifdef T_LORA_PAGER + +#include "AudioBoard.h" + +DriverPins PinsAudioBoardES8311; +AudioBoard board(AudioDriverES8311, PinsAudioBoardES8311); + +// TLora Pager specific init +void lateInitVariant() +{ + // AudioDriverLogger.begin(Serial, AudioDriverLogLevel::Debug); + // I2C: function, scl, sda + PinsAudioBoardES8311.addI2C(PinFunction::CODEC, Wire); + // I2S: function, mclk, bck, ws, data_out, data_in + PinsAudioBoardES8311.addI2S(PinFunction::CODEC, DAC_I2S_MCLK, DAC_I2S_BCK, DAC_I2S_WS, DAC_I2S_DOUT, DAC_I2S_DIN); + + // configure codec + CodecConfig cfg; + cfg.input_device = ADC_INPUT_LINE1; + cfg.output_device = DAC_OUTPUT_ALL; + cfg.i2s.bits = BIT_LENGTH_16BITS; + cfg.i2s.rate = RATE_44K; + board.begin(cfg); +} +#endif \ No newline at end of file diff --git a/variants/esp32s3/tlora-pager/pins_arduino.h b/variants/esp32s3/tlora-pager/pins_arduino.h new file mode 100644 index 000000000..a6321f510 --- /dev/null +++ b/variants/esp32s3/tlora-pager/pins_arduino.h @@ -0,0 +1,19 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +// used for keyboard, battery gauge, charger and haptic driver +static const uint8_t SDA = 3; +static const uint8_t SCL = 2; + +// Default SPI will be mapped to Radio +static const uint8_t SS = 36; +static const uint8_t MOSI = 34; +static const uint8_t MISO = 33; +static const uint8_t SCK = 35; + +#endif /* Pins_Arduino_h */ diff --git a/variants/esp32s3/tlora-pager/platformio.ini b/variants/esp32s3/tlora-pager/platformio.ini new file mode 100644 index 000000000..3d77d879c --- /dev/null +++ b/variants/esp32s3/tlora-pager/platformio.ini @@ -0,0 +1,70 @@ +; LilyGo T-Lora-Pager +[env:tlora-pager] +extends = esp32s3_base +board = t-deck-pro ; same as T-Deck Pro +board_check = true +board_build.partitions = default_16MB.csv +upload_protocol = esptool + +build_flags = ${esp32s3_base.build_flags} + -I variants/esp32s3/tlora-pager + -D T_LORA_PAGER + -D BOARD_HAS_PSRAM + -D GPS_POWER_TOGGLE + -D HAS_SDCARD + -D SDCARD_USE_SPI1 + -D ENABLE_ROTARY_PULLUP + -D ENABLE_BUTTON_PULLUP + -D HALF_STEP + +lib_deps = ${esp32s3_base.lib_deps} + lovyan03/LovyanGFX@1.2.7 + earlephilhower/ESP8266Audio@1.9.9 + earlephilhower/ESP8266SAM@1.0.1 + adafruit/Adafruit DRV2605 Library@1.2.4 + lewisxhe/PCF8563_Library@1.0.1 + lewisxhe/SensorLib@0.3.1 + https://github.com/pschatzmann/arduino-audio-driver/archive/refs/tags/v0.1.3.zip + https://github.com/mverch67/BQ27220/archive/07d92be846abd8a0258a50c23198dac0858b22ed.zip + https://github.com/mverch67/RotaryEncoder + +[env:tlora-pager-tft] +extends = env:tlora-pager +build_flags = + ${env:tlora-pager.build_flags} + -D CONFIG_DISABLE_HAL_LOCKS=1 + -D INPUTDRIVER_ROTARY_TYPE=1 + -D INPUTDRIVER_ROTARY_UP=40 + -D INPUTDRIVER_ROTARY_DOWN=41 + -D INPUTDRIVER_ROTARY_BTN=7 + -D INPUTDRIVER_BUTTON_TYPE=0 + -D HAS_SCREEN=1 + -D HAS_TFT=1 + -D USE_I2S_BUZZER + -D RAM_SIZE=5120 + -D LV_LVGL_H_INCLUDE_SIMPLE + -D LV_CONF_INCLUDE_SIMPLE + -D LV_COMP_CONF_INCLUDE_SIMPLE + -D LV_USE_SYSMON=0 + -D LV_USE_PROFILER=0 + -D LV_USE_PERF_MONITOR=0 + -D LV_USE_MEM_MONITOR=0 + -D LV_USE_LOG=0 + -D USE_LOG_DEBUG + -D LOG_DEBUG_INC=\"DebugConfiguration.h\" + -D RADIOLIB_SPI_PARANOID=0 + -D LGFX_SCREEN_WIDTH=222 + -D LGFX_SCREEN_HEIGHT=480 + -D DISPLAY_SIZE=480x222 ; landscape mode + -D DISPLAY_SET_RESOLUTION + -D LGFX_DRIVER=LGFX_TLORA_PAGER + -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_T_LORA_PAGER.h\" +; -D LVGL_DRIVER=LVGL_T_LORA_PAGER +; -D LV_USE_ST7796=1 + -D VIEW_480x222 + -D USE_PACKET_API + -D MAP_FULL_REDRAW + +lib_deps = + ${env:tlora-pager.lib_deps} + ${device-ui_base.lib_deps} diff --git a/variants/esp32s3/tlora-pager/variant.h b/variants/esp32s3/tlora-pager/variant.h new file mode 100644 index 000000000..ee48088c8 --- /dev/null +++ b/variants/esp32s3/tlora-pager/variant.h @@ -0,0 +1,125 @@ +// ST7796 TFT LCD +#define TFT_CS 38 +#define ST7796_CS TFT_CS +#define ST7796_RS 37 // DC +#define ST7796_SDA MOSI // MOSI +#define ST7796_SCK SCK +#define ST7796_RESET -1 +#define ST7796_MISO MISO +#define ST7796_BUSY -1 +#define ST7796_BL 42 +#define ST7796_SPI_HOST SPI2_HOST +#define TFT_BL 42 +#define SPI_FREQUENCY 75000000 +#define SPI_READ_FREQUENCY 16000000 +#define TFT_HEIGHT 480 +#define TFT_WIDTH 222 +#define TFT_OFFSET_X 49 +#define TFT_OFFSET_Y 0 +#define TFT_OFFSET_ROTATION 3 +#define SCREEN_ROTATE +#define SCREEN_TRANSITION_FRAMERATE 5 +#define BRIGHTNESS_DEFAULT 130 // Medium Low Brightness + +#define I2C_SDA SDA +#define I2C_SCL SCL + +#define USE_POWERSAVE +#define SLEEP_TIME 120 + +// GNNS +#define HAS_GPS 1 +#define GPS_BAUDRATE 38400 +#define GPS_RX_PIN 4 +#define GPS_TX_PIN 12 +#define PIN_GPS_PPS 13 + +// PCF8563 RTC Module +#if __has_include("pcf8563.h") +#include "pcf8563.h" +#endif +#define PCF8563_RTC 0x51 +#define HAS_RTC 1 + +// Rotary +#define ROTARY_A (40) +#define ROTARY_B (41) +#define ROTARY_PRESS (7) + +#define BUTTON_PIN 0 + +// SPI interface SD card slot +#define SPI_MOSI MOSI +#define SPI_SCK SCK +#define SPI_MISO MISO +#define SPI_CS 21 +#define SDCARD_CS SPI_CS +#define SD_SPI_FREQUENCY 75000000U + +// TCA8418 keyboard +#define I2C_NO_RESCAN +#define KB_BL_PIN 46 +#define KB_INT 6 +#define CANNED_MESSAGE_MODULE_ENABLE 1 + +// audio codec ES8311 +#define HAS_I2S +#define DAC_I2S_BCK 11 +#define DAC_I2S_WS 18 +#define DAC_I2S_DOUT 45 +#define DAC_I2S_DIN 17 +#define DAC_I2S_MCLK 10 + +// gyroscope BHI260AP +#define HAS_BHI260AP + +// battery charger BQ25896 +#define HAS_PPM 1 +#define XPOWERS_CHIP_BQ25896 + +// battery quality management BQ27220 +#define HAS_BQ27220 1 +#define BQ27220_I2C_SDA SDA +#define BQ27220_I2C_SCL SCL +#define BQ27220_DESIGN_CAPACITY 1500 + +// NFC ST25R3916 +#define NFC_INT 5 +#define NFC_CS 39 + +// External expansion chip XL9555 +#define USE_XL9555 +#define EXPANDS_DRV_EN (0) +#define EXPANDS_AMP_EN (1) +#define EXPANDS_KB_RST (2) +#define EXPANDS_LORA_EN (3) +#define EXPANDS_GPS_EN (4) +#define EXPANDS_NFC_EN (5) +#define EXPANDS_GPS_RST (7) +#define EXPANDS_KB_EN (8) +#define EXPANDS_GPIO_EN (9) +#define EXPANDS_SD_DET (10) +#define EXPANDS_SD_PULLEN (11) +#define EXPANDS_SD_EN (12) + +// LoRa +#define USE_SX1262 +#define USE_SX1268 + +#define LORA_SCK 35 +#define LORA_MISO 33 +#define LORA_MOSI 34 +#define LORA_CS 36 + +#define LORA_DIO0 -1 // a No connect on the SX1262 module +#define LORA_RESET 47 +#define LORA_DIO1 14 // SX1262 IRQ +#define LORA_DIO2 48 // SX1262 BUSY +#define LORA_DIO3 // Not connected on PCB, but internally on the TTGO SX1262, if DIO3 is high the TXCO is enabled + +#define SX126X_CS LORA_CS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 3.0 From 596cd7e0b6c4225ea21509601ede589bce205937 Mon Sep 17 00:00:00 2001 From: Manuel <71137295+mverch67@users.noreply.github.com> Date: Tue, 26 Aug 2025 20:39:43 +0200 Subject: [PATCH 19/54] enable device telemetry (#7757) --- variants/esp32s3/elecrow_panel/platformio.ini | 2 -- 1 file changed, 2 deletions(-) diff --git a/variants/esp32s3/elecrow_panel/platformio.ini b/variants/esp32s3/elecrow_panel/platformio.ini index 59bc26000..065f22538 100644 --- a/variants/esp32s3/elecrow_panel/platformio.ini +++ b/variants/esp32s3/elecrow_panel/platformio.ini @@ -19,8 +19,6 @@ build_flags = ${esp32s3_base.build_flags} -Os -D MESHTASTIC_EXCLUDE_SERIAL=1 -D MESHTASTIC_EXCLUDE_SOCKETAPI=1 -D MESHTASTIC_EXCLUDE_SCREEN=1 - -D MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 - -D HAS_TELEMETRY=0 -D CONFIG_DISABLE_HAL_LOCKS=1 -D USE_PIN_BUZZER -D HAS_SCREEN=0 From 2c071a32836ff2837f36c2b6e62d1028b4c8d7c6 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Tue, 26 Aug 2025 13:41:33 -0500 Subject: [PATCH 20/54] Don't use pin 0 on RAK for input (#7755) * Don't use pin 0 on RAK for input * Use boolean instead of define --------- Co-authored-by: Ben Meadors --- src/input/RotaryEncoderInterruptBase.cpp | 23 ++++++++++++++++------- src/input/UpDownInterruptBase.cpp | 23 ++++++++++++++++------- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/input/RotaryEncoderInterruptBase.cpp b/src/input/RotaryEncoderInterruptBase.cpp index 0557bc180..88b07a389 100644 --- a/src/input/RotaryEncoderInterruptBase.cpp +++ b/src/input/RotaryEncoderInterruptBase.cpp @@ -18,14 +18,23 @@ void RotaryEncoderInterruptBase::init( this->_eventCcw = eventCcw; this->_eventPressed = eventPressed; - pinMode(pinPress, INPUT_PULLUP); - pinMode(this->_pinA, INPUT_PULLUP); - pinMode(this->_pinB, INPUT_PULLUP); + bool isRAK = false; +#ifdef RAK_4631 + isRAK = true; +#endif - // attachInterrupt(pinPress, onIntPress, RISING); - attachInterrupt(pinPress, onIntPress, RISING); - attachInterrupt(this->_pinA, onIntA, CHANGE); - attachInterrupt(this->_pinB, onIntB, CHANGE); + if (!isRAK || pinPress != 0) { + pinMode(pinPress, INPUT_PULLUP); + attachInterrupt(pinPress, onIntPress, RISING); + } + if (!isRAK || this->_pinA != 0) { + pinMode(this->_pinA, INPUT_PULLUP); + attachInterrupt(this->_pinA, onIntA, CHANGE); + } + if (!isRAK || this->_pinA != 0) { + pinMode(this->_pinB, INPUT_PULLUP); + attachInterrupt(this->_pinB, onIntB, CHANGE); + } this->rotaryLevelA = digitalRead(this->_pinA); this->rotaryLevelB = digitalRead(this->_pinB); diff --git a/src/input/UpDownInterruptBase.cpp b/src/input/UpDownInterruptBase.cpp index c66eb13d0..26b281aaf 100644 --- a/src/input/UpDownInterruptBase.cpp +++ b/src/input/UpDownInterruptBase.cpp @@ -15,14 +15,23 @@ void UpDownInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, this->_eventDown = eventDown; this->_eventUp = eventUp; this->_eventPressed = eventPressed; + bool isRAK = false; +#ifdef RAK_4631 + isRAK = true; +#endif - pinMode(pinPress, INPUT_PULLUP); - pinMode(this->_pinDown, INPUT_PULLUP); - pinMode(this->_pinUp, INPUT_PULLUP); - - attachInterrupt(pinPress, onIntPress, RISING); - attachInterrupt(this->_pinDown, onIntDown, RISING); - attachInterrupt(this->_pinUp, onIntUp, RISING); + if (!isRAK || pinPress != 0) { + pinMode(pinPress, INPUT_PULLUP); + attachInterrupt(pinPress, onIntPress, RISING); + } + if (!isRAK || this->_pinDown != 0) { + pinMode(this->_pinDown, INPUT_PULLUP); + attachInterrupt(this->_pinDown, onIntDown, RISING); + } + if (!isRAK || this->_pinUp != 0) { + pinMode(this->_pinUp, INPUT_PULLUP); + attachInterrupt(this->_pinUp, onIntUp, RISING); + } LOG_DEBUG("Up/down/press GPIO initialized (%d, %d, %d)", this->_pinUp, this->_pinDown, pinPress); From 3dd384dd53e56cd3ecf0b6721790ac968ad926f8 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 26 Aug 2025 19:45:26 -0500 Subject: [PATCH 21/54] Null check --- src/modules/AdminModule.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 9e8ce2e6b..407003f7e 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -505,7 +505,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta if (mp.decoded.want_response && !myReply) { myReply = allocErrorResponse(meshtastic_Routing_Error_NONE, &mp); } - if (mp.pki_encrypted) { + if (mp.pki_encrypted && myReply) { myReply->pki_encrypted = true; } return handled; From f8ba392a2413fdf749d7e2a649dfd8246f737baf Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 26 Aug 2025 20:29:11 -0500 Subject: [PATCH 22/54] Add BaseUI support for L1 EInk (#7751) * Add BaseUI support for L1 EInk * Fix Eink offset * Add joystick * Updates * Adjust Seeed Wio Tracker L1 E-Ink variant (#7326) * Rename variant Needs the -inkhud suffix to work correctly with the web flasher * Display driver for ZJY122250_0213BAAMFGN * Remove dead code from nicheGraphics.h Remnants of T-Echo's nicheGraphics.h file, which was used as a template. * Use ZJY122250_0213BAAMFGN driver Improves display health. We don't need as many full refreshes now. * Tidying * board_check = true --------- Co-authored-by: Ben Meadors * Consolidation * Add hack for existing InkHUD button functionality --------- Co-authored-by: todd-herbert --- src/configuration.h | 2 +- src/graphics/EInkDisplay2.cpp | 19 ++++-- src/graphics/EInkDisplay2.h | 2 +- .../Drivers/EInk/ZJY122250_0213BAAMFGN.cpp | 68 +++++++++++++++++++ .../Drivers/EInk/ZJY122250_0213BAAMFGN.h | 42 ++++++++++++ src/graphics/niche/InkHUD/DisplayHealth.cpp | 5 -- .../seeed_wio_tracker_L1_eink/nicheGraphics.h | 30 ++------ .../seeed_wio_tracker_L1_eink/platformio.ini | 38 +++++++++-- .../seeed_wio_tracker_L1_eink/variant.h | 25 ++++--- 9 files changed, 181 insertions(+), 50 deletions(-) create mode 100644 src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.cpp create mode 100644 src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.h diff --git a/src/configuration.h b/src/configuration.h index 0e24990b5..8b4fd82c7 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -135,7 +135,7 @@ along with this program. If not, see . // ----------------------------------------------------------------------------- // OLED & Input // ----------------------------------------------------------------------------- -#if defined(SEEED_WIO_TRACKER_L1) +#if defined(SEEED_WIO_TRACKER_L1) && !defined(SEEED_WIO_TRACKER_L1_EINK) #define SSD1306_ADDRESS 0x3D #define USE_SH1106 #else diff --git a/src/graphics/EInkDisplay2.cpp b/src/graphics/EInkDisplay2.cpp index 1c9f290b6..c0c09cc27 100644 --- a/src/graphics/EInkDisplay2.cpp +++ b/src/graphics/EInkDisplay2.cpp @@ -67,20 +67,28 @@ bool EInkDisplay::forceDisplay(uint32_t msecLimit) // FIXME - only draw bits have changed (use backbuf similar to the other displays) const bool flipped = config.display.flip_screen; + // HACK for L1 EInk +#if defined(SEEED_WIO_TRACKER_L1_EINK) + // For SEEED_WIO_TRACKER_L1_EINK, setRotation(3) is correct but mirrored; flip both axes + for (uint32_t y = 0; y < displayHeight; y++) { + for (uint32_t x = 0; x < displayWidth; x++) { + auto b = buffer[x + (y / 8) * displayWidth]; + auto isset = b & (1 << (y & 7)); + adafruitDisplay->drawPixel((displayWidth - 1) - x, (displayHeight - 1) - y, isset ? GxEPD_BLACK : GxEPD_WHITE); + } + } +#else for (uint32_t y = 0; y < displayHeight; y++) { for (uint32_t x = 0; x < displayWidth; x++) { - // get src pixel in the page based ordering the OLED lib uses FIXME, super inefficient auto b = buffer[x + (y / 8) * displayWidth]; auto isset = b & (1 << (y & 7)); - - // Handle flip here, rather than with setRotation(), - // Avoids issues when display width is not a multiple of 8 if (flipped) adafruitDisplay->drawPixel((displayWidth - 1) - x, (displayHeight - 1) - y, isset ? GxEPD_BLACK : GxEPD_WHITE); else adafruitDisplay->drawPixel(x, y, isset ? GxEPD_BLACK : GxEPD_WHITE); } } +#endif // Trigger the refresh in GxEPD2 LOG_DEBUG("Update E-Paper"); @@ -235,7 +243,7 @@ bool EInkDisplay::connect() adafruitDisplay->setRotation(1); adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT); } -#elif defined(HELTEC_MESH_POCKET) +#elif defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) { spi1 = &SPI1; spi1->begin(); @@ -249,6 +257,7 @@ bool EInkDisplay::connect() // Init GxEPD2 adafruitDisplay->init(); adafruitDisplay->setRotation(3); + adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT); } #elif defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_VISION_MASTER_E213) diff --git a/src/graphics/EInkDisplay2.h b/src/graphics/EInkDisplay2.h index b840ce9ba..b4cee81fe 100644 --- a/src/graphics/EInkDisplay2.h +++ b/src/graphics/EInkDisplay2.h @@ -84,7 +84,7 @@ class EInkDisplay : public OLEDDisplay SPIClass *hspi = NULL; #endif -#if defined(HELTEC_MESH_POCKET) +#if defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) SPIClass *spi1 = NULL; #endif diff --git a/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.cpp b/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.cpp new file mode 100644 index 000000000..e83588905 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.cpp @@ -0,0 +1,68 @@ +#include "./ZJY122250_0213BAAMFGN.h" + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +using namespace NicheGraphics::Drivers; + +// Map the display controller IC's output to the connected panel +void ZJY122250_0213BAAMFGN::configScanning() +{ + // "Driver output control" + // Scan gates from 0 to 249 (vertical resolution 250px) + sendCommand(0x01); + sendData(0xF9); + sendData(0x00); + sendData(0x00); +} + +// Specify which information is used to control the sequence of voltages applied to move the pixels +// - For this display, configUpdateSequence() specifies that a suitable LUT will be loaded from +// the controller IC's OTP memory, when the update procedure begins. +void ZJY122250_0213BAAMFGN::configWaveform() +{ + switch (updateType) { + case FAST: + sendCommand(0x3C); // Border waveform: + sendData(0x80); // VCOM + break; + case FULL: + default: + sendCommand(0x3C); // Border waveform: + sendData(0x01); // Follow LUT 1 (blink same as white pixels) + break; + } + + sendCommand(0x18); // Temperature sensor: + sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform +} + +void ZJY122250_0213BAAMFGN::configUpdateSequence() +{ + switch (updateType) { + case FAST: + sendCommand(0x22); // Set "update sequence" + sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh" + break; + + case FULL: + default: + sendCommand(0x22); // Set "update sequence" + sendData(0xF7); // Will load LUT from OTP memory + break; + } +} + +// Once the refresh operation has been started, +// begin periodically polling the display to check for completion, using the normal Meshtastic threading code +// Only used when refresh is "async" +void ZJY122250_0213BAAMFGN::detachFromUpdate() +{ + switch (updateType) { + case FAST: + return beginPolling(50, 500); // At least 500ms for fast refresh + case FULL: + default: + return beginPolling(100, 2000); // At least 2 seconds for full refresh + } +} +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.h b/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.h new file mode 100644 index 000000000..82c4ec107 --- /dev/null +++ b/src/graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.h @@ -0,0 +1,42 @@ +/* + +E-Ink display driver + - ZJY122250_0213BAAMFGN + - Manufacturer: Zhongjingyuan + - Size: 2.13 inch + - Resolution: 250px x 122px + - Flex connector marking (not a unique identifier): FPC-A002 + +*/ + +#pragma once + +#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS + +#include "configuration.h" + +#include "./SSD16XX.h" + +namespace NicheGraphics::Drivers +{ +class ZJY122250_0213BAAMFGN : public SSD16XX +{ + // Display properties + private: + static constexpr uint32_t width = 122; + static constexpr uint32_t height = 250; + static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST); + + public: + ZJY122250_0213BAAMFGN() : SSD16XX(width, height, supported) {} + + protected: + virtual void configScanning() override; + virtual void configWaveform() override; + virtual void configUpdateSequence() override; + void detachFromUpdate() override; +}; + +} // namespace NicheGraphics::Drivers + +#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/DisplayHealth.cpp b/src/graphics/niche/InkHUD/DisplayHealth.cpp index 7e1accafd..e8849b72e 100644 --- a/src/graphics/niche/InkHUD/DisplayHealth.cpp +++ b/src/graphics/niche/InkHUD/DisplayHealth.cpp @@ -7,12 +7,7 @@ using namespace NicheGraphics; // Timing for "maintenance" // Paying off full-refresh debt with unprovoked updates, if the display is not very active - -#ifdef SEEED_WIO_TRACKER_L1 -static constexpr uint32_t MAINTENANCE_MS_INITIAL = 5 * 1000UL; -#else static constexpr uint32_t MAINTENANCE_MS_INITIAL = 60 * 1000UL; -#endif static constexpr uint32_t MAINTENANCE_MS = 60 * 60 * 1000UL; InkHUD::DisplayHealth::DisplayHealth() : concurrency::OSThread("Mediator") diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h b/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h index a32753343..7fb890303 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h +++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/nicheGraphics.h @@ -18,16 +18,9 @@ // Shared NicheGraphics components // -------------------------------- -#include "graphics/niche/Drivers/Backlight/LatchingBacklight.h" -#include "graphics/niche/Drivers/EInk/GDEY0213B74.h" +#include "graphics/niche/Drivers/EInk/ZJY122250_0213BAAMFGN.h" #include "graphics/niche/Inputs/TwoButton.h" -// Special case - fix T-Echo's touch button -// ---------------------------------------- -// On a handful of T-Echos, LoRa TX triggers the capacitive touch -// To avoid this, we lockout the button during TX -#include "mesh/RadioLibInterface.h" - void setupNicheGraphics() { using namespace NicheGraphics; @@ -41,7 +34,7 @@ void setupNicheGraphics() // E-Ink Driver // ----------------------------- - Drivers::EInk *driver = new Drivers::GDEY0213B74; + Drivers::EInk *driver = new Drivers::ZJY122250_0213BAAMFGN; driver->begin(&SPI1, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES); // InkHUD @@ -53,8 +46,7 @@ void setupNicheGraphics() inkhud->setDriver(driver); // Set how many FAST updates per FULL update - // Set how unhealthy additional FAST updates beyond this number are - inkhud->setDisplayResilience(7, 1.5); + inkhud->setDisplayResilience(15); // Select fonts InkHUD::Applet::fontLarge = FREESANS_12PT_WIN1252; @@ -62,16 +54,10 @@ void setupNicheGraphics() InkHUD::Applet::fontSmall = FREESANS_6PT_WIN1252; // Customize default settings - inkhud->persistence->settings.userTiles.maxCount = 2; // Two applets side-by-side - // 270 degrees clockwise + inkhud->persistence->settings.rotation = 1; // 90 degrees clockwise inkhud->persistence->settings.optionalFeatures.batteryIcon = true; // Device definitely has a battery - inkhud->persistence->settings.optionalMenuItems.backlight = true; // Until proves capacitive button works by touching it - inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users - - // Setup backlight controller - // Note: AUX button attached further down - Drivers::LatchingBacklight *backlight = Drivers::LatchingBacklight::getInstance(); - backlight->setPin(PIN_EINK_EN); + inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users + inkhud->persistence->settings.userTiles.maxCount = 2; // Two applets side-by-side // Pick applets // Note: order of applets determines priority of "auto-show" feature @@ -83,11 +69,9 @@ void setupNicheGraphics() inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // - inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, no autoshow, default on tile 0 - inkhud->persistence->settings.rotation = 1; - // inkhud->persistence->printSettings(&inkhud->persistence->settings); // Start running InkHUD inkhud->begin(); - // inkhud->persistence->printSettings(&inkhud->persistence->settings); + // Buttons // -------------------------- diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini b/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini index 52ff39d49..7f9eb0e2c 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini +++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/platformio.ini @@ -1,17 +1,47 @@ [env:seeed_wio_tracker_L1_eink] board = seeed_wio_tracker_L1 -extends = nrf52840_base, inkhud +extends = nrf52840_base ;board_level = extra build_flags = ${nrf52840_base.build_flags} - ${inkhud.build_flags} -I variants/nrf52840/seeed_wio_tracker_L1_eink -D SEEED_WIO_TRACKER_L1_EINK -D SEEED_WIO_TRACKER_L1 -I src/platform/nrf52/softdevice -I src/platform/nrf52/softdevice/nrf52 + -DUSE_EINK + -DEINK_DISPLAY_MODEL=GxEPD2_213_B74 + -DEINK_WIDTH=250 + -DEINK_HEIGHT=122 + -DUSE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk + -DEINK_LIMIT_FASTREFRESH=10 ; How many consecutive fast-refreshes are permitted + -DEINK_LIMIT_RATE_BACKGROUND_SEC=30 ; Minimum interval between BACKGROUND updates + -DEINK_LIMIT_RATE_RESPONSIVE_SEC=1 ; Minimum interval between RESPONSIVE updates +; -D EINK_LIMIT_GHOSTING_PX=2000 ; (Optional) How much image ghosting is tolerated + -DEINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached. + -DEINK_HASQUIRK_GHOSTING ; Display model is identified as "prone to ghosting" + -DEINK_HASQUIRK_WEAKFASTREFRESH ; Pixels set with fast-refresh are easy to clear, disrupted by sunlight board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld -build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_wio_tracker_L1_eink> ${inkhud.build_src_filter} +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/seeed_wio_tracker_L1_eink> lib_deps = - ${inkhud.lib_deps} ${nrf52840_base.lib_deps} + https://github.com/meshtastic/GxEPD2#b202ebfec6a4821e098cf7a625ba0f6f2400292d debug_tool = jlink + +[env:seeed_wio_tracker_L1_eink-inkhud] +board = seeed_wio_tracker_L1 +extends = nrf52840_base, inkhud +build_flags = + ${nrf52840_base.build_flags} + ${inkhud.build_flags} + -I variants/nrf52840/seeed_wio_tracker_L1_eink + -D SEEED_WIO_TRACKER_L1 + -D BUTTON_PIN=D13 +board_build.ldscript = src/platform/nrf52/nrf52840_s140_v7.ld +build_src_filter = + ${nrf52_base.build_src_filter} + ${inkhud.build_src_filter} + +<../variants/nrf52840/seeed_wio_tracker_L1_eink> +lib_deps = + ${inkhud.lib_deps} ; Before base libs_deps, so we use ZinggJM/GFXRoot instead of AdafruitGFX (saves space) + ${nrf52840_base.lib_deps} +debug_tool = jlink \ No newline at end of file diff --git a/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h b/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h index 98a7b2c39..f33d200b1 100644 --- a/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h +++ b/variants/nrf52840/seeed_wio_tracker_L1_eink/variant.h @@ -33,17 +33,10 @@ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Button Configuration // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - -#ifdef BUTTON_PIN -#undef BUTTON_PIN -#endif - -#define BUTTON_PIN D13 // This is the Program Button +#define CANCEL_BUTTON_PIN D13 // This is the Program Button // #define BUTTON_NEED_PULLUP 1 -#define BUTTON_ACTIVE_LOW true -#define BUTTON_ACTIVE_PULLUP false - -#define BUTTON_PIN_TOUCH 13 // Touch button +#define CANCEL_BUTTON_ACTIVE_LOW true +#define CANCEL_BUTTON_ACTIVE_PULLUP false // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Digital Pin Mapping (D0-D10) // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -116,7 +109,7 @@ static const uint8_t SCL = PIN_WIRE_SCL; #define PIN_EINK_SCLK 31 #define PIN_EINK_MOSI 33 #define PIN_EINK_EN 14 // unused -#define PIN_SPI1_MISO 15 // unused +#define PIN_SPI1_MISO -1 // 15 unused #define PIN_SPI1_MOSI PIN_EINK_MOSI #define PIN_SPI1_SCK PIN_EINK_SCLK @@ -175,7 +168,17 @@ static const uint8_t SCL = PIN_WIRE_SCL; // joystick // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// trackball +#define HAS_TRACKBALL 1 +#define TB_UP 25 +#define TB_DOWN 26 +#define TB_LEFT 27 +#define TB_RIGHT 28 +#define TB_PRESS 29 +#define TB_DIRECTION FALLING + #define CANNED_MESSAGE_MODULE_ENABLE 1 +#define CANNED_MESSAGE_ADD_CONFIRMATION 1 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Compatibility Definitions From 0903ed8232d6693c5f3aac948760b3f8ce294a14 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 27 Aug 2025 06:02:54 -0500 Subject: [PATCH 23/54] Mesh solar integrate (#7764) * Added HELTEC MeshSolar board. (#7499) * Added HELTEC MeshSolar board. * Set emergency shutdown pin as high impedance * Set emergency shutdown pin as high impedance Set emergency shutdown pin as high impedance * Update variants/nrf52840/heltec_mesh_solar/variant.h Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update variants/nrf52840/heltec_mesh_solar/variant.h Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update variants/nrf52840/heltec_mesh_solar/variant.h Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update I2C SCL pin definition in variant.h --------- Co-authored-by: Ben Meadors Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Updates --------- Co-authored-by: Quency-D <55523105+Quency-D@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- boards/heltec_mesh_solar.json | 54 ++++++ src/Power.cpp | 74 +++++++++ src/SerialConsole.cpp | 8 + src/mesh/StreamAPI.cpp | 119 ++++++++----- src/mesh/StreamAPI.h | 3 + src/modules/SerialModule.cpp | 23 ++- src/platform/nrf52/architecture.h | 2 + src/platform/nrf52/main-nrf52.cpp | 2 +- src/power.h | 2 + .../nrf52840/heltec_mesh_solar/platformio.ini | 19 +++ .../nrf52840/heltec_mesh_solar/variant.cpp | 36 ++++ variants/nrf52840/heltec_mesh_solar/variant.h | 157 ++++++++++++++++++ 12 files changed, 452 insertions(+), 47 deletions(-) create mode 100644 boards/heltec_mesh_solar.json create mode 100644 variants/nrf52840/heltec_mesh_solar/platformio.ini create mode 100644 variants/nrf52840/heltec_mesh_solar/variant.cpp create mode 100644 variants/nrf52840/heltec_mesh_solar/variant.h diff --git a/boards/heltec_mesh_solar.json b/boards/heltec_mesh_solar.json new file mode 100644 index 000000000..9e551c082 --- /dev/null +++ b/boards/heltec_mesh_solar.json @@ -0,0 +1,54 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x4405"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"], + ["0x239A", "0x0071"] + ], + "usb_product": "HT-n5262", + "mcu": "nrf52840", + "variant": "heltec_mesh_solar", + "variants_dir": "variants", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "6.1.1", + "sd_fwid": "0x00B6" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "onboard_tools": ["jlink"], + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" + }, + "frameworks": ["arduino"], + "name": "Heltec nrf (Adafruit BSP)", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://heltec.org/project/meshsolar/", + "vendor": "Heltec" +} diff --git a/src/Power.cpp b/src/Power.cpp index 8a16132f1..bf74f6e53 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -681,6 +681,8 @@ bool Power::setup() found = true; } else if (lipoChargerInit()) { found = true; + } else if (meshSolarInit()) { + found = true; } else if (analogInit()) { found = true; } @@ -1450,3 +1452,75 @@ bool Power::lipoChargerInit() return false; } #endif + + + +#ifdef HELTEC_MESH_SOLAR +#include "meshSolarApp.h" + +/** + * meshSolar class for an SMBUS battery sensor. + */ +class meshSolarBatteryLevel : public HasBatteryLevel +{ + + public: + /** + * Init the I2C meshSolar battery level sensor + */ + bool runOnce() + { + meshSolarStart(); + return true; + } + + /** + * Battery state of charge, from 0 to 100 or -1 for unknown + */ + virtual int getBatteryPercent() override { return meshSolarGetBatteryPercent(); } + + /** + * The raw voltage of the battery in millivolts, or NAN if unknown + */ + virtual uint16_t getBattVoltage() override { return meshSolarGetBattVoltage(); } + + /** + * return true if there is a battery installed in this unit + */ + virtual bool isBatteryConnect() override { return meshSolarIsBatteryConnect(); } + + /** + * return true if there is an external power source detected + */ + virtual bool isVbusIn() override { return meshSolarIsVbusIn();} + + /** + * return true if the battery is currently charging + */ + virtual bool isCharging() override { return meshSolarIsCharging(); } +}; + +meshSolarBatteryLevel meshSolarLevel; + +/** + * Init the meshSolar battery level sensor + */ +bool Power::meshSolarInit() +{ + bool result = meshSolarLevel.runOnce(); + LOG_DEBUG("Power::meshSolarInit mesh solar sensor is %s", result ? "ready" : "not ready yet"); + if (!result) + return false; + batteryLevel = &meshSolarLevel; + return true; +} + +#else +/** + * The meshSolar battery level sensor is unavailable - default to AnalogBatteryLevel + */ +bool Power::meshSolarInit() +{ + return false; +} +#endif \ No newline at end of file diff --git a/src/SerialConsole.cpp b/src/SerialConsole.cpp index 68c41980d..093a24678 100644 --- a/src/SerialConsole.cpp +++ b/src/SerialConsole.cpp @@ -64,6 +64,14 @@ SerialConsole::SerialConsole() : StreamAPI(&Port), RedirectablePrint(&Port), con int32_t SerialConsole::runOnce() { +#ifdef HELTEC_MESH_SOLAR + //After enabling the mesh solar serial port module configuration, command processing is handled by the serial port module. + if(moduleConfig.serial.enabled && moduleConfig.serial.override_console_serial_port + && moduleConfig.serial.mode==meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG) + { + return 250; + } +#endif return runOncePart(); } diff --git a/src/mesh/StreamAPI.cpp b/src/mesh/StreamAPI.cpp index 4a42e5197..3d652b6d6 100644 --- a/src/mesh/StreamAPI.cpp +++ b/src/mesh/StreamAPI.cpp @@ -15,9 +15,65 @@ int32_t StreamAPI::runOncePart() checkConnectionTimeout(); return result; } +int32_t StreamAPI::runOncePart(char *buf, uint16_t bufLen) +{ + auto result = readStream(buf, bufLen); + writeStream(); + checkConnectionTimeout(); + return result; +} + +int32_t StreamAPI::handleRecStream(char *buf, uint16_t bufLen) +{ + uint16_t index = 0; + while (bufLen > index) { // Currently we never want to block + int cInt = buf[index++]; + if (cInt < 0) + break; // We ran out of characters (even though available said otherwise) - this can happen on rf52 adafruit + // arduino + + uint8_t c = (uint8_t)cInt; + + // Use the read pointer for a little state machine, first look for framing, then length bytes, then payload + size_t ptr = rxPtr; + + rxPtr++; // assume we will probably advance the rxPtr + rxBuf[ptr] = c; // store all bytes (including framing) + + // console->printf("rxPtr %d ptr=%d c=0x%x\n", rxPtr, ptr, c); + + if (ptr == 0) { // looking for START1 + if (c != START1) + rxPtr = 0; // failed to find framing + } else if (ptr == 1) { // looking for START2 + if (c != START2) + rxPtr = 0; // failed to find framing + } else if (ptr >= HEADER_LEN - 1) { // we have at least read our 4 byte framing + uint32_t len = (rxBuf[2] << 8) + rxBuf[3]; // big endian 16 bit length follows framing + + // console->printf("len %d\n", len); + + if (ptr == HEADER_LEN - 1) { + // we _just_ finished our 4 byte header, validate length now (note: a length of zero is a valid + // protobuf also) + if (len > MAX_TO_FROM_RADIO_SIZE) + rxPtr = 0; // length is bogus, restart search for framing + } + + if (rxPtr != 0) // Is packet still considered 'good'? + if (ptr + 1 >= len + HEADER_LEN) { // have we received all of the payload? + rxPtr = 0; // start over again on the next packet + + // If we didn't just fail the packet and we now have the right # of bytes, parse it + handleToRadio(rxBuf + HEADER_LEN, len); + } + } + } + return 0; +} /** - * Read any rx chars from the link and call handleToRadio + * Read any rx chars from the link and call handleRecStream */ int32_t StreamAPI::readStream() { @@ -26,50 +82,29 @@ int32_t StreamAPI::readStream() bool recentRx = Throttle::isWithinTimespanMs(lastRxMsec, 2000); return recentRx ? 5 : 250; } else { + char buf[1]; while (stream->available()) { // Currently we never want to block - int cInt = stream->read(); - if (cInt < 0) - break; // We ran out of characters (even though available said otherwise) - this can happen on rf52 adafruit - // arduino - - uint8_t c = (uint8_t)cInt; - - // Use the read pointer for a little state machine, first look for framing, then length bytes, then payload - size_t ptr = rxPtr; - - rxPtr++; // assume we will probably advance the rxPtr - rxBuf[ptr] = c; // store all bytes (including framing) - - // console->printf("rxPtr %d ptr=%d c=0x%x\n", rxPtr, ptr, c); - - if (ptr == 0) { // looking for START1 - if (c != START1) - rxPtr = 0; // failed to find framing - } else if (ptr == 1) { // looking for START2 - if (c != START2) - rxPtr = 0; // failed to find framing - } else if (ptr >= HEADER_LEN - 1) { // we have at least read our 4 byte framing - uint32_t len = (rxBuf[2] << 8) + rxBuf[3]; // big endian 16 bit length follows framing - - // console->printf("len %d\n", len); - - if (ptr == HEADER_LEN - 1) { - // we _just_ finished our 4 byte header, validate length now (note: a length of zero is a valid - // protobuf also) - if (len > MAX_TO_FROM_RADIO_SIZE) - rxPtr = 0; // length is bogus, restart search for framing - } - - if (rxPtr != 0) // Is packet still considered 'good'? - if (ptr + 1 >= len + HEADER_LEN) { // have we received all of the payload? - rxPtr = 0; // start over again on the next packet - - // If we didn't just fail the packet and we now have the right # of bytes, parse it - handleToRadio(rxBuf + HEADER_LEN, len); - } - } + buf[0] = stream->read(); + handleRecStream(buf, 1); } + // we had bytes available this time, so assume we might have them next time also + lastRxMsec = millis(); + return 0; + } +} +/** + * Read any rx chars from the link and call handleRecStream + */ +int32_t StreamAPI::readStream(char *buf, uint16_t bufLen) +{ + uint16_t index = 0; + if (bufLen < 1) { + // Nothing available this time, if the computer has talked to us recently, poll often, otherwise let CPU sleep a long time + bool recentRx = Throttle::isWithinTimespanMs(lastRxMsec, 2000); + return recentRx ? 5 : 250; + } else { + handleRecStream(buf, bufLen); // we had bytes available this time, so assume we might have them next time also lastRxMsec = millis(); return 0; diff --git a/src/mesh/StreamAPI.h b/src/mesh/StreamAPI.h index 6e0364bc1..547dd0175 100644 --- a/src/mesh/StreamAPI.h +++ b/src/mesh/StreamAPI.h @@ -50,12 +50,15 @@ class StreamAPI : public PhoneAPI * phone. */ virtual int32_t runOncePart(); + virtual int32_t runOncePart(char *buf,uint16_t bufLen); private: /** * Read any rx chars from the link and call handleToRadio */ int32_t readStream(); + int32_t readStream(char *buf,uint16_t bufLen); + int32_t handleRecStream(char *buf,uint16_t bufLen); /** * call getFromRadio() and deliver encapsulated packets to the Stream diff --git a/src/modules/SerialModule.cpp b/src/modules/SerialModule.cpp index 866497ecc..a2dbb07d3 100644 --- a/src/modules/SerialModule.cpp +++ b/src/modules/SerialModule.cpp @@ -45,6 +45,9 @@ */ +#ifdef HELTEC_MESH_SOLAR +#include "meshSolarApp.h" +#endif #if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ !defined(CONFIG_IDF_TARGET_ESP32C3) @@ -60,8 +63,9 @@ SerialModule *serialModule; SerialModuleRadio *serialModuleRadio; -#if defined(TTGO_T_ECHO) || defined(T_ECHO_LITE) || defined(CANARYONE) || defined(MESHLINK) || defined(ELECROW_ThinkNode_M1) || \ - defined(ELECROW_ThinkNode_M5) + +#if defined(TTGO_T_ECHO) || defined(CANARYONE) || defined(MESHLINK) || defined(ELECROW_ThinkNode_M1) || \ + defined(ELECROW_ThinkNode_M5) || defined(HELTEC_MESH_SOLAR) SerialModule::SerialModule() : StreamAPI(&Serial), concurrency::OSThread("Serial") {} static Print *serialPrint = &Serial; #elif defined(CONFIG_IDF_TARGET_ESP32C6) @@ -78,7 +82,8 @@ size_t serialPayloadSize; bool SerialModule::isValidConfig(const meshtastic_ModuleConfig_SerialConfig &config) { if (config.override_console_serial_port && !IS_ONE_OF(config.mode, meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA, - meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO)) { + meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO, + meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG)) { const char *warning = "Invalid Serial config: override console serial port is only supported in NMEA and CalTopo output-only modes."; LOG_ERROR(warning); @@ -241,7 +246,17 @@ int32_t SerialModule::runOnce() else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_WS85)) { processWXSerial(); - } else { + } +#if defined(HELTEC_MESH_SOLAR) + else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG)) { + serialPayloadSize = Serial.readBytes(serialBytes, sizeof(serialBytes)-1); + //If the parsing fails, the following parsing will be performed. + if((serialPayloadSize > 0) && (meshSolarCmdHandle(serialBytes)!=0)) { + return runOncePart(serialBytes,serialPayloadSize); + } + } +#endif + else { #if defined(CONFIG_IDF_TARGET_ESP32C6) while (Serial1.available()) { serialPayloadSize = Serial1.readBytes(serialBytes, meshtastic_Constants_DATA_PAYLOAD_LEN); diff --git a/src/platform/nrf52/architecture.h b/src/platform/nrf52/architecture.h index 064bd8ef0..c9938062e 100644 --- a/src/platform/nrf52/architecture.h +++ b/src/platform/nrf52/architecture.h @@ -98,6 +98,8 @@ #define HW_VENDOR meshtastic_HardwareModel_SEEED_WIO_TRACKER_L1_EINK #elif defined(SEEED_WIO_TRACKER_L1) #define HW_VENDOR meshtastic_HardwareModel_SEEED_WIO_TRACKER_L1 +#elif defined(HELTEC_MESH_SOLAR) +#define HW_VENDOR meshtastic_HardwareModel_HELTEC_MESH_SOLAR #else #define HW_VENDOR meshtastic_HardwareModel_NRF52_UNKNOWN #endif diff --git a/src/platform/nrf52/main-nrf52.cpp b/src/platform/nrf52/main-nrf52.cpp index 590d2f0ae..8ce74d5f7 100644 --- a/src/platform/nrf52/main-nrf52.cpp +++ b/src/platform/nrf52/main-nrf52.cpp @@ -323,7 +323,7 @@ void cpuDeepSleep(uint32_t msecToWake) #endif #endif -#ifdef HELTEC_MESH_NODE_T114 +#if defined(HELTEC_MESH_NODE_T114) || defined(HELTEC_MESH_SOLAR) nrf_gpio_cfg_default(PIN_GPS_PPS); detachInterrupt(PIN_GPS_PPS); detachInterrupt(PIN_BUTTON1); diff --git a/src/power.h b/src/power.h index 1c078c06d..e96f5b022 100644 --- a/src/power.h +++ b/src/power.h @@ -128,6 +128,8 @@ class Power : private concurrency::OSThread bool lipoInit(); /// Setup a Lipo charger bool lipoChargerInit(); + /// Setup a meshSolar battery sensor + bool meshSolarInit(); private: void shutdown(); diff --git a/variants/nrf52840/heltec_mesh_solar/platformio.ini b/variants/nrf52840/heltec_mesh_solar/platformio.ini new file mode 100644 index 000000000..65d26dc40 --- /dev/null +++ b/variants/nrf52840/heltec_mesh_solar/platformio.ini @@ -0,0 +1,19 @@ +; First prototype nrf52840/sx1262 device +[env:heltec-mesh-solar] +extends = nrf52840_base +board = heltec_mesh_solar +board_level = pr +debug_tool = jlink + +# add -DCFG_SYSVIEW if you want to use the Segger systemview tool for OS profiling. +build_flags = ${nrf52840_base.build_flags} + -Ivariants/nrf52840/heltec_mesh_solar + -DGPS_POWER_TOGGLE + -DHELTEC_MESH_SOLAR + +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/heltec_mesh_solar> +lib_deps = + ${nrf52840_base.lib_deps} + https://github.com/NMIoT/meshsolar/archive/dfc5330dad443982e6cdd37a61d33fc7252f468b.zip + lewisxhe/PCF8563_Library@^1.0.1 + ArduinoJson@6.21.4 diff --git a/variants/nrf52840/heltec_mesh_solar/variant.cpp b/variants/nrf52840/heltec_mesh_solar/variant.cpp new file mode 100644 index 000000000..8236d7cf4 --- /dev/null +++ b/variants/nrf52840/heltec_mesh_solar/variant.cpp @@ -0,0 +1,36 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 - pins 0 and 1 are hardwired for xtal and should never be enabled + 0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + + // P1 + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; + +void initVariant() +{ + pinMode(BQ4050_EMERGENCY_SHUTDOWN_PIN, INPUT); +} diff --git a/variants/nrf52840/heltec_mesh_solar/variant.h b/variants/nrf52840/heltec_mesh_solar/variant.h new file mode 100644 index 000000000..33c2b2556 --- /dev/null +++ b/variants/nrf52840/heltec_mesh_solar/variant.h @@ -0,0 +1,157 @@ +/* + Copyright (c) 2014-2015 Arduino LLC. All right reserved. + Copyright (c) 2016 Sandeep Mistry All right reserved. + Copyright (c) 2018, Adafruit Industries (adafruit.com) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#ifndef _VARIANT_HELTEC_NRF_ +#define _VARIANT_HELTEC_NRF_ +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +// Number of pins defined in PinDescription array +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (1) +#define NUM_ANALOG_OUTPUTS (0) + + +#define PIN_LED1 (0 + 12) // green (confirmed on 1.0 board) +#define LED_BLUE PIN_LED1 // fake for bluefruit library +#define LED_GREEN PIN_LED1 +#define LED_BUILTIN LED_GREEN +#define LED_STATE_ON 0 // State when LED is lit + +#define HAS_NEOPIXEL // Enable the use of neopixels +#define NEOPIXEL_COUNT 1 // How many neopixels are connected +#define NEOPIXEL_DATA (32+15) // gpio pin used to send data to the neopixels +#define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800) // type of neopixels in use + +/* + * Buttons + */ +#define PIN_BUTTON1 (32 + 10) +// #define PIN_BUTTON2 (0 + 18) // 0.18 is labeled on the board as RESET but we configure it in the bootloader as a regular +// GPIO + +/* +No longer populated on PCB +*/ +#define PIN_SERIAL2_RX (0 + 9) +#define PIN_SERIAL2_TX (0 + 10) +// #define PIN_SERIAL2_EN (0 + 17) + +/* + * I2C + */ + +#define WIRE_INTERFACES_COUNT 2 + +// I2C bus 0 +// Routed to footprint for PCF8563TS RTC +// Not populated on T114 V1, maybe in future? +#define PIN_WIRE_SDA (0 + 6) // P0.26 +#define PIN_WIRE_SCL (0 + 26) // P0.26 + +// I2C bus 1 +// Available on header pins, for general use +#define PIN_WIRE1_SDA (0 + 30) // P0.30 +#define PIN_WIRE1_SCL (0 + 5) // P0.13 + +/* + * Lora radio + */ + +#define USE_SX1262 +// #define USE_SX1268 +#define SX126X_CS (0 + 24) // FIXME - we really should define LORA_CS instead +#define LORA_CS (0 + 24) +#define SX126X_DIO1 (0 + 20) +// Note DIO2 is attached internally to the module to an analog switch for TX/RX switching +// #define SX1262_DIO3 (0 + 21) +// This is used as an *output* from the sx1262 and connected internally to power the tcxo, do not drive from the +// main +// CPU? +#define SX126X_BUSY (0 + 17) +#define SX126X_RESET (0 + 25) +// Not really an E22 but TTGO seems to be trying to clone that +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +/* + * GPS pins + */ + +#define GPS_L76K + +// #define PIN_GPS_RESET (32 + 6) // An output to reset L76K GPS. As per datasheet, low for > 100ms will reset the L76K +// #define GPS_RESET_MODE LOW +// #define PIN_GPS_EN (21) +#define PERIPHERAL_WARMUP_MS 1000 // Make sure I2C QuickLink has stable power before continuing +#define VEXT_ON_VALUE HIGH +// #define GPS_EN_ACTIVE HIGH +#define PIN_GPS_STANDBY (32 + 2) // An output to wake GPS, low means allow sleep, high means force wake +#define PIN_GPS_PPS (32 + 4) +// Seems to be missing on this new board +// #define PIN_GPS_PPS (32 + 4) // Pulse per second input from the GPS +#define GPS_TX_PIN (32 + 5) // This is for bits going TOWARDS the CPU +#define GPS_RX_PIN (32 + 7) // This is for bits going TOWARDS the GPS + +#define GPS_THREAD_INTERVAL 50 + +#define PIN_SERIAL1_RX GPS_TX_PIN +#define PIN_SERIAL1_TX GPS_RX_PIN + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 1 + +// For LORA, spi 0 +#define PIN_SPI_MISO (0 + 23) +#define PIN_SPI_MOSI (0 + 22) +#define PIN_SPI_SCK (0 + 19) + +// #define PIN_PWR_EN (0 + 6) + +// To debug via the segger JLINK console rather than the CDC-ACM serial device +// #define USE_SEGGER + +#define BQ4050_SDA_PIN (32+1) // I2C data line pin +#define BQ4050_SCL_PIN (32+0) // I2C clock line pin +#define BQ4050_EMERGENCY_SHUTDOWN_PIN (32+3) // Emergency shutdown pin + +#define HAS_RTC 0 +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif From 3120bb8fd77bd3dc340ca14b923a0847b95f6ad4 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 27 Aug 2025 06:50:53 -0500 Subject: [PATCH 24/54] Fix check --- src/input/RotaryEncoderImpl.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/input/RotaryEncoderImpl.cpp b/src/input/RotaryEncoderImpl.cpp index b71e800e0..d3fcbbf9d 100644 --- a/src/input/RotaryEncoderImpl.cpp +++ b/src/input/RotaryEncoderImpl.cpp @@ -8,7 +8,10 @@ RotaryEncoderImpl *rotaryEncoderImpl; -RotaryEncoderImpl::RotaryEncoderImpl() : concurrency::OSThread(ORIGIN_NAME), originName(ORIGIN_NAME) {} +RotaryEncoderImpl::RotaryEncoderImpl() : concurrency::OSThread(ORIGIN_NAME), originName(ORIGIN_NAME) +{ + rotary = nullptr; +} bool RotaryEncoderImpl::init() { From 06bccef46204d073f72c7b98b9f5e9e3cc424b64 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 27 Aug 2025 07:17:46 -0500 Subject: [PATCH 25/54] Reinstitute previous streamapi readStream --- src/mesh/StreamAPI.cpp | 80 +++++++++++++++++++++++------------------- 1 file changed, 43 insertions(+), 37 deletions(-) diff --git a/src/mesh/StreamAPI.cpp b/src/mesh/StreamAPI.cpp index 3d652b6d6..a45e11ac3 100644 --- a/src/mesh/StreamAPI.cpp +++ b/src/mesh/StreamAPI.cpp @@ -73,7 +73,7 @@ int32_t StreamAPI::handleRecStream(char *buf, uint16_t bufLen) } /** - * Read any rx chars from the link and call handleRecStream + * Read any rx chars from the link and call handleToRadio */ int32_t StreamAPI::readStream() { @@ -82,50 +82,56 @@ int32_t StreamAPI::readStream() bool recentRx = Throttle::isWithinTimespanMs(lastRxMsec, 2000); return recentRx ? 5 : 250; } else { - char buf[1]; while (stream->available()) { // Currently we never want to block - buf[0] = stream->read(); - handleRecStream(buf, 1); + int cInt = stream->read(); + if (cInt < 0) + break; // We ran out of characters (even though available said otherwise) - this can happen on rf52 adafruit + // arduino + + uint8_t c = (uint8_t)cInt; + + // Use the read pointer for a little state machine, first look for framing, then length bytes, then payload + size_t ptr = rxPtr; + + rxPtr++; // assume we will probably advance the rxPtr + rxBuf[ptr] = c; // store all bytes (including framing) + + // console->printf("rxPtr %d ptr=%d c=0x%x\n", rxPtr, ptr, c); + + if (ptr == 0) { // looking for START1 + if (c != START1) + rxPtr = 0; // failed to find framing + } else if (ptr == 1) { // looking for START2 + if (c != START2) + rxPtr = 0; // failed to find framing + } else if (ptr >= HEADER_LEN - 1) { // we have at least read our 4 byte framing + uint32_t len = (rxBuf[2] << 8) + rxBuf[3]; // big endian 16 bit length follows framing + + // console->printf("len %d\n", len); + + if (ptr == HEADER_LEN - 1) { + // we _just_ finished our 4 byte header, validate length now (note: a length of zero is a valid + // protobuf also) + if (len > MAX_TO_FROM_RADIO_SIZE) + rxPtr = 0; // length is bogus, restart search for framing + } + + if (rxPtr != 0) // Is packet still considered 'good'? + if (ptr + 1 >= len + HEADER_LEN) { // have we received all of the payload? + rxPtr = 0; // start over again on the next packet + + // If we didn't just fail the packet and we now have the right # of bytes, parse it + handleToRadio(rxBuf + HEADER_LEN, len); + } + } } + // we had bytes available this time, so assume we might have them next time also lastRxMsec = millis(); return 0; } } -/** - * Read any rx chars from the link and call handleRecStream - */ -int32_t StreamAPI::readStream(char *buf, uint16_t bufLen) -{ - uint16_t index = 0; - if (bufLen < 1) { - // Nothing available this time, if the computer has talked to us recently, poll often, otherwise let CPU sleep a long time - bool recentRx = Throttle::isWithinTimespanMs(lastRxMsec, 2000); - return recentRx ? 5 : 250; - } else { - handleRecStream(buf, bufLen); - // we had bytes available this time, so assume we might have them next time also - lastRxMsec = millis(); - return 0; - } -} - -/** - * call getFromRadio() and deliver encapsulated packets to the Stream - */ -void StreamAPI::writeStream() -{ - if (canWrite) { - uint32_t len; - do { - // Send every packet we can - len = getFromRadio(txBuf + HEADER_LEN); - emitTxBuffer(len); - } while (len); - } -} - /** * Send the current txBuffer over our stream */ From 237b8908f75bab38c18cc8ec735b86ea59983b76 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 27 Aug 2025 09:54:39 -0500 Subject: [PATCH 26/54] Chainsaw took too much off the top --- src/mesh/StreamAPI.cpp | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/mesh/StreamAPI.cpp b/src/mesh/StreamAPI.cpp index a45e11ac3..20026767e 100644 --- a/src/mesh/StreamAPI.cpp +++ b/src/mesh/StreamAPI.cpp @@ -15,6 +15,7 @@ int32_t StreamAPI::runOncePart() checkConnectionTimeout(); return result; } + int32_t StreamAPI::runOncePart(char *buf, uint16_t bufLen) { auto result = readStream(buf, bufLen); @@ -23,6 +24,38 @@ int32_t StreamAPI::runOncePart(char *buf, uint16_t bufLen) return result; } +/** + * Read any rx chars from the link and call handleRecStream + */ +int32_t StreamAPI::readStream(char *buf, uint16_t bufLen) +{ + if (bufLen < 1) { + // Nothing available this time, if the computer has talked to us recently, poll often, otherwise let CPU sleep a long time + bool recentRx = Throttle::isWithinTimespanMs(lastRxMsec, 2000); + return recentRx ? 5 : 250; + } else { + handleRecStream(buf, bufLen); + // we had bytes available this time, so assume we might have them next time also + lastRxMsec = millis(); + return 0; + } +} + +/** + * call getFromRadio() and deliver encapsulated packets to the Stream + */ +void StreamAPI::writeStream() +{ + if (canWrite) { + uint32_t len; + do { + // Send every packet we can + len = getFromRadio(txBuf + HEADER_LEN); + emitTxBuffer(len); + } while (len); + } +} + int32_t StreamAPI::handleRecStream(char *buf, uint16_t bufLen) { uint16_t index = 0; From 26c38ffc8e3f181e004445a1e86d4da3ecbaf1da Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 27 Aug 2025 11:55:27 -0500 Subject: [PATCH 27/54] Remove debug logging --- variants/esp32s3/unphone/platformio.ini | 2 -- 1 file changed, 2 deletions(-) diff --git a/variants/esp32s3/unphone/platformio.ini b/variants/esp32s3/unphone/platformio.ini index ecb1cbd67..f6c3e2855 100644 --- a/variants/esp32s3/unphone/platformio.ini +++ b/variants/esp32s3/unphone/platformio.ini @@ -52,8 +52,6 @@ build_flags = -D LV_USE_PERF_MONITOR=0 -D LV_USE_MEM_MONITOR=0 -D LV_USE_LOG=0 - -D USE_LOG_DEBUG - -D LOG_DEBUG_INC=\"DebugConfiguration.h\" -D LGFX_SCREEN_WIDTH=320 -D LGFX_SCREEN_HEIGHT=480 -D DISPLAY_SIZE=320x480 ; portrait mode From d21d6d208542b8ca645333e67e3b1e743811eb75 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 27 Aug 2025 13:53:34 -0500 Subject: [PATCH 28/54] Update meshtastic/device-ui digest to a3e0e1b (#7766) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 543205996..ef0fef791 100644 --- a/platformio.ini +++ b/platformio.ini @@ -118,7 +118,7 @@ lib_deps = [device-ui_base] lib_deps = # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master - https://github.com/meshtastic/device-ui/archive/0f32b64dca418c6465763ec576509a6a2bfbc50a.zip + https://github.com/meshtastic/device-ui/archive/a3e0e1be372d069f47b4c19d718f5267251744d7.zip ; Common libs for environmental measurements in telemetry module [environmental_base] From a4d96bebfbe8c699fdef8662a0e28fa8a0c6014b Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 27 Aug 2025 14:35:29 -0500 Subject: [PATCH 29/54] Drop for now --- variants/esp32s3/unphone/platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/variants/esp32s3/unphone/platformio.ini b/variants/esp32s3/unphone/platformio.ini index f6c3e2855..476858ff5 100644 --- a/variants/esp32s3/unphone/platformio.ini +++ b/variants/esp32s3/unphone/platformio.ini @@ -32,6 +32,7 @@ lib_deps = ${esp32s3_base.lib_deps} [env:unphone-tft] +board_level = extra extends = env:unphone build_flags = ${env:unphone.build_flags} From 25a19b49ad9a20fd8296b120183154976b46c0cc Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 27 Aug 2025 15:18:26 -0500 Subject: [PATCH 30/54] This one is not working yet --- variants/esp32s3/tlora-pager/platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/variants/esp32s3/tlora-pager/platformio.ini b/variants/esp32s3/tlora-pager/platformio.ini index 3d77d879c..b16e516a7 100644 --- a/variants/esp32s3/tlora-pager/platformio.ini +++ b/variants/esp32s3/tlora-pager/platformio.ini @@ -29,6 +29,7 @@ lib_deps = ${esp32s3_base.lib_deps} https://github.com/mverch67/RotaryEncoder [env:tlora-pager-tft] +board_level = extra extends = env:tlora-pager build_flags = ${env:tlora-pager.build_flags} From 834c3c5cc2be305cf3ee465c600e4282e0928f46 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 27 Aug 2025 16:24:57 -0500 Subject: [PATCH 31/54] Add this back in --- src/modules/SerialModule.cpp | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/modules/SerialModule.cpp b/src/modules/SerialModule.cpp index a2dbb07d3..880768839 100644 --- a/src/modules/SerialModule.cpp +++ b/src/modules/SerialModule.cpp @@ -45,7 +45,7 @@ */ -#ifdef HELTEC_MESH_SOLAR +#ifdef HELTEC_MESH_SOLAR #include "meshSolarApp.h" #endif @@ -63,9 +63,8 @@ SerialModule *serialModule; SerialModuleRadio *serialModuleRadio; - #if defined(TTGO_T_ECHO) || defined(CANARYONE) || defined(MESHLINK) || defined(ELECROW_ThinkNode_M1) || \ - defined(ELECROW_ThinkNode_M5) || defined(HELTEC_MESH_SOLAR) + defined(ELECROW_ThinkNode_M5) || defined(HELTEC_MESH_SOLAR) || defined(T_ECHO_LITE) SerialModule::SerialModule() : StreamAPI(&Serial), concurrency::OSThread("Serial") {} static Print *serialPrint = &Serial; #elif defined(CONFIG_IDF_TARGET_ESP32C6) @@ -83,7 +82,7 @@ bool SerialModule::isValidConfig(const meshtastic_ModuleConfig_SerialConfig &con { if (config.override_console_serial_port && !IS_ONE_OF(config.mode, meshtastic_ModuleConfig_SerialConfig_Serial_Mode_NMEA, meshtastic_ModuleConfig_SerialConfig_Serial_Mode_CALTOPO, - meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG)) { + meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG)) { const char *warning = "Invalid Serial config: override console serial port is only supported in NMEA and CalTopo output-only modes."; LOG_ERROR(warning); @@ -249,11 +248,11 @@ int32_t SerialModule::runOnce() } #if defined(HELTEC_MESH_SOLAR) else if ((moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_MS_CONFIG)) { - serialPayloadSize = Serial.readBytes(serialBytes, sizeof(serialBytes)-1); - //If the parsing fails, the following parsing will be performed. - if((serialPayloadSize > 0) && (meshSolarCmdHandle(serialBytes)!=0)) { - return runOncePart(serialBytes,serialPayloadSize); - } + serialPayloadSize = Serial.readBytes(serialBytes, sizeof(serialBytes) - 1); + // If the parsing fails, the following parsing will be performed. + if ((serialPayloadSize > 0) && (meshSolarCmdHandle(serialBytes) != 0)) { + return runOncePart(serialBytes, serialPayloadSize); + } } #endif else { From 75b01e17bc6c8b39acfad6bdea9e4e5760722279 Mon Sep 17 00:00:00 2001 From: thebentern <9000580+thebentern@users.noreply.github.com> Date: Thu, 28 Aug 2025 10:33:29 +0000 Subject: [PATCH 32/54] Automated version bumps --- bin/org.meshtastic.meshtasticd.metainfo.xml | 3 +++ debian/changelog | 5 +++-- version.properties | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/bin/org.meshtastic.meshtasticd.metainfo.xml b/bin/org.meshtastic.meshtasticd.metainfo.xml index f3b3bb14d..1d97e2a66 100644 --- a/bin/org.meshtastic.meshtasticd.metainfo.xml +++ b/bin/org.meshtastic.meshtasticd.metainfo.xml @@ -87,6 +87,9 @@ + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.7 + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.6 diff --git a/debian/changelog b/debian/changelog index b36a22168..ff59db89e 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -meshtasticd (2.7.6.0) UNRELEASED; urgency=medium +meshtasticd (2.7.7.0) UNRELEASED; urgency=medium [ Austin Lane ] * Initial packaging @@ -39,5 +39,6 @@ meshtasticd (2.7.6.0) UNRELEASED; urgency=medium [ ] * GitHub Actions Automatic version bump + * GitHub Actions Automatic version bump - -- Tue, 12 Aug 2025 23:48:48 +0000 + -- Ubuntu Thu, 28 Aug 2025 10:33:25 +0000 diff --git a/version.properties b/version.properties index f9e2cb279..2e5193d49 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ [VERSION] major = 2 minor = 7 -build = 6 +build = 7 From 6c7cff7de2a13f7665e98f85050d8df4d968cf3f Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 28 Aug 2025 06:02:24 -0500 Subject: [PATCH 33/54] Merge pull request #7777 from meshtastic/create-pull-request/bump-version Bump release version --- bin/org.meshtastic.meshtasticd.metainfo.xml | 3 +++ debian/changelog | 5 +++-- version.properties | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/bin/org.meshtastic.meshtasticd.metainfo.xml b/bin/org.meshtastic.meshtasticd.metainfo.xml index f3b3bb14d..1d97e2a66 100644 --- a/bin/org.meshtastic.meshtasticd.metainfo.xml +++ b/bin/org.meshtastic.meshtasticd.metainfo.xml @@ -87,6 +87,9 @@ + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.7 + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.6 diff --git a/debian/changelog b/debian/changelog index b36a22168..ff59db89e 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -meshtasticd (2.7.6.0) UNRELEASED; urgency=medium +meshtasticd (2.7.7.0) UNRELEASED; urgency=medium [ Austin Lane ] * Initial packaging @@ -39,5 +39,6 @@ meshtasticd (2.7.6.0) UNRELEASED; urgency=medium [ ] * GitHub Actions Automatic version bump + * GitHub Actions Automatic version bump - -- Tue, 12 Aug 2025 23:48:48 +0000 + -- Ubuntu Thu, 28 Aug 2025 10:33:25 +0000 diff --git a/version.properties b/version.properties index f9e2cb279..2e5193d49 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ [VERSION] major = 2 minor = 7 -build = 6 +build = 7 From dd2f77ea0c68870f527c316d072149584dc6116f Mon Sep 17 00:00:00 2001 From: Jason P Date: Thu, 28 Aug 2025 11:23:24 -0500 Subject: [PATCH 34/54] BaseUI Show/Hide Frame Functionality (#7382) * Rename System Frame (from Memory) in code base * Create menu options to Show/Hide frames: Node Lists, Bearings, Position, LoRa, Clock and Favorites frames * Move Region Picker into submenu * Tweak wording for Send Position vs Node Info if the device has GPS --- src/graphics/Screen.cpp | 221 +++++++++++++++++++--------- src/graphics/Screen.h | 30 +++- src/graphics/draw/DebugRenderer.cpp | 2 +- src/graphics/draw/DebugRenderer.h | 4 +- src/graphics/draw/MenuHandler.cpp | 149 ++++++++++++++++++- src/graphics/draw/MenuHandler.h | 3 + src/graphics/images.h | 4 +- 7 files changed, 330 insertions(+), 83 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 9ef7cce86..76c423133 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -941,71 +941,86 @@ void Screen::setFrames(FrameFocus focus) } #if defined(DISPLAY_CLOCK_FRAME) - fsi.positions.clock = numframes; - normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame - : graphics::ClockRenderer::drawDigitalClockFrame; - indicatorIcons.push_back(digital_icon_clock); + if (!hiddenFrames.clock) { + fsi.positions.clock = numframes; + normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame + : graphics::ClockRenderer::drawDigitalClockFrame; + indicatorIcons.push_back(digital_icon_clock); + } #endif // Declare this early so it’s available in FOCUS_PRESERVE block bool willInsertTextMessage = shouldDrawMessage(&devicestate.rx_text_message); - fsi.positions.home = numframes; - normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused; - indicatorIcons.push_back(icon_home); + if (!hiddenFrames.home) { + fsi.positions.home = numframes; + normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused; + indicatorIcons.push_back(icon_home); + } fsi.positions.textMessage = numframes; normalFrames[numframes++] = graphics::MessageRenderer::drawTextMessageFrame; indicatorIcons.push_back(icon_mail); #ifndef USE_EINK - fsi.positions.nodelist = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen; - indicatorIcons.push_back(icon_nodes); + if (!hiddenFrames.nodelist) { + fsi.positions.nodelist = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen; + indicatorIcons.push_back(icon_nodes); + } #endif // Show detailed node views only on E-Ink builds #ifdef USE_EINK - fsi.positions.nodelist_lastheard = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen; - indicatorIcons.push_back(icon_nodes); - - fsi.positions.nodelist_hopsignal = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen; - indicatorIcons.push_back(icon_signal); - - fsi.positions.nodelist_distance = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen; - indicatorIcons.push_back(icon_distance); + if (!hiddenFrames.nodelist_lastheard) { + fsi.positions.nodelist_lastheard = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen; + indicatorIcons.push_back(icon_nodes); + } + if (!hiddenFrames.nodelist_hopsignal) { + fsi.positions.nodelist_hopsignal = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen; + indicatorIcons.push_back(icon_signal); + } + if (!hiddenFrames.nodelist_distance) { + fsi.positions.nodelist_distance = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen; + indicatorIcons.push_back(icon_distance); + } #endif #if HAS_GPS - fsi.positions.nodelist_bearings = numframes; - normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses; - indicatorIcons.push_back(icon_list); - - fsi.positions.gps = numframes; - normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen; - indicatorIcons.push_back(icon_compass); + if (!hiddenFrames.nodelist_bearings) { + fsi.positions.nodelist_bearings = numframes; + normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses; + indicatorIcons.push_back(icon_list); + } + if (!hiddenFrames.gps) { + fsi.positions.gps = numframes; + normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen; + indicatorIcons.push_back(icon_compass); + } #endif - if (RadioLibInterface::instance) { + if (RadioLibInterface::instance && !hiddenFrames.lora) { fsi.positions.lora = numframes; normalFrames[numframes++] = graphics::DebugRenderer::drawLoRaFocused; indicatorIcons.push_back(icon_radio); } - if (!dismissedFrames.memory) { - fsi.positions.memory = numframes; - normalFrames[numframes++] = graphics::DebugRenderer::drawMemoryUsage; - indicatorIcons.push_back(icon_memory); + if (!hiddenFrames.system) { + fsi.positions.system = numframes; + normalFrames[numframes++] = graphics::DebugRenderer::drawSystemScreen; + indicatorIcons.push_back(icon_system); } #if !defined(DISPLAY_CLOCK_FRAME) - fsi.positions.clock = numframes; - normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame - : graphics::ClockRenderer::drawDigitalClockFrame; - indicatorIcons.push_back(digital_icon_clock); + if (!hiddenFrames.clock) { + fsi.positions.clock = numframes; + normalFrames[numframes++] = uiconfig.is_clockface_analog ? graphics::ClockRenderer::drawAnalogClockFrame + : graphics::ClockRenderer::drawDigitalClockFrame; + indicatorIcons.push_back(digital_icon_clock); + } #endif #if HAS_WIFI && !defined(ARCH_PORTDUINO) - if (!dismissedFrames.wifi && isWifiAvailable()) { + if (!hiddenFrames.wifi && isWifiAvailable()) { fsi.positions.wifi = numframes; normalFrames[numframes++] = graphics::DebugRenderer::drawDebugInfoWiFiTrampoline; indicatorIcons.push_back(icon_wifi); @@ -1047,27 +1062,29 @@ void Screen::setFrames(FrameFocus focus) if (numMeshNodes > 0) numMeshNodes--; - // Temporary array to hold favorite node frames - std::vector favoriteFrames; + if (!hiddenFrames.show_favorites) { + // Temporary array to hold favorite node frames + std::vector favoriteFrames; - for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { - const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); - if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) { - favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo); + for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { + const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); + if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) { + favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo); + } } - } - // Insert favorite frames *after* collecting them all - if (!favoriteFrames.empty()) { - fsi.positions.firstFavorite = numframes; - for (const auto &f : favoriteFrames) { - normalFrames[numframes++] = f; - indicatorIcons.push_back(icon_node); + // Insert favorite frames *after* collecting them all + if (!favoriteFrames.empty()) { + fsi.positions.firstFavorite = numframes; + for (const auto &f : favoriteFrames) { + normalFrames[numframes++] = f; + indicatorIcons.push_back(icon_node); + } + fsi.positions.lastFavorite = numframes - 1; + } else { + fsi.positions.firstFavorite = 255; + fsi.positions.lastFavorite = 255; } - fsi.positions.lastFavorite = numframes - 1; - } else { - fsi.positions.firstFavorite = 255; - fsi.positions.lastFavorite = 255; } fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE @@ -1106,7 +1123,7 @@ void Screen::setFrames(FrameFocus focus) ui->switchToFrame(fsi.positions.clock); break; case FOCUS_SYSTEM: - ui->switchToFrame(fsi.positions.memory); + ui->switchToFrame(fsi.positions.system); break; case FOCUS_PRESERVE: @@ -1134,30 +1151,96 @@ void Screen::setFrameImmediateDraw(FrameCallback *drawFrames) setFastFramerate(); } +void Screen::toggleFrameVisibility(const std::string &frameName) +{ +#ifndef USE_EINK + if (frameName == "nodelist") { + hiddenFrames.nodelist = !hiddenFrames.nodelist; + } +#endif +#ifdef USE_EINK + if (frameName == "nodelist_lastheard") { + hiddenFrames.nodelist_lastheard = !hiddenFrames.nodelist_lastheard; + } + if (frameName == "nodelist_hopsignal") { + hiddenFrames.nodelist_hopsignal = !hiddenFrames.nodelist_hopsignal; + } + if (frameName == "nodelist_distance") { + hiddenFrames.nodelist_distance = !hiddenFrames.nodelist_distance; + } +#endif +#if HAS_GPS + if (frameName == "nodelist_bearings") { + hiddenFrames.nodelist_bearings = !hiddenFrames.nodelist_bearings; + } + if (frameName == "gps") { + hiddenFrames.gps = !hiddenFrames.gps; + } +#endif + if (frameName == "lora") { + hiddenFrames.lora = !hiddenFrames.lora; + } + if (frameName == "clock") { + hiddenFrames.clock = !hiddenFrames.clock; + } + if (frameName == "show_favorites") { + hiddenFrames.show_favorites = !hiddenFrames.show_favorites; + } +} + +bool Screen::isFrameHidden(const std::string &frameName) const +{ +#ifndef USE_EINK + if (frameName == "nodelist") + return hiddenFrames.nodelist; +#endif +#ifdef USE_EINK + if (frameName == "nodelist_lastheard") + return hiddenFrames.nodelist_lastheard; + if (frameName == "nodelist_hopsignal") + return hiddenFrames.nodelist_hopsignal; + if (frameName == "nodelist_distance") + return hiddenFrames.nodelist_distance; +#endif +#if HAS_GPS + if (frameName == "nodelist_bearings") + return hiddenFrames.nodelist_bearings; + if (frameName == "gps") + return hiddenFrames.gps; +#endif + if (frameName == "lora") + return hiddenFrames.lora; + if (frameName == "clock") + return hiddenFrames.clock; + if (frameName == "show_favorites") + return hiddenFrames.show_favorites; + + return false; +} + // Dismisses the currently displayed screen frame, if possible // Relevant for text message, waypoint, others in future? // Triggered with a CardKB keycombo -void Screen::dismissCurrentFrame() +void Screen::hideCurrentFrame() { uint8_t currentFrame = ui->getUiState()->currentFrame; bool dismissed = false; - if (currentFrame == framesetInfo.positions.textMessage && devicestate.has_rx_text_message) { - LOG_INFO("Dismiss Text Message"); + LOG_INFO("Hide Text Message"); devicestate.has_rx_text_message = false; memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message)); } else if (currentFrame == framesetInfo.positions.waypoint && devicestate.has_rx_waypoint) { - LOG_DEBUG("Dismiss Waypoint"); + LOG_DEBUG("Hide Waypoint"); devicestate.has_rx_waypoint = false; - dismissedFrames.waypoint = true; + hiddenFrames.waypoint = true; dismissed = true; } else if (currentFrame == framesetInfo.positions.wifi) { - LOG_DEBUG("Dismiss WiFi Screen"); - dismissedFrames.wifi = true; + LOG_DEBUG("Hide WiFi Screen"); + hiddenFrames.wifi = true; dismissed = true; - } else if (currentFrame == framesetInfo.positions.memory) { - LOG_INFO("Dismiss Memory"); - dismissedFrames.memory = true; + } else if (currentFrame == framesetInfo.positions.lora) { + LOG_INFO("Hide LoRa"); + hiddenFrames.lora = true; dismissed = true; } @@ -1309,7 +1392,7 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) // Outgoing message (likely sent from phone) devicestate.has_rx_text_message = false; memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message)); - dismissedFrames.textMessage = true; + hiddenFrames.textMessage = true; hasUnreadMessage = false; // Clear unread state when user replies setFrames(FOCUS_PRESERVE); // Stay on same frame, silently update frame list @@ -1439,7 +1522,7 @@ int Screen::handleInputEvent(const InputEvent *event) } else if (event->inputEvent == INPUT_BROKER_SELECT) { if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) { menuHandler::homeBaseMenu(); - } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.memory) { + } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.system) { menuHandler::systemBaseMenu(); #if HAS_GPS } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.gps && gps) { @@ -1448,7 +1531,7 @@ int Screen::handleInputEvent(const InputEvent *event) } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.clock) { menuHandler::clockMenu(); } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) { - menuHandler::LoraRegionPicker(); + menuHandler::loraMenu(); } else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage) { if (devicestate.rx_text_message.from) { menuHandler::messageResponseMenu(); diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 0f100d455..8c13bcf9a 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -593,7 +593,11 @@ class Screen : public concurrency::OSThread void setSSLFrames(); // Dismiss the currently focussed frame, if possible (e.g. text message, waypoint) - void dismissCurrentFrame(); + void hideCurrentFrame(); + + // Menu-driven Show / Hide Toggle + void toggleFrameVisibility(const std::string &frameName); + bool isFrameHidden(const std::string &frameName) const; #ifdef USE_EINK /// Draw an image to remain on E-Ink display after screen off @@ -655,7 +659,7 @@ class Screen : public concurrency::OSThread uint8_t settings = 255; uint8_t wifi = 255; uint8_t deviceFocused = 255; - uint8_t memory = 255; + uint8_t system = 255; uint8_t gps = 255; uint8_t home = 255; uint8_t textMessage = 255; @@ -673,12 +677,28 @@ class Screen : public concurrency::OSThread uint8_t frameCount = 0; } framesetInfo; - struct DismissedFrames { + struct hiddenFrames { bool textMessage = false; bool waypoint = false; bool wifi = false; - bool memory = false; - } dismissedFrames; + bool system = false; + bool home = false; + bool clock = false; +#ifndef USE_EINK + bool nodelist = false; +#endif +#ifdef USE_EINK + bool nodelist_lastheard = false; + bool nodelist_hopsignal = false; + bool nodelist_distance = false; +#endif +#if HAS_GPS + bool nodelist_bearings = false; + bool gps = false; +#endif + bool lora = false; + bool show_favorites = false; + } hiddenFrames; /// Try to start drawing ASAP void setFastFramerate(); diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index a0f29f10d..446fe7863 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -487,7 +487,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, // **************************** // * System Screen * // **************************** -void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { display->clear(); display->setFont(FONT_SMALL); diff --git a/src/graphics/draw/DebugRenderer.h b/src/graphics/draw/DebugRenderer.h index f4d484f58..3382e931d 100644 --- a/src/graphics/draw/DebugRenderer.h +++ b/src/graphics/draw/DebugRenderer.h @@ -31,8 +31,8 @@ void drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state // LoRa information display void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); -// Memory screen display -void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +// System screen display +void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); } // namespace DebugRenderer } // namespace graphics diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index a4f25797a..e92a54751 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -29,6 +29,24 @@ menuHandler::screenMenus menuHandler::menuQueue = menu_none; bool test_enabled = false; uint8_t test_count = 0; +void menuHandler::loraMenu() +{ + static const char *optionsArray[] = {"Back", "Region Picker"}; + enum optionsNumbers { Back = 0, lora_picker = 1 }; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "LoRa Actions"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 2; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Back) { + // No action + } else if (selected == lora_picker) { + menuHandler::menuQueue = menuHandler::lora_picker; + } + }; + screen->showOverlayBanner(bannerOptions); +} + void menuHandler::OnboardMessage() { static const char *optionsArray[] = {"OK", "Got it!"}; @@ -308,7 +326,7 @@ void menuHandler::messageResponseMenu() bannerOptions.optionsCount = options; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Dismiss) { - screen->dismissCurrentFrame(); + screen->hideCurrentFrame(); } else if (selected == Preset) { if (devicestate.rx_text_message.to == NODENUM_BROADCAST) { cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel); @@ -349,8 +367,11 @@ void menuHandler::homeBaseMenu() optionsArray[options] = "Sleep Screen"; optionsEnumArray[options++] = Sleep; #endif - - optionsArray[options] = "Send Position"; + if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + optionsArray[options] = "Send Position"; + } else { + optionsArray[options] = "Send Node Info"; + } optionsEnumArray[options++] = Position; optionsArray[options] = "New Preset Msg"; optionsEnumArray[options++] = Preset; @@ -430,7 +451,7 @@ void menuHandler::textMessageBaseMenu() void menuHandler::systemBaseMenu() { - enum optionsNumbers { Back, Notifications, ScreenOptions, Bluetooth, PowerMenu, Test, enumEnd }; + enum optionsNumbers { Back, Notifications, ScreenOptions, Bluetooth, PowerMenu, FrameToggles, Test, enumEnd }; static const char *optionsArray[enumEnd] = {"Back"}; static int optionsEnumArray[enumEnd] = {Back}; int options = 1; @@ -443,6 +464,9 @@ void menuHandler::systemBaseMenu() optionsEnumArray[options++] = ScreenOptions; #endif + optionsArray[options] = "Frame Visiblity Toggle"; + optionsEnumArray[options++] = FrameToggles; + optionsArray[options] = "Bluetooth Toggle"; optionsEnumArray[options++] = Bluetooth; @@ -469,6 +493,9 @@ void menuHandler::systemBaseMenu() } else if (selected == PowerMenu) { menuHandler::menuQueue = menuHandler::power_menu; screen->runNow(); + } else if (selected == FrameToggles) { + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); } else if (selected == Test) { menuHandler::menuQueue = menuHandler::test_menu; screen->runNow(); @@ -535,6 +562,7 @@ void menuHandler::positionBaseMenu() optionsArray[options] = "Compass Calibrate"; optionsEnumArray[options++] = CompassCalibrate; } + BannerOverlayOptions bannerOptions; bannerOptions.message = "Position Action"; bannerOptions.optionsArrayPtr = optionsArray; @@ -1146,6 +1174,116 @@ void menuHandler::keyVerificationFinalPrompt() } } +void menuHandler::FrameToggles_menu() +{ + enum optionsNumbers { + Finish, + nodelist, + nodelist_lastheard, + nodelist_hopsignal, + nodelist_distance, + nodelist_bearings, + gps, + lora, + clock, + show_favorites, + enumEnd + }; + static const char *optionsArray[enumEnd] = {"Finish"}; + static int optionsEnumArray[enumEnd] = {Finish}; + int options = 1; + + // Track last selected index (not enum value!) + static int lastSelectedIndex = 0; + +#ifndef USE_EINK + optionsArray[options] = screen->isFrameHidden("nodelist") ? "Show Node List" : "Hide Node List"; + optionsEnumArray[options++] = nodelist; +#endif +#ifdef USE_EINK + optionsArray[options] = screen->isFrameHidden("nodelist_lastheard") ? "Show NL - Last Heard" : "Hide NL - Last Heard"; + optionsEnumArray[options++] = nodelist_lastheard; + optionsArray[options] = screen->isFrameHidden("nodelist_hopsignal") ? "Show NL - Hops/Signal" : "Hide NL - Hops/Signal"; + optionsEnumArray[options++] = nodelist_hopsignal; + optionsArray[options] = screen->isFrameHidden("nodelist_distance") ? "Show NL - Distance" : "Hide NL - Distance"; + optionsEnumArray[options++] = nodelist_distance; +#endif +#if HAS_GPS + optionsArray[options] = screen->isFrameHidden("nodelist_bearings") ? "Show Bearings" : "Hide Bearings"; + optionsEnumArray[options++] = nodelist_bearings; + + optionsArray[options] = screen->isFrameHidden("gps") ? "Show Position" : "Hide Position"; + optionsEnumArray[options++] = gps; +#endif + + optionsArray[options] = screen->isFrameHidden("lora") ? "Show LoRa" : "Hide LoRa"; + optionsEnumArray[options++] = lora; + + optionsArray[options] = screen->isFrameHidden("clock") ? "Show Clock" : "Hide Clock"; + optionsEnumArray[options++] = clock; + + optionsArray[options] = screen->isFrameHidden("show_favorites") ? "Show Favorites" : "Hide Favorites"; + optionsEnumArray[options++] = show_favorites; + + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Show/Hide Frames"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = options; + bannerOptions.optionsEnumPtr = optionsEnumArray; + bannerOptions.InitialSelected = lastSelectedIndex; // Use index, not enum value + + bannerOptions.bannerCallback = [optionsEnumArray, options](int selected) mutable -> void { + // Find the index of selected in optionsEnumArray + int idx = 0; + for (; idx < options; ++idx) { + if (optionsEnumArray[idx] == selected) + break; + } + lastSelectedIndex = idx; + + if (selected == Finish) { + screen->setFrames(Screen::FOCUS_DEFAULT); + } else if (selected == nodelist) { + screen->toggleFrameVisibility("nodelist"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == nodelist_lastheard) { + screen->toggleFrameVisibility("nodelist_lastheard"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == nodelist_hopsignal) { + screen->toggleFrameVisibility("nodelist_hopsignal"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == nodelist_distance) { + screen->toggleFrameVisibility("nodelist_distance"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == nodelist_bearings) { + screen->toggleFrameVisibility("nodelist_bearings"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == gps) { + screen->toggleFrameVisibility("gps"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == lora) { + screen->toggleFrameVisibility("lora"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == clock) { + screen->toggleFrameVisibility("clock"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } else if (selected == show_favorites) { + screen->toggleFrameVisibility("show_favorites"); + menuHandler::menuQueue = menuHandler::FrameToggles; + screen->runNow(); + } + }; + screen->showOverlayBanner(bannerOptions); +} + void menuHandler::handleMenuSwitch(OLEDDisplay *display) { if (menuQueue != menu_none) @@ -1242,6 +1380,9 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display) case power_menu: powerMenu(); break; + case FrameToggles: + FrameToggles_menu(); + break; case throttle_message: screen->showSimpleBanner("Too Many Attempts\nTry again in 60 seconds.", 5000); break; diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index b15cf237d..00df22d6c 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -39,11 +39,13 @@ class menuHandler key_verification_final_prompt, trace_route_menu, throttle_message, + FrameToggles }; static screenMenus menuQueue; static void OnboardMessage(); static void LoraRegionPicker(uint32_t duration = 30000); + static void loraMenu(); static void handleMenuSwitch(OLEDDisplay *display); static void showConfirmationBanner(const char *message, std::function onConfirm); static void clockMenu(); @@ -76,6 +78,7 @@ class menuHandler static void notificationsMenu(); static void screenOptionsMenu(); static void powerMenu(); + static void FrameToggles_menu(); private: static void saveUIConfig(); diff --git a/src/graphics/images.h b/src/graphics/images.h index c66e4b992..e349cb6e0 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -118,8 +118,8 @@ const uint8_t icon_radio[] PROGMEM = { 0xA9 // Row 7: #..#.#.# }; -// 🪙 Memory Icon -const uint8_t icon_memory[] PROGMEM = { +// 🪙 System Icon +const uint8_t icon_system[] PROGMEM = { 0x24, // Row 0: ..#..#.. 0x3C, // Row 1: ..####.. 0xC3, // Row 2: ##....## From b0e8321514d91da0f5a90c324e3d9dce0b36af5b Mon Sep 17 00:00:00 2001 From: Tom Fifield Date: Fri, 29 Aug 2025 09:45:46 +1000 Subject: [PATCH 35/54] Only send Neighbours if we have some to send. (#7493) * Only send Neighbours if we have some to send. The original intent of NeighborInfo was that when a NeighbourInfo was sent all of the nodes that saw it would reply with NeighbourInfo. So, NeighbourInfo was sent even if there were no hop-zero nodes in the NodeDB. Since 2023, when this was implemented, our understanding of running city-wide meshes has improved substantially. We have taken steps to reduce the impact of NeighborInfo over LoRa. This change aligns with those ideas: we will now only send NeighborInfo if we have some neighbors to contribute. The impact of this change is that a node must first see another directly connected node in another packet type before NeighborInfo is sent. This means that a node with no neighbors is no longer able to trigger other nodes to broadcast NeighborInfo. It will, however, receive the regular periodic broadcast of NeighborInfo, and will be able to send NeighborInfo if it has at least 1 neighbor. * Include all the things * AvOid memleak --- src/modules/NeighborInfoModule.cpp | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/modules/NeighborInfoModule.cpp b/src/modules/NeighborInfoModule.cpp index eebf428a4..97dc17001 100644 --- a/src/modules/NeighborInfoModule.cpp +++ b/src/modules/NeighborInfoModule.cpp @@ -105,14 +105,15 @@ void NeighborInfoModule::sendNeighborInfo(NodeNum dest, bool wantReplies) { meshtastic_NeighborInfo neighborInfo = meshtastic_NeighborInfo_init_zero; collectNeighborInfo(&neighborInfo); - meshtastic_MeshPacket *p = allocDataProtobuf(neighborInfo); - // send regardless of whether or not we have neighbors in our DB, - // because we want to get neighbors for the next cycle - p->to = dest; - p->decoded.want_response = wantReplies; - p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; - printNeighborInfo("SENDING", &neighborInfo); - service->sendToMesh(p, RX_SRC_LOCAL, true); + // only send neighbours if we have some to send + if (neighborInfo.neighbors_count > 0) { + meshtastic_MeshPacket *p = allocDataProtobuf(neighborInfo); + p->to = dest; + p->decoded.want_response = wantReplies; + p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; + printNeighborInfo("SENDING", &neighborInfo); + service->sendToMesh(p, RX_SRC_LOCAL, true); + } } /* @@ -214,4 +215,4 @@ meshtastic_Neighbor *NeighborInfoModule::getOrCreateNeighbor(NodeNum originalSen neighbors.push_back(new_nbr); } return &neighbors.back(); -} \ No newline at end of file +} From 5f8503c62ddb5a1d40cdbf71960d11a08e6d5e07 Mon Sep 17 00:00:00 2001 From: Tom Fifield Date: Sat, 30 Aug 2025 00:08:33 +1000 Subject: [PATCH 36/54] We don't gotTime if time is 2019. (#7772) There are certain GPS chips that have a hard-coded time in firmware that they will return before lock. We set our own hard-coded time, BUILD_EPOCH, that should be newer and use the comparison to not set a bad time. In https://github.com/meshtastic/firmware/pull/7261 we introduced the RTCSetResult and improved it in https://github.com/meshtastic/firmware/pull/7375 . However, the original try-fix left logic in GPS.cpp that could still result in broadcasting the bad time. Further, as part of our fix we cleared the GPS buffer if we didn't get a good time. The mesh was hurting at the time, so this was a reasonable approach. However, given time tends to come in when we're trying to get early lock, this had the potential side effect of throwing away valuable information to get position lock. This change reverses the clearBuffer and changes the logic so if time is not set it will not be broadcast. Fixes https://github.com/meshtastic/firmware/issues/7771 Fixes https://github.com/meshtastic/firmware/issues/7750 --- src/gps/GPS.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index ae74f0fe2..881021975 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -1504,7 +1504,7 @@ static int32_t toDegInt(RawDegrees d) * Perform any processing that should be done only while the GPS is awake and looking for a fix. * Override this method to check for new locations * - * @return true if we've acquired a new location + * @return true if we've set a new time */ bool GPS::lookForTime() { @@ -1544,11 +1544,12 @@ The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of s if (t.tm_mon > -1) { LOG_DEBUG("NMEA GPS time %02d-%02d-%02d %02d:%02d:%02d age %d", d.year(), d.month(), t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, ti.age()); - if (perhapsSetRTC(RTCQualityGPS, t) == RTCSetResultInvalidTime) { - // Clear the GPS buffer if we got an invalid time - clearBuffer(); + if (perhapsSetRTC(RTCQualityGPS, t) == RTCSetResultSuccess) { + LOG_DEBUG("Time set."); + return true; + } else { + return false; } - return true; } else return false; } else From d3e3a91096f2189ea710d94c7dda145f1046c6ad Mon Sep 17 00:00:00 2001 From: Tom Fifield Date: Sat, 30 Aug 2025 00:08:33 +1000 Subject: [PATCH 37/54] We don't gotTime if time is 2019. (#7772) There are certain GPS chips that have a hard-coded time in firmware that they will return before lock. We set our own hard-coded time, BUILD_EPOCH, that should be newer and use the comparison to not set a bad time. In https://github.com/meshtastic/firmware/pull/7261 we introduced the RTCSetResult and improved it in https://github.com/meshtastic/firmware/pull/7375 . However, the original try-fix left logic in GPS.cpp that could still result in broadcasting the bad time. Further, as part of our fix we cleared the GPS buffer if we didn't get a good time. The mesh was hurting at the time, so this was a reasonable approach. However, given time tends to come in when we're trying to get early lock, this had the potential side effect of throwing away valuable information to get position lock. This change reverses the clearBuffer and changes the logic so if time is not set it will not be broadcast. Fixes https://github.com/meshtastic/firmware/issues/7771 Fixes https://github.com/meshtastic/firmware/issues/7750 --- src/gps/GPS.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index ae74f0fe2..881021975 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -1504,7 +1504,7 @@ static int32_t toDegInt(RawDegrees d) * Perform any processing that should be done only while the GPS is awake and looking for a fix. * Override this method to check for new locations * - * @return true if we've acquired a new location + * @return true if we've set a new time */ bool GPS::lookForTime() { @@ -1544,11 +1544,12 @@ The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of s if (t.tm_mon > -1) { LOG_DEBUG("NMEA GPS time %02d-%02d-%02d %02d:%02d:%02d age %d", d.year(), d.month(), t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, ti.age()); - if (perhapsSetRTC(RTCQualityGPS, t) == RTCSetResultInvalidTime) { - // Clear the GPS buffer if we got an invalid time - clearBuffer(); + if (perhapsSetRTC(RTCQualityGPS, t) == RTCSetResultSuccess) { + LOG_DEBUG("Time set."); + return true; + } else { + return false; } - return true; } else return false; } else From fb34dac08dcc72c50a4fa164291c4d9bcc5045fa Mon Sep 17 00:00:00 2001 From: Wilson Date: Fri, 29 Aug 2025 23:26:27 +0800 Subject: [PATCH 38/54] Add On-Screen Keyboard for UpDown Encoder and Rotary Encoder. (#7762) * Add On-Screen Keyboard for UpDownInterrupt. Pls notice the new keyboard layout was inspired and adviced by https://github.com/csrutil * Add longPress event for RotaryEncoder Press. * Update UpdownInterrupt UP and DOWN on main UI. * Change the interrupt trigger mode from rising edge to falling edge to improve button response. --- src/buzz/BuzzerFeedbackThread.cpp | 2 + src/graphics/draw/NotificationRenderer.cpp | 58 ++++++---- src/input/ButtonThread.h | 3 + src/input/InputBroker.h | 4 +- src/input/RotaryEncoderInterruptBase.cpp | 41 ++++++- src/input/RotaryEncoderInterruptBase.h | 14 ++- src/input/RotaryEncoderInterruptImpl1.cpp | 5 +- src/input/UpDownInterruptBase.cpp | 120 +++++++++++++++++---- src/input/UpDownInterruptBase.h | 30 +++++- src/input/UpDownInterruptImpl1.cpp | 14 ++- 10 files changed, 236 insertions(+), 55 deletions(-) diff --git a/src/buzz/BuzzerFeedbackThread.cpp b/src/buzz/BuzzerFeedbackThread.cpp index 838224c69..12b30a705 100644 --- a/src/buzz/BuzzerFeedbackThread.cpp +++ b/src/buzz/BuzzerFeedbackThread.cpp @@ -33,7 +33,9 @@ int BuzzerFeedbackThread::handleInputEvent(const InputEvent *event) break; case INPUT_BROKER_UP: + case INPUT_BROKER_UP_LONG: case INPUT_BROKER_DOWN: + case INPUT_BROKER_DOWN_LONG: case INPUT_BROKER_LEFT: case INPUT_BROKER_RIGHT: playChirp(); // Navigation feedback diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index 221d95075..b53cd2f3f 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -7,10 +7,18 @@ #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" #include "graphics/images.h" +#include "input/RotaryEncoderInterruptImpl1.h" +#include "input/UpDownInterruptImpl1.h" +#if HAS_BUTTON +#include "input/ButtonThread.h" +#endif #include "main.h" #include #include #include +#if HAS_TRACKBALL +#include "input/TrackballInterruptImpl1.h" +#endif #ifdef ARCH_ESP32 #include "esp_task_wdt.h" @@ -18,6 +26,11 @@ using namespace meshtastic; +#if HAS_BUTTON +// Global button thread pointer defined in main.cpp +extern ::ButtonThread *UserButtonThread; +#endif + // External references to global variables from Screen.cpp extern std::vector functionSymbol; extern std::string functionSymbolString; @@ -288,12 +301,9 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta if (nodeDB->getMeshNodeByIndex(i + 1)->has_user) { std::string sanitized = sanitizeString(nodeDB->getMeshNodeByIndex(i + 1)->user.long_name); strncpy(temp_name, sanitized.c_str(), sizeof(temp_name) - 1); - } else { snprintf(temp_name, sizeof(temp_name), "(%04X)", (uint16_t)(nodeDB->getMeshNodeByIndex(i + 1)->num & 0xFFFF)); } - // make temp buffer for name - // fi if (i == curSelected) { selectedNodenum = nodeDB->getMeshNodeByIndex(i + 1)->num; if (isHighResolution) { @@ -307,7 +317,8 @@ void NotificationRenderer::drawNodePicker(OLEDDisplay *display, OLEDDisplayUiSta } scratchLineBuffer[scratchLineNum][39] = '\0'; } else { - strncpy(scratchLineBuffer[scratchLineNum], temp_name, 36); + strncpy(scratchLineBuffer[scratchLineNum], temp_name, 39); + scratchLineBuffer[scratchLineNum][39] = '\0'; } linePointers[linesShown] = scratchLineBuffer[scratchLineNum++]; } @@ -623,59 +634,68 @@ void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiStat return; } - // Handle input events for virtual keyboard navigation if (inEvent.inputEvent != INPUT_BROKER_NONE) { if (inEvent.inputEvent == INPUT_BROKER_UP) { - virtualKeyboard->moveCursorUp(); + // high frequency for move cursor left/right than up/down with encoders + extern ::RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1; + extern ::UpDownInterruptImpl1 *upDownInterruptImpl1; + if (::rotaryEncoderInterruptImpl1 || ::upDownInterruptImpl1) { + virtualKeyboard->moveCursorLeft(); + } else { + virtualKeyboard->moveCursorUp(); + } } else if (inEvent.inputEvent == INPUT_BROKER_DOWN) { - virtualKeyboard->moveCursorDown(); + extern ::RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1; + extern ::UpDownInterruptImpl1 *upDownInterruptImpl1; + if (::rotaryEncoderInterruptImpl1 || ::upDownInterruptImpl1) { + virtualKeyboard->moveCursorRight(); + } else { + virtualKeyboard->moveCursorDown(); + } } else if (inEvent.inputEvent == INPUT_BROKER_LEFT) { virtualKeyboard->moveCursorLeft(); } else if (inEvent.inputEvent == INPUT_BROKER_RIGHT) { virtualKeyboard->moveCursorRight(); + } else if (inEvent.inputEvent == INPUT_BROKER_UP_LONG) { + virtualKeyboard->moveCursorUp(); + } else if (inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) { + virtualKeyboard->moveCursorDown(); } else if (inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) { - // Long press UP = move left virtualKeyboard->moveCursorLeft(); } else if (inEvent.inputEvent == INPUT_BROKER_USER_PRESS) { - // Long press DOWN = move right virtualKeyboard->moveCursorRight(); } else if (inEvent.inputEvent == INPUT_BROKER_SELECT) { virtualKeyboard->handlePress(); } else if (inEvent.inputEvent == INPUT_BROKER_SELECT_LONG) { virtualKeyboard->handleLongPress(); } else if (inEvent.inputEvent == INPUT_BROKER_CANCEL) { - // Cancel virtual keyboard - call callback with empty string - auto callback = textInputCallback; // Store callback before clearing - - // Clean up first to prevent re-entry + auto callback = textInputCallback; delete virtualKeyboard; virtualKeyboard = nullptr; textInputCallback = nullptr; resetBanner(); - - // Call callback after cleanup if (callback) { callback(""); } - - // Restore normal overlays if (screen) { screen->setFrames(graphics::Screen::FOCUS_PRESERVE); } return; } - // Reset input event after processing + // Consume the event after processing for virtual keyboard inEvent.inputEvent = INPUT_BROKER_NONE; } - // Clear the display and draw virtual keyboard + // Clear the screen to avoid overlapping with underlying frames or overlays display->setColor(BLACK); display->fillRect(0, 0, display->getWidth(), display->getHeight()); display->setColor(WHITE); + // Draw the virtual keyboard virtualKeyboard->draw(display, 0, 0); } else { // If virtualKeyboard is null, reset the banner to avoid getting stuck + LOG_INFO("Virtual keyboard is null - resetting banner"); resetBanner(); } } diff --git a/src/input/ButtonThread.h b/src/input/ButtonThread.h index c6d6557e2..7de38341c 100644 --- a/src/input/ButtonThread.h +++ b/src/input/ButtonThread.h @@ -76,6 +76,9 @@ class ButtonThread : public Observable, public concurrency:: return digitalRead(buttonPin); // Most buttons are active low by default } + // Returns true while this thread's button is physically held down + bool isHeld() { return isButtonPressed(_pinNum); } + // Disconnect and reconnect interrupts for light sleep #ifdef ARCH_ESP32 int beforeLightSleep(void *unused); diff --git a/src/input/InputBroker.h b/src/input/InputBroker.h index 012a403f5..2cdfa2ae2 100644 --- a/src/input/InputBroker.h +++ b/src/input/InputBroker.h @@ -4,7 +4,9 @@ enum input_broker_event { INPUT_BROKER_NONE = 0, INPUT_BROKER_SELECT = 10, - INPUT_BROKER_SELECT_LONG, + INPUT_BROKER_SELECT_LONG = 11, + INPUT_BROKER_UP_LONG = 12, + INPUT_BROKER_DOWN_LONG = 13, INPUT_BROKER_UP = 17, INPUT_BROKER_DOWN = 18, INPUT_BROKER_LEFT = 19, diff --git a/src/input/RotaryEncoderInterruptBase.cpp b/src/input/RotaryEncoderInterruptBase.cpp index 88b07a389..204a0fbf0 100644 --- a/src/input/RotaryEncoderInterruptBase.cpp +++ b/src/input/RotaryEncoderInterruptBase.cpp @@ -8,15 +8,17 @@ RotaryEncoderInterruptBase::RotaryEncoderInterruptBase(const char *name) : concu void RotaryEncoderInterruptBase::init( uint8_t pinA, uint8_t pinB, uint8_t pinPress, input_broker_event eventCw, input_broker_event eventCcw, - input_broker_event eventPressed, + input_broker_event eventPressed, input_broker_event eventPressedLong, // std::function onIntA, std::function onIntB, std::function onIntPress) : void (*onIntA)(), void (*onIntB)(), void (*onIntPress)()) { this->_pinA = pinA; this->_pinB = pinB; + this->_pinPress = pinPress; this->_eventCw = eventCw; this->_eventCcw = eventCcw; this->_eventPressed = eventPressed; + this->_eventPressedLong = eventPressedLong; bool isRAK = false; #ifdef RAK_4631 @@ -46,10 +48,37 @@ int32_t RotaryEncoderInterruptBase::runOnce() InputEvent e; e.inputEvent = INPUT_BROKER_NONE; e.source = this->_originName; + unsigned long now = millis(); + // Handle press long/short detection if (this->action == ROTARY_ACTION_PRESSED) { - LOG_DEBUG("Rotary event Press"); - e.inputEvent = this->_eventPressed; + bool buttonPressed = !digitalRead(_pinPress); + if (!pressDetected && buttonPressed) { + pressDetected = true; + pressStartTime = now; + } + + if (pressDetected) { + uint32_t duration = now - pressStartTime; + if (!buttonPressed) { + // released -> if short press, send short, else already sent long + if (duration < LONG_PRESS_DURATION && now - lastPressKeyTime >= pressDebounceMs) { + lastPressKeyTime = now; + LOG_DEBUG("Rotary event Press short"); + e.inputEvent = this->_eventPressed; + } + pressDetected = false; + pressStartTime = 0; + lastPressLongEventTime = 0; + this->action = ROTARY_ACTION_NONE; + } else if (duration >= LONG_PRESS_DURATION && this->_eventPressedLong != INPUT_BROKER_NONE && + lastPressLongEventTime == 0) { + // fire single-shot long press + lastPressLongEventTime = now; + LOG_DEBUG("Rotary event Press long"); + e.inputEvent = this->_eventPressedLong; + } + } } else if (this->action == ROTARY_ACTION_CW) { LOG_DEBUG("Rotary event CW"); e.inputEvent = this->_eventCw; @@ -62,7 +91,9 @@ int32_t RotaryEncoderInterruptBase::runOnce() this->notifyObservers(&e); } - this->action = ROTARY_ACTION_NONE; + if (!pressDetected) { + this->action = ROTARY_ACTION_NONE; + } return INT32_MAX; } @@ -70,7 +101,7 @@ int32_t RotaryEncoderInterruptBase::runOnce() void RotaryEncoderInterruptBase::intPressHandler() { this->action = ROTARY_ACTION_PRESSED; - setIntervalFromNow(20); // TODO: this modifies a non-volatile variable! + setIntervalFromNow(20); // start checking for long/short } void RotaryEncoderInterruptBase::intAHandler() diff --git a/src/input/RotaryEncoderInterruptBase.h b/src/input/RotaryEncoderInterruptBase.h index 9bdab4730..4f9757609 100644 --- a/src/input/RotaryEncoderInterruptBase.h +++ b/src/input/RotaryEncoderInterruptBase.h @@ -13,7 +13,7 @@ class RotaryEncoderInterruptBase : public Observable, public public: explicit RotaryEncoderInterruptBase(const char *name); void init(uint8_t pinA, uint8_t pinB, uint8_t pinPress, input_broker_event eventCw, input_broker_event eventCcw, - input_broker_event eventPressed, + input_broker_event eventPressed, input_broker_event eventPressedLong, // std::function onIntA, std::function onIntB, std::function onIntPress); void (*onIntA)(), void (*onIntB)(), void (*onIntPress)()); void intPressHandler(); @@ -33,10 +33,22 @@ class RotaryEncoderInterruptBase : public Observable, public volatile RotaryEncoderInterruptBaseActionType action = ROTARY_ACTION_NONE; private: + // pins and events uint8_t _pinA = 0; uint8_t _pinB = 0; + uint8_t _pinPress = 0; input_broker_event _eventCw = INPUT_BROKER_NONE; input_broker_event _eventCcw = INPUT_BROKER_NONE; input_broker_event _eventPressed = INPUT_BROKER_NONE; + input_broker_event _eventPressedLong = INPUT_BROKER_NONE; const char *_originName; + + // Long press detection variables + uint32_t pressStartTime = 0; + bool pressDetected = false; + uint32_t lastPressLongEventTime = 0; + unsigned long lastPressKeyTime = 0; + static const uint32_t LONG_PRESS_DURATION = 300; // ms + static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 0; // 0 = single-shot for rotary select + const unsigned long pressDebounceMs = 200; // ms }; diff --git a/src/input/RotaryEncoderInterruptImpl1.cpp b/src/input/RotaryEncoderInterruptImpl1.cpp index 4f19c8b0b..12cbc36fb 100644 --- a/src/input/RotaryEncoderInterruptImpl1.cpp +++ b/src/input/RotaryEncoderInterruptImpl1.cpp @@ -1,5 +1,6 @@ #include "RotaryEncoderInterruptImpl1.h" #include "InputBroker.h" +extern bool osk_found; RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1; @@ -19,12 +20,14 @@ bool RotaryEncoderInterruptImpl1::init() input_broker_event eventCw = static_cast(moduleConfig.canned_message.inputbroker_event_cw); input_broker_event eventCcw = static_cast(moduleConfig.canned_message.inputbroker_event_ccw); input_broker_event eventPressed = static_cast(moduleConfig.canned_message.inputbroker_event_press); + input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG; // moduleConfig.canned_message.ext_notification_module_output - RotaryEncoderInterruptBase::init(pinA, pinB, pinPress, eventCw, eventCcw, eventPressed, + RotaryEncoderInterruptBase::init(pinA, pinB, pinPress, eventCw, eventCcw, eventPressed, eventPressedLong, RotaryEncoderInterruptImpl1::handleIntA, RotaryEncoderInterruptImpl1::handleIntB, RotaryEncoderInterruptImpl1::handleIntPressed); inputBroker->registerSource(this); + osk_found = true; return true; } diff --git a/src/input/UpDownInterruptBase.cpp b/src/input/UpDownInterruptBase.cpp index 26b281aaf..0bf0f5cd4 100644 --- a/src/input/UpDownInterruptBase.cpp +++ b/src/input/UpDownInterruptBase.cpp @@ -7,14 +7,22 @@ UpDownInterruptBase::UpDownInterruptBase(const char *name) : concurrency::OSThre } void UpDownInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, input_broker_event eventDown, - input_broker_event eventUp, input_broker_event eventPressed, void (*onIntDown)(), + input_broker_event eventUp, input_broker_event eventPressed, input_broker_event eventPressedLong, + input_broker_event eventUpLong, input_broker_event eventDownLong, void (*onIntDown)(), void (*onIntUp)(), void (*onIntPress)(), unsigned long updownDebounceMs) { this->_pinDown = pinDown; this->_pinUp = pinUp; + this->_pinPress = pinPress; this->_eventDown = eventDown; this->_eventUp = eventUp; this->_eventPressed = eventPressed; + this->_eventPressedLong = eventPressedLong; + this->_eventUpLong = eventUpLong; + this->_eventDownLong = eventDownLong; + + // Store debounce configuration passed by caller + this->updownDebounceMs = updownDebounceMs; bool isRAK = false; #ifdef RAK_4631 isRAK = true; @@ -22,20 +30,20 @@ void UpDownInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, if (!isRAK || pinPress != 0) { pinMode(pinPress, INPUT_PULLUP); - attachInterrupt(pinPress, onIntPress, RISING); + attachInterrupt(pinPress, onIntPress, FALLING); } if (!isRAK || this->_pinDown != 0) { pinMode(this->_pinDown, INPUT_PULLUP); - attachInterrupt(this->_pinDown, onIntDown, RISING); + attachInterrupt(this->_pinDown, onIntDown, FALLING); } if (!isRAK || this->_pinUp != 0) { pinMode(this->_pinUp, INPUT_PULLUP); - attachInterrupt(this->_pinUp, onIntUp, RISING); + attachInterrupt(this->_pinUp, onIntUp, FALLING); } LOG_DEBUG("Up/down/press GPIO initialized (%d, %d, %d)", this->_pinUp, this->_pinDown, pinPress); - this->setInterval(100); + this->setInterval(20); } int32_t UpDownInterruptBase::runOnce() @@ -43,23 +51,88 @@ int32_t UpDownInterruptBase::runOnce() InputEvent e; e.inputEvent = INPUT_BROKER_NONE; unsigned long now = millis(); - if (this->action == UPDOWN_ACTION_PRESSED) { - if (now - lastPressKeyTime >= pressDebounceMs) { - lastPressKeyTime = now; - LOG_DEBUG("GPIO event Press"); - e.inputEvent = this->_eventPressed; + + // Read all button states once at the beginning + bool pressButtonPressed = !digitalRead(_pinPress); + bool upButtonPressed = !digitalRead(_pinUp); + bool downButtonPressed = !digitalRead(_pinDown); + + // Handle initial button press detection - only if not already detected + if (this->action == UPDOWN_ACTION_PRESSED && pressButtonPressed && !pressDetected) { + pressDetected = true; + pressStartTime = now; + } else if (this->action == UPDOWN_ACTION_UP && upButtonPressed && !upDetected) { + upDetected = true; + upStartTime = now; + } else if (this->action == UPDOWN_ACTION_DOWN && downButtonPressed && !downDetected) { + downDetected = true; + downStartTime = now; + } + + // Handle long press detection for press button + if (pressDetected && pressStartTime > 0) { + uint32_t pressDuration = now - pressStartTime; + + if (!pressButtonPressed) { + // Button released + if (pressDuration < LONG_PRESS_DURATION && now - lastPressKeyTime >= pressDebounceMs) { + lastPressKeyTime = now; + e.inputEvent = this->_eventPressed; + } + // Reset state + pressDetected = false; + pressStartTime = 0; + lastPressLongEventTime = 0; + } else if (pressDuration >= LONG_PRESS_DURATION && lastPressLongEventTime == 0) { + // First long press event only - avoid repeated events causing lag + e.inputEvent = this->_eventPressedLong; + lastPressLongEventTime = now; } - } else if (this->action == UPDOWN_ACTION_UP) { - if (now - lastUpKeyTime >= updownDebounceMs) { - lastUpKeyTime = now; - LOG_DEBUG("GPIO event Up"); - e.inputEvent = this->_eventUp; + } + + // Handle long press detection for up button + if (upDetected && upStartTime > 0) { + uint32_t upDuration = now - upStartTime; + + if (!upButtonPressed) { + // Button released + if (upDuration < LONG_PRESS_DURATION && now - lastUpKeyTime >= updownDebounceMs) { + lastUpKeyTime = now; + e.inputEvent = this->_eventUp; + } + // Reset state + upDetected = false; + upStartTime = 0; + lastUpLongEventTime = 0; + } else if (upDuration >= LONG_PRESS_DURATION) { + // Auto-repeat long press events + if (lastUpLongEventTime == 0 || (now - lastUpLongEventTime) >= LONG_PRESS_REPEAT_INTERVAL) { + e.inputEvent = this->_eventUpLong; + lastUpLongEventTime = now; + } } - } else if (this->action == UPDOWN_ACTION_DOWN) { - if (now - lastDownKeyTime >= updownDebounceMs) { - lastDownKeyTime = now; - LOG_DEBUG("GPIO event Down"); - e.inputEvent = this->_eventDown; + } + + // Handle long press detection for down button + if (downDetected && downStartTime > 0) { + uint32_t downDuration = now - downStartTime; + + if (!downButtonPressed) { + // Button released + if (downDuration < LONG_PRESS_DURATION && now - lastDownKeyTime >= updownDebounceMs) { + lastDownKeyTime = now; + e.inputEvent = this->_eventDown; + } + // Reset state + downDetected = false; + downStartTime = 0; + lastDownLongEventTime = 0; + } else if (downDuration >= LONG_PRESS_DURATION) { + // Auto-repeat long press events + if (lastDownLongEventTime == 0 || (now - lastDownLongEventTime) >= LONG_PRESS_REPEAT_INTERVAL) { + e.inputEvent = this->_eventDownLong; + lastDownLongEventTime = now; + } } } @@ -69,8 +142,11 @@ int32_t UpDownInterruptBase::runOnce() this->notifyObservers(&e); } - this->action = UPDOWN_ACTION_NONE; - return 100; + if (!pressDetected && !upDetected && !downDetected) { + this->action = UPDOWN_ACTION_NONE; + } + + return 20; // This will control how the input frequency } void UpDownInterruptBase::intPressHandler() diff --git a/src/input/UpDownInterruptBase.h b/src/input/UpDownInterruptBase.h index a83a298f2..ae84efdaf 100644 --- a/src/input/UpDownInterruptBase.h +++ b/src/input/UpDownInterruptBase.h @@ -8,7 +8,8 @@ class UpDownInterruptBase : public Observable, public concur public: explicit UpDownInterruptBase(const char *name); void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, input_broker_event eventDown, input_broker_event eventUp, - input_broker_event eventPressed, void (*onIntDown)(), void (*onIntUp)(), void (*onIntPress)(), + input_broker_event eventPressed, input_broker_event eventPressedLong, input_broker_event eventUpLong, + input_broker_event eventDownLong, void (*onIntDown)(), void (*onIntUp)(), void (*onIntPress)(), unsigned long updownDebounceMs = 50); void intPressHandler(); void intDownHandler(); @@ -17,16 +18,41 @@ class UpDownInterruptBase : public Observable, public concur int32_t runOnce() override; protected: - enum UpDownInterruptBaseActionType { UPDOWN_ACTION_NONE, UPDOWN_ACTION_PRESSED, UPDOWN_ACTION_UP, UPDOWN_ACTION_DOWN }; + enum UpDownInterruptBaseActionType { + UPDOWN_ACTION_NONE, + UPDOWN_ACTION_PRESSED, + UPDOWN_ACTION_PRESSED_LONG, + UPDOWN_ACTION_UP, + UPDOWN_ACTION_UP_LONG, + UPDOWN_ACTION_DOWN, + UPDOWN_ACTION_DOWN_LONG + }; volatile UpDownInterruptBaseActionType action = UPDOWN_ACTION_NONE; + // Long press detection variables + uint32_t pressStartTime = 0; + uint32_t upStartTime = 0; + uint32_t downStartTime = 0; + bool pressDetected = false; + bool upDetected = false; + bool downDetected = false; + uint32_t lastPressLongEventTime = 0; + uint32_t lastUpLongEventTime = 0; + uint32_t lastDownLongEventTime = 0; + static const uint32_t LONG_PRESS_DURATION = 300; + static const uint32_t LONG_PRESS_REPEAT_INTERVAL = 300; + private: uint8_t _pinDown = 0; uint8_t _pinUp = 0; + uint8_t _pinPress = 0; input_broker_event _eventDown = INPUT_BROKER_NONE; input_broker_event _eventUp = INPUT_BROKER_NONE; input_broker_event _eventPressed = INPUT_BROKER_NONE; + input_broker_event _eventPressedLong = INPUT_BROKER_NONE; + input_broker_event _eventUpLong = INPUT_BROKER_NONE; + input_broker_event _eventDownLong = INPUT_BROKER_NONE; const char *_originName; unsigned long lastUpKeyTime = 0; diff --git a/src/input/UpDownInterruptImpl1.cpp b/src/input/UpDownInterruptImpl1.cpp index 761b92348..9b0b1f39e 100644 --- a/src/input/UpDownInterruptImpl1.cpp +++ b/src/input/UpDownInterruptImpl1.cpp @@ -1,5 +1,6 @@ #include "UpDownInterruptImpl1.h" #include "InputBroker.h" +extern bool osk_found; UpDownInterruptImpl1 *upDownInterruptImpl1; @@ -17,13 +18,18 @@ bool UpDownInterruptImpl1::init() uint8_t pinDown = moduleConfig.canned_message.inputbroker_pin_b; uint8_t pinPress = moduleConfig.canned_message.inputbroker_pin_press; - input_broker_event eventDown = INPUT_BROKER_DOWN; - input_broker_event eventUp = INPUT_BROKER_UP; + input_broker_event eventDown = INPUT_BROKER_USER_PRESS; // acts like RIGHT/DOWN + input_broker_event eventUp = INPUT_BROKER_ALT_PRESS; // acts like LEFT/UP input_broker_event eventPressed = INPUT_BROKER_SELECT; + input_broker_event eventPressedLong = INPUT_BROKER_SELECT_LONG; + input_broker_event eventUpLong = INPUT_BROKER_UP_LONG; + input_broker_event eventDownLong = INPUT_BROKER_DOWN_LONG; - UpDownInterruptBase::init(pinDown, pinUp, pinPress, eventDown, eventUp, eventPressed, UpDownInterruptImpl1::handleIntDown, - UpDownInterruptImpl1::handleIntUp, UpDownInterruptImpl1::handleIntPressed); + UpDownInterruptBase::init(pinDown, pinUp, pinPress, eventDown, eventUp, eventPressed, eventPressedLong, eventUpLong, + eventDownLong, UpDownInterruptImpl1::handleIntDown, UpDownInterruptImpl1::handleIntUp, + UpDownInterruptImpl1::handleIntPressed); inputBroker->registerSource(this); + osk_found = true; return true; } From 9b41131af88c7aef00480a4be203f990f51dacb1 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 29 Aug 2025 10:31:55 -0500 Subject: [PATCH 39/54] Backmerge (#7782) * Merge pull request #7777 from meshtastic/create-pull-request/bump-version Bump release version * Only send Neighbours if we have some to send. (#7493) * Only send Neighbours if we have some to send. The original intent of NeighborInfo was that when a NeighbourInfo was sent all of the nodes that saw it would reply with NeighbourInfo. So, NeighbourInfo was sent even if there were no hop-zero nodes in the NodeDB. Since 2023, when this was implemented, our understanding of running city-wide meshes has improved substantially. We have taken steps to reduce the impact of NeighborInfo over LoRa. This change aligns with those ideas: we will now only send NeighborInfo if we have some neighbors to contribute. The impact of this change is that a node must first see another directly connected node in another packet type before NeighborInfo is sent. This means that a node with no neighbors is no longer able to trigger other nodes to broadcast NeighborInfo. It will, however, receive the regular periodic broadcast of NeighborInfo, and will be able to send NeighborInfo if it has at least 1 neighbor. * Include all the things * AvOid memleak * We don't gotTime if time is 2019. (#7772) There are certain GPS chips that have a hard-coded time in firmware that they will return before lock. We set our own hard-coded time, BUILD_EPOCH, that should be newer and use the comparison to not set a bad time. In https://github.com/meshtastic/firmware/pull/7261 we introduced the RTCSetResult and improved it in https://github.com/meshtastic/firmware/pull/7375 . However, the original try-fix left logic in GPS.cpp that could still result in broadcasting the bad time. Further, as part of our fix we cleared the GPS buffer if we didn't get a good time. The mesh was hurting at the time, so this was a reasonable approach. However, given time tends to come in when we're trying to get early lock, this had the potential side effect of throwing away valuable information to get position lock. This change reverses the clearBuffer and changes the logic so if time is not set it will not be broadcast. Fixes https://github.com/meshtastic/firmware/issues/7771 Fixes https://github.com/meshtastic/firmware/issues/7750 --------- Co-authored-by: Tom Fifield --- src/modules/NeighborInfoModule.cpp | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/modules/NeighborInfoModule.cpp b/src/modules/NeighborInfoModule.cpp index eebf428a4..97dc17001 100644 --- a/src/modules/NeighborInfoModule.cpp +++ b/src/modules/NeighborInfoModule.cpp @@ -105,14 +105,15 @@ void NeighborInfoModule::sendNeighborInfo(NodeNum dest, bool wantReplies) { meshtastic_NeighborInfo neighborInfo = meshtastic_NeighborInfo_init_zero; collectNeighborInfo(&neighborInfo); - meshtastic_MeshPacket *p = allocDataProtobuf(neighborInfo); - // send regardless of whether or not we have neighbors in our DB, - // because we want to get neighbors for the next cycle - p->to = dest; - p->decoded.want_response = wantReplies; - p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; - printNeighborInfo("SENDING", &neighborInfo); - service->sendToMesh(p, RX_SRC_LOCAL, true); + // only send neighbours if we have some to send + if (neighborInfo.neighbors_count > 0) { + meshtastic_MeshPacket *p = allocDataProtobuf(neighborInfo); + p->to = dest; + p->decoded.want_response = wantReplies; + p->priority = meshtastic_MeshPacket_Priority_BACKGROUND; + printNeighborInfo("SENDING", &neighborInfo); + service->sendToMesh(p, RX_SRC_LOCAL, true); + } } /* @@ -214,4 +215,4 @@ meshtastic_Neighbor *NeighborInfoModule::getOrCreateNeighbor(NodeNum originalSen neighbors.push_back(new_nbr); } return &neighbors.back(); -} \ No newline at end of file +} From 4e03df5ea7a30ac6458545df59ec24557a46598d Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 29 Aug 2025 12:09:22 -0500 Subject: [PATCH 40/54] Fix freetext hang (#7781) * Fixed freetext hangs by adding canned modules back to self-sourced packets and transition to SENDING_ACTIVE state * Update meshmodule handling --- src/mesh/MeshModule.cpp | 11 ++--------- src/mesh/MeshModule.h | 2 +- src/mesh/NodeDB.cpp | 8 ++++---- src/mesh/Router.cpp | 6 +++--- src/modules/CannedMessageModule.cpp | 3 +-- src/modules/RoutingModule.cpp | 2 +- src/modules/RoutingModule.h | 2 -- 7 files changed, 12 insertions(+), 22 deletions(-) diff --git a/src/mesh/MeshModule.cpp b/src/mesh/MeshModule.cpp index 409c52179..22fcec663 100644 --- a/src/mesh/MeshModule.cpp +++ b/src/mesh/MeshModule.cpp @@ -85,11 +85,8 @@ meshtastic_MeshPacket *MeshModule::allocErrorResponse(meshtastic_Routing_Error e return r; } -void MeshModule::callModules(meshtastic_MeshPacket &mp, RxSource src, const char *specificModule) +void MeshModule::callModules(meshtastic_MeshPacket &mp, RxSource src) { - if (specificModule) { - LOG_DEBUG("Calling specific module: %s", specificModule); - } // LOG_DEBUG("In call modules"); bool moduleFound = false; @@ -103,15 +100,11 @@ void MeshModule::callModules(meshtastic_MeshPacket &mp, RxSource src, const char // Was this message directed to us specifically? Will be false if we are sniffing someone elses packets auto ourNodeNum = nodeDB->getNodeNum(); bool toUs = isBroadcast(mp.to) || isToUs(&mp); + bool fromUs = mp.from == ourNodeNum; for (auto i = modules->begin(); i != modules->end(); ++i) { auto &pi = **i; - // If specificModule is provided, only call that specific module - if (specificModule && (!pi.name || strcmp(pi.name, specificModule) != 0)) { - continue; - } - pi.currentRequest = ∓ /// We only call modules that are interested in the packet (and the message is destined to us or we are promiscious) diff --git a/src/mesh/MeshModule.h b/src/mesh/MeshModule.h index bf735439f..eda3f8881 100644 --- a/src/mesh/MeshModule.h +++ b/src/mesh/MeshModule.h @@ -73,7 +73,7 @@ class MeshModule /** For use only by MeshService */ - static void callModules(meshtastic_MeshPacket &mp, RxSource src = RX_SRC_RADIO, const char *specificModule = nullptr); + static void callModules(meshtastic_MeshPacket &mp, RxSource src = RX_SRC_RADIO); static std::vector GetMeshModulesWithUIFrames(int startIndex); static void observeUIEvents(Observer *observer); diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index d544d0174..c8eba1b2e 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -1711,10 +1711,10 @@ bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelInde /// we updateGUI and updateGUIforNode if we think our this change is big enough for a redraw void NodeDB::updateFrom(const meshtastic_MeshPacket &mp) { - // if (mp.from == getNodeNum()) { - // LOG_DEBUG("Ignore update from self"); - // return; - // } + if (mp.from == getNodeNum()) { + LOG_DEBUG("Ignore update from self"); + return; + } if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.from) { LOG_DEBUG("Update DB node 0x%x, rx_time=%u", mp.from, mp.rx_time); diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 1f835bca7..c7e32c4a1 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -562,7 +562,7 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p) // Now that we are encrypting the packet channel should be the hash (no longer the index) p->channel = hash; if (hash < 0) { - // No suitable channel could be found for sending + // No suitable channel could be found for return meshtastic_Routing_Error_NO_CHANNEL; } crypto->encryptPacket(getFrom(p), p->id, numbytes, bytes); @@ -578,7 +578,7 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p) // Now that we are encrypting the packet channel should be the hash (no longer the index) p->channel = hash; if (hash < 0) { - // No suitable channel could be found for sending + // No suitable channel could be found for return meshtastic_Routing_Error_NO_CHANNEL; } crypto->encryptPacket(getFrom(p), p->id, numbytes, bytes); @@ -671,7 +671,7 @@ void Router::handleReceived(meshtastic_MeshPacket *p, RxSource src) mqtt->onSend(*p_encrypted, *p, p->channel); #endif } else if (p->from == nodeDB->getNodeNum() && !skipHandle) { - MeshModule::callModules(*p, src, ROUTING_MODULE); + MeshModule::callModules(*p, src); } packetPool.release(p_encrypted); // Release the encrypted packet diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index d40dcd24f..e9165e57c 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -632,10 +632,10 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo // Normal canned message selection if (runState == CANNED_MESSAGE_RUN_STATE_INACTIVE || runState == CANNED_MESSAGE_RUN_STATE_DISABLED) { } else { +#if CANNED_MESSAGE_ADD_CONFIRMATION // Show confirmation dialog before sending canned message NodeNum destNode = dest; ChannelIndex chan = channel; -#if CANNED_MESSAGE_ADD_CONFIRMATION graphics::menuHandler::showConfirmationBanner("Send message?", [this, destNode, chan, current]() { this->sendText(destNode, chan, current, false); payload = runState; @@ -991,7 +991,6 @@ int32_t CannedMessageModule::runOnce() this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; } } - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; this->currentMessageIndex = -1; this->freetext = ""; this->cursor = 0; diff --git a/src/modules/RoutingModule.cpp b/src/modules/RoutingModule.cpp index b10413cc8..e7e92c79a 100644 --- a/src/modules/RoutingModule.cpp +++ b/src/modules/RoutingModule.cpp @@ -73,7 +73,7 @@ uint8_t RoutingModule::getHopLimitForResponse(uint8_t hopStart, uint8_t hopLimit return Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit); // Use the default hop limit } -RoutingModule::RoutingModule() : ProtobufModule(ROUTING_MODULE, meshtastic_PortNum_ROUTING_APP, &meshtastic_Routing_msg) +RoutingModule::RoutingModule() : ProtobufModule("routing", meshtastic_PortNum_ROUTING_APP, &meshtastic_Routing_msg) { isPromiscuous = true; diff --git a/src/modules/RoutingModule.h b/src/modules/RoutingModule.h index 7b43a6e98..c047f6e29 100644 --- a/src/modules/RoutingModule.h +++ b/src/modules/RoutingModule.h @@ -2,8 +2,6 @@ #include "Channels.h" #include "ProtobufModule.h" -static const char *ROUTING_MODULE = "routing"; - /** * Routing module for router control messages */ From 10c683626325166605aa56a7ecbafc5a303cfd77 Mon Sep 17 00:00:00 2001 From: Tom Fifield Date: Sat, 30 Aug 2025 04:22:23 +1000 Subject: [PATCH 41/54] Can't trust RTCs to tell the time. (#7779) Further to https://github.com/meshtastic/firmware/pull/7772 , we discovered that some RTCs have hard-coded start times well in the past. This patch gives RTCs the same treatment as GPS - if the time is earlier than BUILD_EPOCH, we don't use it. Fixes #7771 Fixes #7750 --- src/gps/RTC.cpp | 26 ++++++++++++++++++++++---- src/gps/RTC.h | 2 +- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index d574c9ad0..185adacd9 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -23,7 +23,7 @@ static uint64_t zeroOffsetSecs; // GPS based time in secs since 1970 - only upda * Reads the current date and time from the RTC module and updates the system time. * @return True if the RTC was successfully read and the system time was updated, false otherwise. */ -void readFromRTC() +RTCSetResult readFromRTC() { struct timeval tv; /* btw settimeofday() is helpful here too*/ #ifdef RV3028_RTC @@ -44,8 +44,15 @@ void readFromRTC() t.tm_sec = rtc.getSecond(); tv.tv_sec = gm_mktime(&t); tv.tv_usec = 0; - uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms + +#ifdef BUILD_EPOCH + if (tv.tv_sec < BUILD_EPOCH) { + LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + return RTCSetResultInvalidTime; + } +#endif + LOG_DEBUG("Read RTC time from RV3028 getTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); timeStartMsec = now; @@ -53,6 +60,7 @@ void readFromRTC() if (currentQuality == RTCQualityNone) { currentQuality = RTCQualityDevice; } + return RTCSetResultSuccess; } #elif defined(PCF8563_RTC) if (rtc_found.address == PCF8563_RTC) { @@ -75,8 +83,15 @@ void readFromRTC() t.tm_sec = tc.second; tv.tv_sec = gm_mktime(&t); tv.tv_usec = 0; - uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms + +#ifdef BUILD_EPOCH + if (tv.tv_sec < BUILD_EPOCH) { + LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + return RTCSetResultInvalidTime; + } +#endif + LOG_DEBUG("Read RTC time from PCF8563 getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); timeStartMsec = now; @@ -84,6 +99,7 @@ void readFromRTC() if (currentQuality == RTCQualityNone) { currentQuality = RTCQualityDevice; } + return RTCSetResultSuccess; } #else if (!gettimeofday(&tv, NULL)) { @@ -92,8 +108,10 @@ void readFromRTC() LOG_DEBUG("Read RTC time as %ld", printableEpoch); timeStartMsec = now; zeroOffsetSecs = tv.tv_sec; + return RTCSetResultSuccess; } #endif + return RTCSetResultNotSet; } /** @@ -101,7 +119,7 @@ void readFromRTC() * * @param q The quality of the provided time. * @param tv A pointer to a timeval struct containing the time to potentially set the RTC to. - * @return True if the RTC was set, false otherwise. + * @return RTCSetResult * * If we haven't yet set our RTC this boot, set it from a GPS derived time */ diff --git a/src/gps/RTC.h b/src/gps/RTC.h index 96dec575b..010be6886 100644 --- a/src/gps/RTC.h +++ b/src/gps/RTC.h @@ -48,7 +48,7 @@ uint32_t getTime(bool local = false); /// Return time since 1970 in secs. If quality is RTCQualityNone return zero uint32_t getValidTime(RTCQuality minQuality, bool local = false); -void readFromRTC(); +RTCSetResult readFromRTC(); time_t gm_mktime(struct tm *tm); From 11db6d4dcc1d886f79696b797659e3ac58cb9d43 Mon Sep 17 00:00:00 2001 From: Tom Fifield Date: Sat, 30 Aug 2025 04:22:23 +1000 Subject: [PATCH 42/54] Can't trust RTCs to tell the time. (#7779) Further to https://github.com/meshtastic/firmware/pull/7772 , we discovered that some RTCs have hard-coded start times well in the past. This patch gives RTCs the same treatment as GPS - if the time is earlier than BUILD_EPOCH, we don't use it. Fixes #7771 Fixes #7750 --- src/gps/RTC.cpp | 26 ++++++++++++++++++++++---- src/gps/RTC.h | 2 +- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index d574c9ad0..185adacd9 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -23,7 +23,7 @@ static uint64_t zeroOffsetSecs; // GPS based time in secs since 1970 - only upda * Reads the current date and time from the RTC module and updates the system time. * @return True if the RTC was successfully read and the system time was updated, false otherwise. */ -void readFromRTC() +RTCSetResult readFromRTC() { struct timeval tv; /* btw settimeofday() is helpful here too*/ #ifdef RV3028_RTC @@ -44,8 +44,15 @@ void readFromRTC() t.tm_sec = rtc.getSecond(); tv.tv_sec = gm_mktime(&t); tv.tv_usec = 0; - uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms + +#ifdef BUILD_EPOCH + if (tv.tv_sec < BUILD_EPOCH) { + LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + return RTCSetResultInvalidTime; + } +#endif + LOG_DEBUG("Read RTC time from RV3028 getTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); timeStartMsec = now; @@ -53,6 +60,7 @@ void readFromRTC() if (currentQuality == RTCQualityNone) { currentQuality = RTCQualityDevice; } + return RTCSetResultSuccess; } #elif defined(PCF8563_RTC) if (rtc_found.address == PCF8563_RTC) { @@ -75,8 +83,15 @@ void readFromRTC() t.tm_sec = tc.second; tv.tv_sec = gm_mktime(&t); tv.tv_usec = 0; - uint32_t printableEpoch = tv.tv_sec; // Print lib only supports 32 bit but time_t can be 64 bit on some platforms + +#ifdef BUILD_EPOCH + if (tv.tv_sec < BUILD_EPOCH) { + LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); + return RTCSetResultInvalidTime; + } +#endif + LOG_DEBUG("Read RTC time from PCF8563 getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); timeStartMsec = now; @@ -84,6 +99,7 @@ void readFromRTC() if (currentQuality == RTCQualityNone) { currentQuality = RTCQualityDevice; } + return RTCSetResultSuccess; } #else if (!gettimeofday(&tv, NULL)) { @@ -92,8 +108,10 @@ void readFromRTC() LOG_DEBUG("Read RTC time as %ld", printableEpoch); timeStartMsec = now; zeroOffsetSecs = tv.tv_sec; + return RTCSetResultSuccess; } #endif + return RTCSetResultNotSet; } /** @@ -101,7 +119,7 @@ void readFromRTC() * * @param q The quality of the provided time. * @param tv A pointer to a timeval struct containing the time to potentially set the RTC to. - * @return True if the RTC was set, false otherwise. + * @return RTCSetResult * * If we haven't yet set our RTC this boot, set it from a GPS derived time */ diff --git a/src/gps/RTC.h b/src/gps/RTC.h index 96dec575b..010be6886 100644 --- a/src/gps/RTC.h +++ b/src/gps/RTC.h @@ -48,7 +48,7 @@ uint32_t getTime(bool local = false); /// Return time since 1970 in secs. If quality is RTCQualityNone return zero uint32_t getValidTime(RTCQuality minQuality, bool local = false); -void readFromRTC(); +RTCSetResult readFromRTC(); time_t gm_mktime(struct tm *tm); From ed394f5f9df0cc854d97ab342ab364e027f58006 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 29 Aug 2025 13:58:32 -0500 Subject: [PATCH 43/54] Update protobufs (#7784) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --- protobufs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protobufs b/protobufs index 8985852d7..4c4427c4a 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 8985852d752de3f7210f9a4a3e0923120ec438b3 +Subproject commit 4c4427c4a73c86fed7dc8632188bb8be95349d81 From b53dd2ec904a63d82a7b86d545e12b5de199ce87 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 29 Aug 2025 19:27:14 -0500 Subject: [PATCH 44/54] Automated version bumps (#7790) Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> --- bin/org.meshtastic.meshtasticd.metainfo.xml | 3 +++ debian/changelog | 7 +++++-- version.properties | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/bin/org.meshtastic.meshtasticd.metainfo.xml b/bin/org.meshtastic.meshtasticd.metainfo.xml index 1d97e2a66..bebbc285e 100644 --- a/bin/org.meshtastic.meshtasticd.metainfo.xml +++ b/bin/org.meshtastic.meshtasticd.metainfo.xml @@ -87,6 +87,9 @@ + + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.8 + https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.7 diff --git a/debian/changelog b/debian/changelog index ff59db89e..3bb0de79c 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -meshtasticd (2.7.7.0) UNRELEASED; urgency=medium +meshtasticd (2.7.8.0) UNRELEASED; urgency=medium [ Austin Lane ] * Initial packaging @@ -41,4 +41,7 @@ meshtasticd (2.7.7.0) UNRELEASED; urgency=medium * GitHub Actions Automatic version bump * GitHub Actions Automatic version bump - -- Ubuntu Thu, 28 Aug 2025 10:33:25 +0000 + [ ] + * GitHub Actions Automatic version bump + + -- Sat, 30 Aug 2025 00:26:04 +0000 diff --git a/version.properties b/version.properties index 2e5193d49..506675fa8 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ [VERSION] major = 2 minor = 7 -build = 7 +build = 8 From 4a669032dcc55012e1e60bd66f2619599b332a6f Mon Sep 17 00:00:00 2001 From: Wilson Date: Sat, 30 Aug 2025 08:37:18 +0800 Subject: [PATCH 45/54] Change user button to cancel button on meshtiny. (#7789) --- variants/nrf52840/meshtiny/platformio.ini | 10 +--------- variants/nrf52840/meshtiny/variant.h | 9 +++------ 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/variants/nrf52840/meshtiny/platformio.ini b/variants/nrf52840/meshtiny/platformio.ini index ef744a1c3..5f03f5cb2 100644 --- a/variants/nrf52840/meshtiny/platformio.ini +++ b/variants/nrf52840/meshtiny/platformio.ini @@ -4,15 +4,7 @@ extends = nrf52840_base board = meshtiny board_level = extra build_flags = ${nrf52840_base.build_flags} -Ivariants/nrf52840/meshtiny -D MESHTINY - -DGPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. - -DRADIOLIB_EXCLUDE_SX128X=1 - -DRADIOLIB_EXCLUDE_SX127X=1 - -DRADIOLIB_EXCLUDE_LR11X0=1 - -D INPUTDRIVER_ENCODER_TYPE=2 - -D INPUTDRIVER_ENCODER_UP=4 - -D INPUTDRIVER_ENCODER_DOWN=26 - -D INPUTDRIVER_ENCODER_BTN=28 - -D USE_PIN_BUZZER=PIN_BUZZER + -D USE_PIN_BUZZER -D MESHTASTIC_EXCLUDE_GPS=1 build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nrf52840/meshtiny> lib_deps = diff --git a/variants/nrf52840/meshtiny/variant.h b/variants/nrf52840/meshtiny/variant.h index 83ad4c5b9..d1139b3be 100644 --- a/variants/nrf52840/meshtiny/variant.h +++ b/variants/nrf52840/meshtiny/variant.h @@ -21,8 +21,6 @@ #define MESHTINY -// #define RAK4630 - /** Master clock frequency */ #define VARIANT_MCK (64000000ul) @@ -76,11 +74,10 @@ extern "C" { * Buttons */ -#define PIN_BUTTON1 9 +#define CANCEL_BUTTON_PIN 9 #define BUTTON_NEED_PULLUP -#define PIN_BUTTON2 12 -#define PIN_BUTTON3 24 -#define PIN_BUTTON4 25 +#define CANCEL_BUTTON_ACTIVE_LOW true +#define CANCEL_BUTTON_ACTIVE_PULLUP false /* * Analog pins From ca79760372c930b7cbf16937cff6dc9c80e0ae26 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sun, 31 Aug 2025 21:08:58 -0500 Subject: [PATCH 46/54] Add support for the RV-3028 on native Linux (#7802) --- src/configuration.h | 4 ++-- src/gps/RTC.cpp | 8 ++++---- src/main.cpp | 2 +- variants/native/portduino/platformio.ini | 5 ++++- variants/native/portduino/variant.h | 5 ++++- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/configuration.h b/src/configuration.h index 8b4fd82c7..81632c89e 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -26,10 +26,10 @@ along with this program. If not, see . #include -#ifdef RV3028_RTC +#if __has_include("Melopero_RV3028.h") #include "Melopero_RV3028.h" #endif -#ifdef PCF8563_RTC +#if __has_include("pcf8563.h") #include "pcf8563.h" #endif diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index 185adacd9..ceb79eebf 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -55,9 +55,9 @@ RTCSetResult readFromRTC() LOG_DEBUG("Read RTC time from RV3028 getTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); - timeStartMsec = now; - zeroOffsetSecs = tv.tv_sec; if (currentQuality == RTCQualityNone) { + timeStartMsec = now; + zeroOffsetSecs = tv.tv_sec; currentQuality = RTCQualityDevice; } return RTCSetResultSuccess; @@ -94,9 +94,9 @@ RTCSetResult readFromRTC() LOG_DEBUG("Read RTC time from PCF8563 getDateTime as %02d-%02d-%02d %02d:%02d:%02d (%ld)", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, printableEpoch); - timeStartMsec = now; - zeroOffsetSecs = tv.tv_sec; if (currentQuality == RTCQualityNone) { + timeStartMsec = now; + zeroOffsetSecs = tv.tv_sec; currentQuality = RTCQualityDevice; } return RTCSetResultSuccess; diff --git a/src/main.cpp b/src/main.cpp index bdabf5e37..31ce039f9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -421,7 +421,7 @@ void setup() struct timeval tv; tv.tv_sec = time(NULL); tv.tv_usec = 0; - perhapsSetRTC(RTCQualityNTP, &tv); + perhapsSetRTC(RTCQualityDevice, &tv); #endif powerMonInit(); diff --git a/variants/native/portduino/platformio.ini b/variants/native/portduino/platformio.ini index 62942a80e..c47ab8bf1 100644 --- a/variants/native/portduino/platformio.ini +++ b/variants/native/portduino/platformio.ini @@ -3,7 +3,10 @@ extends = portduino_base build_flags = ${portduino_base.build_flags} -I variants/native/portduino -I /usr/include board = cross_platform -lib_deps = ${portduino_base.lib_deps} +lib_deps = + ${portduino_base.lib_deps} + melopero/Melopero RV3028@^1.1.0 + build_src_filter = ${portduino_base.build_src_filter} [env:native] diff --git a/variants/native/portduino/variant.h b/variants/native/portduino/variant.h index ce7dbd865..a7ca865be 100644 --- a/variants/native/portduino/variant.h +++ b/variants/native/portduino/variant.h @@ -4,4 +4,7 @@ #define CANNED_MESSAGE_MODULE_ENABLE 1 #define HAS_GPS 1 #define MAX_RX_TOPHONE settingsMap[maxtophone] -#define MAX_NUM_NODES settingsMap[maxnodes] \ No newline at end of file +#define MAX_NUM_NODES settingsMap[maxnodes] + +// RAK12002 RTC Module +#define RV3028_RTC (uint8_t)0b1010010 \ No newline at end of file From 088be6bf6af84607a8131088461406f5ec8e3b11 Mon Sep 17 00:00:00 2001 From: Tom Fifield Date: Tue, 2 Sep 2025 14:22:48 +1000 Subject: [PATCH 47/54] Fix device-install.bat baud rate (master --> develop) (#7816) * Upgrade trunk (#7763) Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com> * Fix device-install.bat baud rate As reported by @gruberaaron , work to improve the 1200bps reset for esptool caused all runs of device-install.bat to use 1200bps as the baud rate. This change removes the general SET "ESPTOOL_BAUD=1200" that was causing the issues and places the baud settings for reset mode inside the conditional. Fixes https://github.com/meshtastic/firmware/issues/7172 --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com> --- .trunk/trunk.yaml | 4 ++-- bin/device-install.bat | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index a0dcf2ff5..c57c16319 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -8,8 +8,8 @@ plugins: uri: https://github.com/trunk-io/plugins lint: enabled: - - checkov@3.2.465 - - renovate@41.82.10 + - checkov@3.2.467 + - renovate@41.88.0 - prettier@3.6.2 - trufflehog@3.90.5 - yamllint@1.37.1 diff --git a/bin/device-install.bat b/bin/device-install.bat index 12bfd4f6e..93b2fcec1 100755 --- a/bin/device-install.bat +++ b/bin/device-install.bat @@ -100,7 +100,6 @@ IF NOT "!FILENAME:update=!"=="!FILENAME!" ( ) :skip-filename -SET "ESPTOOL_BAUD=1200" CALL :LOG_MESSAGE DEBUG "Determine the correct esptool command to use..." IF NOT "__%PYTHON%__"=="____" ( @@ -142,7 +141,7 @@ CALL :LOG_MESSAGE INFO "Using esptool baud: !ESPTOOL_BAUD!." IF %BPS_RESET% EQU 1 ( @REM Attempt to change mode via 1200bps Reset. - CALL :RUN_ESPTOOL !ESPTOOL_BAUD! --after no_reset read_flash_status + CALL :RUN_ESPTOOL 1200 --after no_reset read_flash_status GOTO eof ) From d66665b96e23220e8f22be8b41ca0251aab101dd Mon Sep 17 00:00:00 2001 From: Manuel <71137295+mverch67@users.noreply.github.com> Date: Mon, 1 Sep 2025 14:57:49 +0200 Subject: [PATCH 48/54] fix: T-LoRa Pager / T-Deck Pro shutdown (#7792) * power down during LS and shutdown * fix T-Deck Pro shutdown * use device specific define * slightly rephrase the power off display message --- src/Power.cpp | 48 +++++++++++++++++++++++++----------------------- src/sleep.cpp | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 23 deletions(-) diff --git a/src/Power.cpp b/src/Power.cpp index bf74f6e53..a123fe984 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -128,6 +128,7 @@ RAK9154Sensor rak9154Sensor; #ifdef HAS_PPM // note: XPOWERS_CHIP_XXX must be defined in variant.h #include +XPowersPPM *PPM = NULL; #endif #ifdef HAS_BQ27220 @@ -681,7 +682,7 @@ bool Power::setup() found = true; } else if (lipoChargerInit()) { found = true; - } else if (meshSolarInit()) { + } else if (meshSolarInit()) { found = true; } else if (analogInit()) { found = true; @@ -745,7 +746,11 @@ void Power::shutdown() #if HAS_SCREEN if (screen) { +#ifdef T_DECK_PRO + screen->showSimpleBanner("Device is powered off.\nConnect USB to start!", 0); // T-Deck Pro has no power button +#else screen->showSimpleBanner("Shutting Down...", 0); // stays on screen +#endif } #endif #if !defined(ARCH_STM32WL) @@ -763,7 +768,7 @@ void Power::shutdown() #ifdef PIN_LED3 ledOff(PIN_LED3); #endif - doDeepSleep(DELAY_FOREVER, false, true); + doDeepSleep(DELAY_FOREVER, true, true); #elif defined(ARCH_PORTDUINO) exit(EXIT_SUCCESS); #else @@ -1320,7 +1325,6 @@ bool Power::lipoInit() class LipoCharger : public HasBatteryLevel { private: - XPowersPPM *ppm = nullptr; BQ27220 *bq = nullptr; public: @@ -1329,41 +1333,41 @@ class LipoCharger : public HasBatteryLevel */ bool runOnce() { - if (ppm == nullptr) { - ppm = new XPowersPPM; - bool result = ppm->init(Wire, I2C_SDA, I2C_SCL, BQ25896_ADDR); + if (PPM == nullptr) { + PPM = new XPowersPPM; + bool result = PPM->init(Wire, I2C_SDA, I2C_SCL, BQ25896_ADDR); if (result) { LOG_INFO("PPM BQ25896 init succeeded"); // Set the minimum operating voltage. Below this voltage, the PPM will protect - // ppm->setSysPowerDownVoltage(3100); + // PPM->setSysPowerDownVoltage(3100); // Set input current limit, default is 500mA - // ppm->setInputCurrentLimit(800); + // PPM->setInputCurrentLimit(800); // Disable current limit pin - // ppm->disableCurrentLimitPin(); + // PPM->disableCurrentLimitPin(); // Set the charging target voltage, Range:3840 ~ 4608mV ,step:16 mV - ppm->setChargeTargetVoltage(4288); + PPM->setChargeTargetVoltage(4288); // Set the precharge current , Range: 64mA ~ 1024mA ,step:64mA - // ppm->setPrechargeCurr(64); + // PPM->setPrechargeCurr(64); // The premise is that limit pin is disabled, or it will // only follow the maximum charging current set by limit pin. // Set the charging current , Range:0~5056mA ,step:64mA - ppm->setChargerConstantCurr(1024); + PPM->setChargerConstantCurr(1024); // To obtain voltage data, the ADC must be enabled first - ppm->enableMeasure(); + PPM->enableMeasure(); // Turn on charging function // If there is no battery connected, do not turn on the charging function - ppm->enableCharge(); + PPM->enableCharge(); } else { LOG_WARN("PPM BQ25896 init failed"); - delete ppm; - ppm = nullptr; + delete PPM; + PPM = nullptr; return false; } } @@ -1404,23 +1408,23 @@ class LipoCharger : public HasBatteryLevel /** * return true if there is a battery installed in this unit */ - virtual bool isBatteryConnect() override { return ppm->getBattVoltage() > 0; } + virtual bool isBatteryConnect() override { return PPM->getBattVoltage() > 0; } /** * return true if there is an external power source detected */ - virtual bool isVbusIn() override { return ppm->getVbusVoltage() > 0; } + virtual bool isVbusIn() override { return PPM->getVbusVoltage() > 0; } /** * return true if the battery is currently charging */ virtual bool isCharging() override { - bool isCharging = ppm->isCharging(); + bool isCharging = PPM->isCharging(); if (isCharging) { LOG_DEBUG("BQ27220 time to full charge: %d min", bq->getTimeToFull()); } else { - if (!ppm->isVbusIn()) { + if (!PPM->isVbusIn()) { LOG_DEBUG("BQ27220 time to empty: %d min (%d mAh)", bq->getTimeToEmpty(), bq->getRemainingCapacity()); } } @@ -1453,8 +1457,6 @@ bool Power::lipoChargerInit() } #endif - - #ifdef HELTEC_MESH_SOLAR #include "meshSolarApp.h" @@ -1492,7 +1494,7 @@ class meshSolarBatteryLevel : public HasBatteryLevel /** * return true if there is an external power source detected */ - virtual bool isVbusIn() override { return meshSolarIsVbusIn();} + virtual bool isVbusIn() override { return meshSolarIsVbusIn(); } /** * return true if the battery is currently charging diff --git a/src/sleep.cpp b/src/sleep.cpp index 1a5f246c5..bff318900 100644 --- a/src/sleep.cpp +++ b/src/sleep.cpp @@ -32,6 +32,16 @@ esp_sleep_source_t wakeCause; // the reason we booted this time #endif #include "Throttle.h" +#ifdef USE_XL9555 +#include "ExtensionIOXL9555.hpp" +extern ExtensionIOXL9555 io; +#endif + +#ifdef HAS_PPM +#include +extern XPowersPPM *PPM; +#endif + #ifndef INCLUDE_vTaskSuspend #define INCLUDE_vTaskSuspend 0 #endif @@ -297,6 +307,14 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveN #endif #endif +#ifdef HAS_PPM + if (PPM) { + LOG_INFO("PMM shutdown"); + console->flush(); + PPM->shutdown(); + } +#endif + #ifdef HAS_PMU if (pmu_found && PMU) { // Obsolete comment: from back when we we used to receive lora packets while CPU was in deep sleep. @@ -412,6 +430,16 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r if (pmu_found) gpio_wakeup_enable((gpio_num_t)PMU_IRQ, GPIO_INTR_LOW_LEVEL); // pmu irq #endif + +#ifdef T_LORA_PAGER + LOG_DEBUG("power down XL9555 io"); + io.digitalWrite(EXPANDS_DRV_EN, LOW); + io.digitalWrite(EXPANDS_AMP_EN, LOW); + io.digitalWrite(EXPANDS_KB_EN, LOW); + io.digitalWrite(EXPANDS_SD_EN, LOW); + io.digitalWrite(EXPANDS_GPIO_EN, LOW); +#endif + auto res = esp_sleep_enable_gpio_wakeup(); if (res != ESP_OK) { LOG_ERROR("esp_sleep_enable_gpio_wakeup result %d", res); @@ -452,6 +480,14 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r gpio_wakeup_disable((gpio_num_t)RF95_IRQ); } #endif +#ifdef T_LORA_PAGER + LOG_DEBUG("power up XL9555 io"); + io.digitalWrite(EXPANDS_DRV_EN, HIGH); + io.digitalWrite(EXPANDS_AMP_EN, HIGH); + io.digitalWrite(EXPANDS_KB_EN, HIGH); + io.digitalWrite(EXPANDS_SD_EN, HIGH); + io.digitalWrite(EXPANDS_GPIO_EN, HIGH); +#endif esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause(); notifyLightSleepEnd.notifyObservers(cause); // Button interrupts are reattached here From 102c447fe3871fa897d8506440bb1a8a4a00b241 Mon Sep 17 00:00:00 2001 From: Onyx Clawe <58921814+OnyxClawe@users.noreply.github.com> Date: Mon, 1 Sep 2025 05:57:15 -0700 Subject: [PATCH 49/54] Update variant.h (#7520) Updated ADC, Full charge now results in 100% charge being reported instead of 95% charge Co-authored-by: OnyxtheDragon <58921814+OnyxtheDragon@users.noreply.github.com> --- variants/nrf52840/heltec_mesh_node_t114/variant.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/nrf52840/heltec_mesh_node_t114/variant.h b/variants/nrf52840/heltec_mesh_node_t114/variant.h index f4f0baf13..b71106a53 100644 --- a/variants/nrf52840/heltec_mesh_node_t114/variant.h +++ b/variants/nrf52840/heltec_mesh_node_t114/variant.h @@ -208,7 +208,7 @@ No longer populated on PCB #undef AREF_VOLTAGE #define AREF_VOLTAGE 3.0 #define VBAT_AR_INTERNAL AR_INTERNAL_3_0 -#define ADC_MULTIPLIER (4.90F) +#define ADC_MULTIPLIER (4.99F) #define HAS_RTC 0 #ifdef __cplusplus From 16d7de5989a7627cf0c9c44d7abd0686e0afe641 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 07:58:01 -0500 Subject: [PATCH 50/54] Upgrade trunk (#7804) Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com> --- .trunk/trunk.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index c57c16319..651e25b2a 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -9,14 +9,14 @@ plugins: lint: enabled: - checkov@3.2.467 - - renovate@41.88.0 + - renovate@41.90.1 - prettier@3.6.2 - trufflehog@3.90.5 - yamllint@1.37.1 - bandit@1.8.6 - trivy@0.65.0 - taplo@0.10.0 - - ruff@0.12.10 + - ruff@0.12.11 - isort@6.0.1 - markdownlint@0.45.0 - oxipng@9.1.5 From b8d7222423e52098c67e04ea8dffebcd437a28f3 Mon Sep 17 00:00:00 2001 From: Jason P Date: Tue, 2 Sep 2025 05:55:57 -0500 Subject: [PATCH 51/54] If usePreset is False, show value as Custom (#7812) --- src/DisplayFormatters.cpp | 9 ++++++++- src/DisplayFormatters.h | 3 ++- src/graphics/draw/DebugRenderer.cpp | 8 +------- src/mesh/Channels.cpp | 5 +++-- src/mesh/RadioInterface.cpp | 3 ++- 5 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/DisplayFormatters.cpp b/src/DisplayFormatters.cpp index 44bc0897b..d367aa661 100644 --- a/src/DisplayFormatters.cpp +++ b/src/DisplayFormatters.cpp @@ -1,7 +1,14 @@ #include "DisplayFormatters.h" -const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName) +const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName, + bool usePreset) { + + // If use_preset is false, always return "Custom" + if (!usePreset) { + return "Custom"; + } + switch (preset) { case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO: return useShortName ? "ShortT" : "ShortTurbo"; diff --git a/src/DisplayFormatters.h b/src/DisplayFormatters.h index f8ccfcbb6..2d7a3e8db 100644 --- a/src/DisplayFormatters.h +++ b/src/DisplayFormatters.h @@ -4,5 +4,6 @@ class DisplayFormatters { public: - static const char *getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName); + static const char *getModemPresetDisplayName(meshtastic_Config_LoRaConfig_ModemPreset preset, bool useShortName, + bool usePreset); }; diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index 446fe7863..3e4030e0f 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -263,12 +263,6 @@ void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t display->drawString(x + 1, y, "USB"); } - // auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, true); - - // display->drawString(x + SCREEN_WIDTH - display->getStringWidth(mode), y, mode); - // if (config.display.heading_bold) - // display->drawString(x + SCREEN_WIDTH - display->getStringWidth(mode) - 1, y, mode); - uint32_t currentMillis = millis(); uint32_t seconds = currentMillis / 1000; uint32_t minutes = seconds / 60; @@ -398,7 +392,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, display->drawString(nameX, getTextPositions(display)[line++], shortnameble); // === Second Row: Radio Preset === - auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false); + auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset); char regionradiopreset[25]; const char *region = myRegion ? myRegion->name : NULL; if (region != nullptr) { diff --git a/src/mesh/Channels.cpp b/src/mesh/Channels.cpp index 70e4127d8..4ef41ddfb 100644 --- a/src/mesh/Channels.cpp +++ b/src/mesh/Channels.cpp @@ -368,7 +368,7 @@ const char *Channels::getName(size_t chIndex) // Per mesh.proto spec, if bandwidth is specified we must ignore modemPreset enum, we assume that in that case // the app effed up and forgot to set channelSettings.name if (config.lora.use_preset) { - channelName = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false); + channelName = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset); } else { channelName = "Custom"; } @@ -382,7 +382,8 @@ bool Channels::isDefaultChannel(ChannelIndex chIndex) const auto &ch = getByIndex(chIndex); if (ch.settings.psk.size == 1 && ch.settings.psk.bytes[0] == 1) { const char *name = getName(chIndex); - const char *presetName = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false); + const char *presetName = + DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset); // Check if the name is the default derived from the modem preset if (strcmp(name, presetName) == 0) return true; diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index e721431b1..20a0bdbd1 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -589,7 +589,8 @@ void RadioInterface::applyModemConfig() // Check if we use the default frequency slot RadioInterface::uses_default_frequency_slot = - channel_num == hash(DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false)) % numChannels; + channel_num == + hash(DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset)) % numChannels; // Old frequency selection formula // float freq = myRegion->freqStart + ((((myRegion->freqEnd - myRegion->freqStart) / numChannels) / 2) * channel_num); From c5fad6cca1b1f68f9c47d3d9d260badbf9e7a3f9 Mon Sep 17 00:00:00 2001 From: Tom Fifield Date: Tue, 2 Sep 2025 21:05:14 +1000 Subject: [PATCH 52/54] Hold for 20s after GPS lock (#7801) * Hold for >20s after GPS lock GPS chips are designed to stay locked for a while to download some data and save it. This data is important for speeding up future locks, and making them higher quality. Our present configuration could make every lock perform similar to first lock. This patch sets a hold of between 20s and 10% of the lock search time after lock is acquired. This should allow the GPS to finish its work before we turn it off. Fixes https://github.com/meshtastic/firmware/issues/7466 * Remove T1000E-specific GPS holds The new code does the same thing, for all devices. * Fix publishing settings * Cleanups, removing unused variables. * ifdef log line with GPS_DEBUG * fixQual is not a bool. --- src/gps/GPS.cpp | 73 +++++++-------------- src/gps/GPS.h | 2 +- variants/nrf52840/tracker-t1000-e/variant.h | 3 +- variants/nrf52840/wio-t1000-s/variant.h | 3 +- 4 files changed, 25 insertions(+), 56 deletions(-) diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index 881021975..9ae7ae97d 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -843,9 +843,6 @@ void GPS::setPowerState(GPSPowerState newState, uint32_t sleepTime) setPowerPMU(true); // Power (PMU): on writePinStandby(false); // Standby (pin): awake (not standby) setPowerUBLOX(true); // Standby (UBLOX): awake -#ifdef GNSS_AIROHA - lastFixStartMsec = 0; -#endif break; case GPS_SOFTSLEEP: @@ -863,9 +860,7 @@ void GPS::setPowerState(GPSPowerState newState, uint32_t sleepTime) writePinStandby(true); // Standby (pin): asleep (not awake) setPowerUBLOX(false, sleepTime); // Standby (UBLOX): asleep, timed #ifdef GNSS_AIROHA - if (config.position.gps_update_interval * 1000 >= GPS_FIX_HOLD_TIME * 2) { - digitalWrite(PIN_GPS_EN, LOW); - } + digitalWrite(PIN_GPS_EN, LOW); #endif break; @@ -877,9 +872,7 @@ void GPS::setPowerState(GPSPowerState newState, uint32_t sleepTime) writePinStandby(true); // Standby (pin): asleep setPowerUBLOX(false, 0); // Standby (UBLOX): asleep, indefinitely #ifdef GNSS_AIROHA - if (config.position.gps_update_interval * 1000 >= GPS_FIX_HOLD_TIME * 2) { - digitalWrite(PIN_GPS_EN, LOW); - } + digitalWrite(PIN_GPS_EN, LOW); #endif break; } @@ -1062,6 +1055,8 @@ void GPS::down() } // If update interval long enough (or softsleep unsupported): hardsleep instead setPowerState(GPS_HARDSLEEP, sleepTime); + // Reset the fix quality to 0, since we're off. + fixQual = 0; } } @@ -1121,11 +1116,19 @@ int32_t GPS::runOnce() shouldPublish = true; } + uint8_t prev_fixQual = fixQual; bool gotLoc = lookForLocation(); if (gotLoc && !hasValidLocation) { // declare that we have location ASAP LOG_DEBUG("hasValidLocation RISING EDGE"); hasValidLocation = true; shouldPublish = true; + // Hold for 20secs after getting a lock to download ephemeris etc + fixHoldEnds = millis() + 20000; + } + + if (gotLoc && prev_fixQual == 0) { // just got a lock after turning back on. + fixHoldEnds = millis() + 20000; + shouldPublish = true; // Publish immediately, since next publish is at end of hold } bool tooLong = scheduling.searchedTooLong(); @@ -1134,8 +1137,7 @@ int32_t GPS::runOnce() // Once we get a location we no longer desperately want an update if ((gotLoc && gotTime) || tooLong) { - - if (tooLong) { + if (tooLong && !gotLoc) { // we didn't get a location during this ack window, therefore declare loss of lock if (hasValidLocation) { LOG_DEBUG("hasValidLocation FALLING EDGE"); @@ -1143,9 +1145,15 @@ int32_t GPS::runOnce() p = meshtastic_Position_init_default; hasValidLocation = false; } - - down(); - shouldPublish = true; // publish our update for this just finished acquisition window + if (millis() > fixHoldEnds) { + shouldPublish = true; // publish our update at the end of the lock hold + publishUpdate(); + down(); +#ifdef GPS_DEBUG + } else { + LOG_DEBUG("Holding for GPS data download: %d ms (numSats=%d)", fixHoldEnds - millis(), p.sats_in_view); +#endif + } } // If state has changed do a publish @@ -1508,24 +1516,6 @@ static int32_t toDegInt(RawDegrees d) */ bool GPS::lookForTime() { - -#ifdef GNSS_AIROHA - uint8_t fix = reader.fixQuality(); - if (fix >= 1 && fix <= 5) { - if (lastFixStartMsec > 0) { - if (Throttle::isWithinTimespanMs(lastFixStartMsec, GPS_FIX_HOLD_TIME)) { - return false; - } else { - clearBuffer(); - } - } else { - lastFixStartMsec = millis(); - return false; - } - } else { - return false; - } -#endif auto ti = reader.time; auto d = reader.date; if (ti.isValid() && d.isValid()) { // Note: we don't check for updated, because we'll only be called if needed @@ -1564,25 +1554,6 @@ The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of s */ bool GPS::lookForLocation() { -#ifdef GNSS_AIROHA - if ((config.position.gps_update_interval * 1000) >= (GPS_FIX_HOLD_TIME * 2)) { - uint8_t fix = reader.fixQuality(); - if (fix >= 1 && fix <= 5) { - if (lastFixStartMsec > 0) { - if (Throttle::isWithinTimespanMs(lastFixStartMsec, GPS_FIX_HOLD_TIME)) { - return false; - } else { - clearBuffer(); - } - } else { - lastFixStartMsec = millis(); - return false; - } - } else { - return false; - } - } -#endif // By default, TinyGPS++ does not parse GPGSA lines, which give us // the 2D/3D fixType (see NMEAGPS.h) // At a minimum, use the fixQuality indicator in GPGGA (FIXME?) diff --git a/src/gps/GPS.h b/src/gps/GPS.h index 9be57017f..177cfe74b 100644 --- a/src/gps/GPS.h +++ b/src/gps/GPS.h @@ -159,7 +159,7 @@ class GPS : private concurrency::OSThread uint8_t fixType = 0; // fix type from GPGSA #endif - uint32_t lastWakeStartMsec = 0, lastSleepStartMsec = 0, lastFixStartMsec = 0; + uint32_t fixHoldEnds = 0; uint32_t rx_gpio = 0; uint32_t tx_gpio = 0; diff --git a/variants/nrf52840/tracker-t1000-e/variant.h b/variants/nrf52840/tracker-t1000-e/variant.h index 81b4ef3fb..403552ec0 100644 --- a/variants/nrf52840/tracker-t1000-e/variant.h +++ b/variants/nrf52840/tracker-t1000-e/variant.h @@ -124,8 +124,7 @@ extern "C" { #define GPS_RTC_INT (0 + 15) // P0.15, normal is LOW, wake by HIGH #define GPS_RESETB_OUT (32 + 14) // P1.14, always input pull_up -#define GPS_FIX_HOLD_TIME 15000 // ms -#define BATTERY_PIN 2 // P0.02/AIN0, BAT_ADC +#define BATTERY_PIN 2 // P0.02/AIN0, BAT_ADC #define BATTERY_IMMUTABLE #define ADC_MULTIPLIER (2.0F) // P0.04/AIN2 is VCC_ADC, P0.05/AIN3 is CHARGER_DET, P1.03 is CHARGE_STA, P1.04 is CHARGE_DONE diff --git a/variants/nrf52840/wio-t1000-s/variant.h b/variants/nrf52840/wio-t1000-s/variant.h index eb6a34d6c..02f8a20b2 100644 --- a/variants/nrf52840/wio-t1000-s/variant.h +++ b/variants/nrf52840/wio-t1000-s/variant.h @@ -123,7 +123,6 @@ extern "C" { #define GPS_RESETB_OUT (32 + 14) // P1.14, awlays input pull_up // #define GPS_THREAD_INTERVAL 50 -#define GPS_FIX_HOLD_TIME 15000 // ms #define BATTERY_PIN 2 // #define ADC_CHANNEL ADC1_GPIO2_CHANNEL @@ -157,4 +156,4 @@ extern "C" { * Arduino objects - C++ only *----------------------------------------------------------------------------*/ -#endif // _VARIANT_WIO_SDK_WM1110_ \ No newline at end of file +#endif // _VARIANT_WIO_SDK_WM1110_ From 7612799ef667c15e8e158605dcbd4e7dcf41dc14 Mon Sep 17 00:00:00 2001 From: Tom Fifield Date: Tue, 2 Sep 2025 21:40:59 +1000 Subject: [PATCH 53/54] Fix GPS that hard code 2080 as the start time. (#7803) * Fix GPS that hard code 2080 as the start time. Some GPS chips, such as the AG3335 in T1000e and L96 have a hardcoded time of 2080-01-05 when they start up. To fix that in a way that seems permanent, let's ignore times that are more than 40 years since the firmware was built. We should followup in late 2039 to see if any changes are needed. Reported-By: @b8b8 * Update src/gps/RTC.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Put FORTY_YEARS in header and use in both places. * Restore Ben's nicer log lines. --------- Co-authored-by: Ben Meadors Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/gps/RTC.cpp | 8 ++++++++ src/gps/RTC.h | 3 +++ 2 files changed, 11 insertions(+) diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index ceb79eebf..e208e2df9 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -132,6 +132,10 @@ RTCSetResult perhapsSetRTC(RTCQuality q, const struct timeval *tv, bool forceUpd if (tv->tv_sec < BUILD_EPOCH) { LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); return RTCSetResultInvalidTime; + } else if (tv->tv_sec > (BUILD_EPOCH + FORTY_YEARS)) { + LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, BUILD_EPOCH, + BUILD_EPOCH + FORTY_YEARS); + return RTCSetResultInvalidTime; } #endif @@ -250,6 +254,10 @@ RTCSetResult perhapsSetRTC(RTCQuality q, struct tm &t) if (tv.tv_sec < BUILD_EPOCH) { LOG_WARN("Ignore time (%ld) before build epoch (%ld)!", printableEpoch, BUILD_EPOCH); return RTCSetResultInvalidTime; + } else if (tv.tv_sec > (BUILD_EPOCH + FORTY_YEARS)) { + LOG_WARN("Ignore time (%ld) too far in the future (build epoch: %ld, max allowed: %ld)!", printableEpoch, BUILD_EPOCH, + BUILD_EPOCH + FORTY_YEARS); + return RTCSetResultInvalidTime; } #endif diff --git a/src/gps/RTC.h b/src/gps/RTC.h index 010be6886..03350823c 100644 --- a/src/gps/RTC.h +++ b/src/gps/RTC.h @@ -55,3 +55,6 @@ time_t gm_mktime(struct tm *tm); #define SEC_PER_DAY 86400 #define SEC_PER_HOUR 3600 #define SEC_PER_MIN 60 +#ifdef BUILD_EPOCH +#define FORTY_YEARS (40UL * 365 * SEC_PER_DAY) // probably time to update your firmware +#endif From 0952007805e447a185ead329aceb88eb21f39d3d Mon Sep 17 00:00:00 2001 From: Chloe Bethel Date: Tue, 2 Sep 2025 13:08:57 +0100 Subject: [PATCH 54/54] Make ExternalNotification show up in excluded_modules, more STM32 modules (#7797) * Show ExternalNotification as excluded if it is * Enable ExternalNotification, SerialModule and RangeTest on STM32WL * Misc fixes for #7797 - ARCH_STM32 -> ARCH_STM32WL, use less flash by dropping weather station support for serialmodule, set tx/rx pins before begin * Enable Serial1 on RAK3172, make SerialModule use it (console is on LPUART1) * Fix SerialModule on RAK3172, fix board definition of RAK3172 to include the right pin mapping. --- boards/wiscore_rak3172.json | 2 +- src/main.cpp | 2 +- src/modules/ExternalNotificationModule.cpp | 3 ++- src/modules/Modules.cpp | 9 +++---- src/modules/RangeTestModule.cpp | 4 +-- src/modules/SerialModule.cpp | 30 +++++++++++++++++----- src/modules/SerialModule.h | 4 +-- variants/stm32/rak3172/platformio.ini | 4 +++ 8 files changed, 38 insertions(+), 20 deletions(-) diff --git a/boards/wiscore_rak3172.json b/boards/wiscore_rak3172.json index 714e09115..69ee506b4 100644 --- a/boards/wiscore_rak3172.json +++ b/boards/wiscore_rak3172.json @@ -5,7 +5,7 @@ }, "core": "stm32", "cpu": "cortex-m4", - "extra_flags": "-DSTM32WLxx -DSTM32WLE5xx -DARDUINO_GENERIC_WLE5CCUX", + "extra_flags": "-DSTM32WLxx -DSTM32WLE5xx -DARDUINO_RAK3172_MODULE", "f_cpu": "48000000L", "mcu": "stm32wle5ccu", "variant": "STM32WLxx/WL54CCU_WL55CCU_WLE4C(8-B-C)U_WLE5C(8-B-C)U", diff --git a/src/main.cpp b/src/main.cpp index 31ce039f9..73f68e95e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1531,7 +1531,7 @@ extern meshtastic_DeviceMetadata getDeviceMetadata() #if ((!HAS_SCREEN || NO_EXT_GPIO) || MESHTASTIC_EXCLUDE_CANNEDMESSAGES) && !defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS) deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_CANNEDMSG_CONFIG; #endif -#if NO_EXT_GPIO +#if NO_EXT_GPIO || MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_EXTNOTIF_CONFIG; #endif // Only edge case here is if we apply this a device with built in Accelerometer and want to detect interrupts diff --git a/src/modules/ExternalNotificationModule.cpp b/src/modules/ExternalNotificationModule.cpp index 1f871f87e..2f2934984 100644 --- a/src/modules/ExternalNotificationModule.cpp +++ b/src/modules/ExternalNotificationModule.cpp @@ -364,9 +364,10 @@ ExternalNotificationModule::ExternalNotificationModule() // moduleConfig.external_notification.alert_message_buzzer = true; if (moduleConfig.external_notification.enabled) { +#if !defined(MESHTASTIC_EXCLUDE_INPUTBROKER) if (inputBroker) // put our callback in the inputObserver list inputObserver.observe(inputBroker); - +#endif if (nodeDB->loadProto(rtttlConfigFile, meshtastic_RTTTLConfig_size, sizeof(meshtastic_RTTTLConfig), &meshtastic_RTTTLConfig_msg, &rtttlConfig) != LoadFileResult::LOAD_SUCCESS) { memset(rtttlConfig.ringtone, 0, sizeof(rtttlConfig.ringtone)); diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index 0d405fa81..b9b4dd3e5 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -88,7 +88,7 @@ #include "modules/StoreForwardModule.h" #endif #endif -#if defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_PORTDUINO) + #if !MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION #include "modules/ExternalNotificationModule.h" #endif @@ -98,7 +98,6 @@ #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !MESHTASTIC_EXCLUDE_SERIAL #include "modules/SerialModule.h" #endif -#endif #if !MESHTASTIC_EXCLUDE_DROPZONE #include "modules/DropzoneModule.h" @@ -246,8 +245,8 @@ void setupModules() #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_POWER_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR new PowerTelemetryModule(); #endif -#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ - !defined(CONFIG_IDF_TARGET_ESP32C3) +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)) && \ + !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) #if !MESHTASTIC_EXCLUDE_SERIAL if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { new SerialModule(); @@ -268,13 +267,11 @@ void setupModules() storeForwardModule = new StoreForwardModule(); #endif #endif -#if defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_PORTDUINO) #if !MESHTASTIC_EXCLUDE_EXTERNALNOTIFICATION externalNotificationModule = new ExternalNotificationModule(); #endif #if !MESHTASTIC_EXCLUDE_RANGETEST && !MESHTASTIC_EXCLUDE_GPS new RangeTestModule(); -#endif #endif } else { #if !MESHTASTIC_EXCLUDE_ADMIN diff --git a/src/modules/RangeTestModule.cpp b/src/modules/RangeTestModule.cpp index 6f3d69acf..d1d2d9ead 100644 --- a/src/modules/RangeTestModule.cpp +++ b/src/modules/RangeTestModule.cpp @@ -31,7 +31,7 @@ uint32_t packetSequence = 0; int32_t RangeTestModule::runOnce() { -#if defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_PORTDUINO) +#if defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_STM32WL) || defined(ARCH_PORTDUINO) /* Uncomment the preferences below if you want to use the module @@ -130,7 +130,7 @@ void RangeTestModuleRadio::sendPayload(NodeNum dest, bool wantReplies) ProcessMessage RangeTestModuleRadio::handleReceived(const meshtastic_MeshPacket &mp) { -#if defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_PORTDUINO) +#if defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_STM32WL) || defined(ARCH_PORTDUINO) if (moduleConfig.range_test.enabled) { diff --git a/src/modules/SerialModule.cpp b/src/modules/SerialModule.cpp index 880768839..7485f1c2d 100644 --- a/src/modules/SerialModule.cpp +++ b/src/modules/SerialModule.cpp @@ -49,8 +49,8 @@ #include "meshSolarApp.h" #endif -#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ - !defined(CONFIG_IDF_TARGET_ESP32C3) +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)) && \ + !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) #define RX_BUFFER 256 #define TIMEOUT 250 @@ -67,7 +67,7 @@ SerialModuleRadio *serialModuleRadio; defined(ELECROW_ThinkNode_M5) || defined(HELTEC_MESH_SOLAR) || defined(T_ECHO_LITE) SerialModule::SerialModule() : StreamAPI(&Serial), concurrency::OSThread("Serial") {} static Print *serialPrint = &Serial; -#elif defined(CONFIG_IDF_TARGET_ESP32C6) +#elif defined(CONFIG_IDF_TARGET_ESP32C6) || defined(RAK3172) SerialModule::SerialModule() : StreamAPI(&Serial1), concurrency::OSThread("Serial") {} static Print *serialPrint = &Serial1; #else @@ -173,7 +173,18 @@ int32_t SerialModule::runOnce() Serial.begin(baud); Serial.setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); } - +#elif defined(ARCH_STM32WL) +#ifndef RAK3172 + HardwareSerial *serialInstance = &Serial2; +#else + HardwareSerial *serialInstance = &Serial1; +#endif + if (moduleConfig.serial.rxd && moduleConfig.serial.txd) { + serialInstance->setTx(moduleConfig.serial.txd); + serialInstance->setRx(moduleConfig.serial.rxd); + } + serialInstance->begin(baud); + serialInstance->setTimeout(moduleConfig.serial.timeout > 0 ? moduleConfig.serial.timeout : TIMEOUT); #elif defined(ARCH_ESP32) if (moduleConfig.serial.rxd && moduleConfig.serial.txd) { @@ -260,8 +271,13 @@ int32_t SerialModule::runOnce() while (Serial1.available()) { serialPayloadSize = Serial1.readBytes(serialBytes, meshtastic_Constants_DATA_PAYLOAD_LEN); #else - while (Serial2.available()) { - serialPayloadSize = Serial2.readBytes(serialBytes, meshtastic_Constants_DATA_PAYLOAD_LEN); +#ifndef RAK3172 + HardwareSerial *serialInstance = &Serial2; +#else + HardwareSerial *serialInstance = &Serial1; +#endif + while (serialInstance->available()) { + serialPayloadSize = serialInstance->readBytes(serialBytes, meshtastic_Constants_DATA_PAYLOAD_LEN); #endif serialModuleRadio->sendPayload(); } @@ -511,7 +527,7 @@ ParsedLine parseLine(const char *line) void SerialModule::processWXSerial() { #if !defined(TTGO_T_ECHO) && !defined(T_ECHO_LITE) && !defined(CANARYONE) && !defined(CONFIG_IDF_TARGET_ESP32C6) && \ - !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M5) + !defined(MESHLINK) && !defined(ELECROW_ThinkNode_M1) && !defined(ELECROW_ThinkNode_M5) && !defined(ARCH_STM32WL) static unsigned int lastAveraged = 0; static unsigned int averageIntervalMillis = 300000; // 5 minutes hard coded. static double dir_sum_sin = 0; diff --git a/src/modules/SerialModule.h b/src/modules/SerialModule.h index 1c74c927c..dbe4f75db 100644 --- a/src/modules/SerialModule.h +++ b/src/modules/SerialModule.h @@ -8,8 +8,8 @@ #include #include -#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ - !defined(CONFIG_IDF_TARGET_ESP32C3) +#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)) && \ + !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) class SerialModule : public StreamAPI, private concurrency::OSThread { diff --git a/variants/stm32/rak3172/platformio.ini b/variants/stm32/rak3172/platformio.ini index 4f9edbb92..a12b9f21c 100644 --- a/variants/stm32/rak3172/platformio.ini +++ b/variants/stm32/rak3172/platformio.ini @@ -6,6 +6,10 @@ board_upload.maximum_size = 233472 ; reserve the last 28KB for filesystem build_flags = ${stm32_base.build_flags} -Ivariants/stm32/rak3172 + -DRAK3172 + -DENABLE_HWSERIAL1 + -DPIN_SERIAL1_RX=PB7 + -DPIN_SERIAL1_TX=PB6 -DPIN_WIRE_SDA=PA11 -DPIN_WIRE_SCL=PA12 -DMESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1