diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 8d5635f..cf5e210 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -34,6 +34,7 @@ along with this program. If not, see . #include "draw/DebugRenderer.h" #include "draw/MenuHandler.h" #include "draw/MessageRenderer.h" +#include "draw/MessageHistory.h" // CicadaTAK Phase 67 #include "draw/NodeListRenderer.h" #include "draw/NotificationRenderer.h" #include "draw/UIRenderer.h" @@ -1367,10 +1368,30 @@ int Screen::handleInputEvent(const InputEvent *event) // If no modules are using the input, move between frames if (!inputIntercepted) { - if (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_ALT_PRESS) { - showPrevFrame(); - } else if (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS) { - showNextFrame(); + // CicadaTAK patch: accept UP and DOWN as synonyms for LEFT and RIGHT so a + // rotary encoder mapped to UP/DOWN events navigates screen frames bidirectionally + // the same way it navigates the canned-message list. + // + // Phase 67 addition: when on the text-message frame, the same encoder rotation + // first tries to scroll through the message history (last N received messages). + // If we're already at the boundary of the history (oldest going prev, newest + // going next), we fall through to ordinary frame navigation. This gives + // intuitive "scroll history, then next frame" behavior on a single axis. + const bool onTextMsgFrame = (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage); + if (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_UP || + event->inputEvent == INPUT_BROKER_ALT_PRESS) { + if (onTextMsgFrame && graphics::MessageHistory::scrollOlder()) { + setFastFramerate(); // force immediate OLED redraw — bare forceDisplay() is a no-op on non-eink + } else { + showPrevFrame(); + } + } else if (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_DOWN || + event->inputEvent == INPUT_BROKER_USER_PRESS) { + if (onTextMsgFrame && graphics::MessageHistory::scrollNewer()) { + setFastFramerate(); // force immediate OLED redraw + } else { + showNextFrame(); + } } else if (event->inputEvent == INPUT_BROKER_SELECT) { if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) { menuHandler::homeBaseMenu(); diff --git a/src/graphics/draw/MessageRenderer.cpp b/src/graphics/draw/MessageRenderer.cpp index 1178291..f6d9e2f 100644 --- a/src/graphics/draw/MessageRenderer.cpp +++ b/src/graphics/draw/MessageRenderer.cpp @@ -24,6 +24,7 @@ along with this program. If not, see . #include "configuration.h" #if HAS_SCREEN #include "MessageRenderer.h" +#include "MessageHistory.h" // CicadaTAK Phase 67 // Core includes #include "NodeDB.h" @@ -175,7 +176,12 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 // Clear the unread message indicator when viewing the message hasUnreadMessage = false; - const meshtastic_MeshPacket &mp = devicestate.rx_text_message; + // CicadaTAK Phase 67: read from the history ring buffer (MAX 10) so the user + // can scroll back through recent messages with UP/DOWN. Falls back to the + // legacy devicestate.rx_text_message if the buffer is empty (which it would + // be on a fresh boot before any message arrives). + const meshtastic_MeshPacket *historyMp = graphics::MessageHistory::current(); + const meshtastic_MeshPacket &mp = historyMp ? *historyMp : devicestate.rx_text_message; const char *msg = reinterpret_cast(mp.decoded.payload.bytes); display->clear(); @@ -191,7 +197,16 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 bool isBold = config.display.heading_bold; // === Set Title + // CicadaTAK Phase 67: append "N/M" history position if more than one message stored + char titleBuf[24]; const char *titleStr = "Messages"; + if (graphics::MessageHistory::count() > 1) { + // Display 1-indexed: 1/N = newest, N/N = oldest in buffer + snprintf(titleBuf, sizeof(titleBuf), "Messages %d/%d", + graphics::MessageHistory::currentOffset() + 1, + graphics::MessageHistory::count()); + titleStr = titleBuf; + } // Check if we have more than an empty message to show char messageBuf[237]; diff --git a/src/main.cpp b/src/main.cpp index 9e46021..c9b752c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1051,7 +1051,10 @@ void setup() BaseType_t higherWake = 0; mainDelay.interruptFromISR(&higherWake); }; - userConfig.singlePress = INPUT_BROKER_USER_PRESS; + // CicadaTAK: short press launches canned messages (was USER_PRESS). + // Long press still SELECT for menu access. Suits rotary-encoder UX + // where CW/CCW already handle "next/prev" so USER_PRESS isn't needed. + userConfig.singlePress = INPUT_BROKER_ALT_LONG; userConfig.longPress = INPUT_BROKER_SELECT; userConfig.longPressTime = 500; userConfig.longLongPress = INPUT_BROKER_SHUTDOWN; diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index b6cb1b0..1fdeb3c 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -354,13 +354,16 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) if (isSelect) { return 0; // Main button press no longer runs through powerFSM } - // Let LEFT/RIGHT pass through so frame navigation works - if (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_RIGHT) { + // CicadaTAK patch (Path 1): pass UP/DOWN through to frame navigation too + // (just like LEFT/RIGHT already do), so a rotary encoder mapped to UP/DOWN + // doesn't get hijacked into launching canned messages from every screen. + // Only ALT_LONG (long-press of the encoder button) launches the list now. + if (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_RIGHT || + event->inputEvent == INPUT_BROKER_UP || event->inputEvent == INPUT_BROKER_DOWN) { break; } - // Handle UP/DOWN: activate canned message list! - if (event->inputEvent == INPUT_BROKER_UP || event->inputEvent == INPUT_BROKER_DOWN || - event->inputEvent == INPUT_BROKER_ALT_LONG) { + // Long-press launches the canned message list + if (event->inputEvent == INPUT_BROKER_ALT_LONG) { LaunchWithDestination(NODENUM_BROADCAST); return 1; } @@ -392,14 +395,17 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) bool CannedMessageModule::isUpEvent(const InputEvent *event) { - return event->inputEvent == INPUT_BROKER_UP || + // CicadaTAK patch: accept LEFT as a synonym for UP so a rotary encoder mapped to + // LEFT/RIGHT events navigates the canned-message list bidirectionally. + return event->inputEvent == INPUT_BROKER_UP || event->inputEvent == INPUT_BROKER_LEFT || ((runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER || runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) && event->inputEvent == INPUT_BROKER_ALT_PRESS); } bool CannedMessageModule::isDownEvent(const InputEvent *event) { - return event->inputEvent == INPUT_BROKER_DOWN || + // CicadaTAK patch: accept RIGHT as a synonym for DOWN. + return event->inputEvent == INPUT_BROKER_DOWN || event->inputEvent == INPUT_BROKER_RIGHT || ((runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER || runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) && event->inputEvent == INPUT_BROKER_USER_PRESS); @@ -583,11 +589,15 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo bool handled = false; - // Handle up/down navigation - if (isUp && messagesCount > 0) { + // Handle up/down navigation — but only if the module is already active. + // CicadaTAK patch: previously, isUp/isDown from any screen would activate the + // module here, defeating Path 1's INACTIVE pass-through. Require the user to + // explicitly enter the canned-message list (ALT_LONG long-press) before + // up/down scrolls within it. + if (isUp && messagesCount > 0 && runState != CANNED_MESSAGE_RUN_STATE_INACTIVE) { runState = CANNED_MESSAGE_RUN_STATE_ACTION_UP; handled = true; - } else if (isDown && messagesCount > 0) { + } else if (isDown && messagesCount > 0 && runState != CANNED_MESSAGE_RUN_STATE_INACTIVE) { runState = CANNED_MESSAGE_RUN_STATE_ACTION_DOWN; handled = true; } else if (isSelect) { diff --git a/src/modules/TextMessageModule.cpp b/src/modules/TextMessageModule.cpp index 72df330..d2e79dc 100644 --- a/src/modules/TextMessageModule.cpp +++ b/src/modules/TextMessageModule.cpp @@ -5,6 +5,7 @@ #include "buzz.h" #include "configuration.h" #include "graphics/Screen.h" +#include "graphics/draw/MessageHistory.h" // CicadaTAK Phase 67: ring buffer of last N text messages TextMessageModule *textMessageModule; ProcessMessage TextMessageModule::handleReceived(const meshtastic_MeshPacket &mp) @@ -17,6 +18,9 @@ ProcessMessage TextMessageModule::handleReceived(const meshtastic_MeshPacket &mp // Keep a copy of the most recent text message. devicestate.rx_text_message = mp; devicestate.has_rx_text_message = true; + // CicadaTAK Phase 67: also push into the history ring buffer so the + // standalone OLED text-message frame can scroll through the last N. + graphics::MessageHistory::record(mp); // Only trigger screen wake if configuration allows it if (shouldWakeOnReceivedMessage()) { diff --git a/src/graphics/draw/MessageHistory.h b/src/graphics/draw/MessageHistory.h new file mode 100644 index 0000000..733ffe3 --- /dev/null +++ b/src/graphics/draw/MessageHistory.h @@ -0,0 +1,51 @@ +// CicadaTAK addition (Phase 67) — ring buffer of recent received text messages. +// +// Stock Meshtastic stores ONE incoming text message in +// devicestate.rx_text_message; each new message overwrites the previous. +// This buffer keeps the last MAX messages so the standalone OLED text-message +// frame can scroll through history instead of only showing the newest. +// +// Memory: MAX × sizeof(meshtastic_MeshPacket) ≈ 10 × 381 bytes = ~3.8 KB. + +#pragma once + +#include "mesh/generated/meshtastic/mesh.pb.h" + +namespace graphics +{ +namespace MessageHistory +{ + +constexpr int MAX = 10; + +// Push a newly received text message onto the ring buffer. Caller is +// expected to be TextMessageModule::handleReceived after it stores into +// devicestate.rx_text_message. +void record(const meshtastic_MeshPacket &mp); + +// Currently-visible message (governed by scroll offset). nullptr if buffer +// is empty. +const meshtastic_MeshPacket *current(); + +// Number of messages stored (0..MAX). +int count(); + +// Current scroll offset. 0 = newest, count()-1 = oldest. +int currentOffset(); + +// Move scroll position one step toward older. Returns true if scrolled, +// false if already at the oldest end. +bool scrollOlder(); + +// Move scroll position one step toward newer. Returns true if scrolled, +// false if already at the newest. +bool scrollNewer(); + +// Snap scroll position back to newest (called e.g. when the frame is +// freshly entered). Currently unused — record() does NOT auto-reset so +// that a user reading an older message isn't yanked to newest by an +// incoming message. +void resetToNewest(); + +} // namespace MessageHistory +} // namespace graphics diff --git a/src/graphics/draw/MessageHistory.cpp b/src/graphics/draw/MessageHistory.cpp new file mode 100644 index 0000000..7dc54dd --- /dev/null +++ b/src/graphics/draw/MessageHistory.cpp @@ -0,0 +1,78 @@ +// CicadaTAK addition (Phase 67) — implementation. See MessageHistory.h. + +#include "MessageHistory.h" + +#include + +namespace graphics +{ +namespace MessageHistory +{ + +static meshtastic_MeshPacket buffer[MAX]; +static int head = 0; // next write position +static int stored = 0; // count of valid messages in buffer (0..MAX) +static int currentOff = 0; // scroll offset; 0 = newest + +void record(const meshtastic_MeshPacket &mp) +{ + buffer[head] = mp; + head = (head + 1) % MAX; + if (stored < MAX) { + stored++; + } + // Deliberately NOT resetting currentOff here — if the user is reading + // an older message, an incoming new one shouldn't yank them to newest. + // If they were already on newest (offset 0), they stay on newest, which + // now points to the just-arrived message because head moved. + // If they were on offset N, that physical slot still holds the same + // message they were reading until N more messages arrive and rotate it + // out of the buffer. +} + +const meshtastic_MeshPacket *current() +{ + if (stored == 0) { + return nullptr; + } + // head points to NEXT write. Newest is at head-1. Offset N is N steps + // older. + int idx = (head - 1 - currentOff + MAX) % MAX; + return &buffer[idx]; +} + +int count() +{ + return stored; +} + +int currentOffset() +{ + return currentOff; +} + +bool scrollOlder() +{ + if (currentOff >= stored - 1) { + return false; + } + currentOff++; + return true; +} + +bool scrollNewer() +{ + if (currentOff <= 0) { + return false; + } + currentOff--; + return true; +} + +void resetToNewest() +{ + currentOff = 0; +} + +} // namespace MessageHistory +} // namespace graphics