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