diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 154481f..82e55d5 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -13,6 +13,7 @@ static bool composeMode = false; static char composeBuffer[138]; // 137 chars max + null terminator static int composePos = 0; + static uint8_t composeChannelIdx = 0; // Which channel to send to void initKeyboard(); void handleKeyboardInput(); @@ -402,7 +403,7 @@ void handleKeyboardInput() { if (key == '\b') { // Backspace - check if shift was recently pressed for cancel combo if (keyboard.wasShiftRecentlyPressed(500)) { - // Shift+Backspace = Cancel + // Shift+Backspace = Cancel (works anytime) Serial.println("Compose: Shift+Backspace, cancelling..."); composeMode = false; composeBuffer[0] = '\0'; @@ -420,6 +421,40 @@ void handleKeyboardInput() { return; } + // A/D keys switch channels (only when buffer is empty or as special function) + if ((key == 'a' || key == 'A') && composePos == 0) { + // Previous channel + if (composeChannelIdx > 0) { + composeChannelIdx--; + } else { + // Wrap to last valid channel + for (uint8_t i = MAX_GROUP_CHANNELS - 1; i > 0; i--) { + ChannelDetails ch; + if (the_mesh.getChannel(i, ch) && ch.name[0] != '\0') { + composeChannelIdx = i; + break; + } + } + } + Serial.printf("Compose: Channel switched to %d\n", composeChannelIdx); + drawComposeScreen(); + return; + } + + if ((key == 'd' || key == 'D') && composePos == 0) { + // Next channel + ChannelDetails ch; + uint8_t nextIdx = composeChannelIdx + 1; + if (the_mesh.getChannel(nextIdx, ch) && ch.name[0] != '\0') { + composeChannelIdx = nextIdx; + } else { + composeChannelIdx = 0; // Wrap to first channel + } + Serial.printf("Compose: Channel switched to %d\n", composeChannelIdx); + drawComposeScreen(); + return; + } + // Regular character input if (key >= 32 && key < 127 && composePos < 137) { composeBuffer[composePos++] = key; @@ -438,7 +473,11 @@ void handleKeyboardInput() { composeMode = true; composeBuffer[0] = '\0'; composePos = 0; - Serial.println("Entering compose mode"); + // If on channel screen, sync compose channel with viewed channel + if (ui_task.isOnChannelScreen()) { + composeChannelIdx = ui_task.getChannelScreenViewIdx(); + } + Serial.printf("Entering compose mode, channel %d\n", composeChannelIdx); drawComposeScreen(); break; @@ -451,20 +490,46 @@ void handleKeyboardInput() { case 'w': case 'W': - case 'a': - case 'A': - // Navigate left/previous - Serial.println("Nav: Previous"); - ui_task.injectKey(0xF2); // KEY_PREV + // Navigate up/previous (scroll on channel screen) + if (ui_task.isOnChannelScreen()) { + ui_task.injectKey('w'); // Pass directly for channel switching + } else { + Serial.println("Nav: Previous"); + ui_task.injectKey(0xF2); // KEY_PREV + } break; - + case 's': case 'S': + // Navigate down/next (scroll on channel screen) + if (ui_task.isOnChannelScreen()) { + ui_task.injectKey('s'); // Pass directly for channel switching + } else { + Serial.println("Nav: Next"); + ui_task.injectKey(0xF1); // KEY_NEXT + } + break; + + case 'a': + case 'A': + // Navigate left or switch channel (on channel screen) + if (ui_task.isOnChannelScreen()) { + ui_task.injectKey('a'); // Pass directly for channel switching + } else { + Serial.println("Nav: Previous"); + ui_task.injectKey(0xF2); // KEY_PREV + } + break; + case 'd': case 'D': - // Navigate right/next - Serial.println("Nav: Next"); - ui_task.injectKey(0xF1); // KEY_NEXT + // Navigate right or switch channel (on channel screen) + if (ui_task.isOnChannelScreen()) { + ui_task.injectKey('d'); // Pass directly for channel switching + } else { + Serial.println("Nav: Next"); + ui_task.injectKey(0xF1); // KEY_NEXT + } break; case '\r': @@ -499,7 +564,16 @@ void drawComposeScreen() { display.setTextSize(1); display.setColor(DisplayDriver::GREEN); display.setCursor(0, 0); - display.print("Compose - Public Channel"); + + // Get the channel name for display + ChannelDetails channel; + char headerBuf[40]; + if (the_mesh.getChannel(composeChannelIdx, channel)) { + snprintf(headerBuf, sizeof(headerBuf), "To: %s", channel.name); + } else { + snprintf(headerBuf, sizeof(headerBuf), "To: Channel %d", composeChannelIdx); + } + display.print(headerBuf); display.setColor(DisplayDriver::LIGHT); display.drawRect(0, 11, display.width(), 1); @@ -534,9 +608,21 @@ void drawComposeScreen() { display.setCursor(0, statusY); display.setColor(DisplayDriver::YELLOW); - char status[50]; - sprintf(status, "%d/137 Enter:Send Sh+Del:Cancel", composePos); - display.print(status); + char status[40]; + if (composePos == 0) { + // Empty buffer - show channel switching hint + display.print("A/D:Ch"); + sprintf(status, "Sh+Del:X"); + display.setCursor(display.width() - display.getTextWidth(status) - 2, statusY); + display.print(status); + } else { + // Has text - show send/cancel hint + sprintf(status, "%d/137 Ent:Send", composePos); + display.print(status); + sprintf(status, "Sh+Del:X"); + display.setCursor(display.width() - display.getTextWidth(status) - 2, statusY); + display.print(status); + } display.endFrame(); #endif @@ -545,25 +631,25 @@ void drawComposeScreen() { void sendComposedMessage() { if (composePos == 0) return; - MESH_DEBUG_PRINTLN("Sending message: %s", composeBuffer); + MESH_DEBUG_PRINTLN("Sending message to channel %d: %s", composeChannelIdx, composeBuffer); - // Get the Public channel (index 0) + // Get the selected channel ChannelDetails channel; - if (the_mesh.getChannel(0, channel)) { + if (the_mesh.getChannel(composeChannelIdx, channel)) { uint32_t timestamp = rtc_clock.getCurrentTime(); // Send to channel if (the_mesh.sendGroupMessage(timestamp, channel.channel, the_mesh.getNodePrefs()->node_name, composeBuffer, composePos)) { - MESH_DEBUG_PRINTLN("Message sent to Public channel"); + MESH_DEBUG_PRINTLN("Message sent to channel %s", channel.name); ui_task.showAlert("Sent!", 1500); } else { MESH_DEBUG_PRINTLN("Failed to send message"); ui_task.showAlert("Send failed!", 1500); } } else { - MESH_DEBUG_PRINTLN("Could not get Public channel"); + MESH_DEBUG_PRINTLN("Could not get channel %d", composeChannelIdx); ui_task.showAlert("No channel!", 1500); } } diff --git a/examples/companion_radio/ui-new/ChannelScreen.h b/examples/companion_radio/ui-new/ChannelScreen.h index 81eafa3..7562032 100644 --- a/examples/companion_radio/ui-new/ChannelScreen.h +++ b/examples/companion_radio/ui-new/ChannelScreen.h @@ -2,19 +2,27 @@ #include #include +#include #include // Maximum messages to store in history #define CHANNEL_MSG_HISTORY_SIZE 20 #define CHANNEL_MSG_TEXT_LEN 160 +#ifndef MAX_GROUP_CHANNELS + #define MAX_GROUP_CHANNELS 20 +#endif + class UITask; // Forward declaration +class MyMesh; // Forward declaration +extern MyMesh the_mesh; class ChannelScreen : public UIScreen { public: struct ChannelMessage { uint32_t timestamp; uint8_t path_len; + uint8_t channel_idx; // Which channel this message belongs to char text[CHANNEL_MSG_TEXT_LEN]; bool valid; }; @@ -28,10 +36,12 @@ private: int _newestIdx; // Index of newest message (circular buffer) int _scrollPos; // Current scroll position (0 = newest) int _msgsPerPage; // Messages that fit on screen + uint8_t _viewChannelIdx; // Which channel we're currently viewing public: ChannelScreen(UITask* task, mesh::RTCClock* rtc) - : _task(task), _rtc(rtc), _msgCount(0), _newestIdx(-1), _scrollPos(0), _msgsPerPage(3) { + : _task(task), _rtc(rtc), _msgCount(0), _newestIdx(-1), _scrollPos(0), + _msgsPerPage(3), _viewChannelIdx(0) { // Initialize all messages as invalid for (int i = 0; i < CHANNEL_MSG_HISTORY_SIZE; i++) { _messages[i].valid = false; @@ -39,13 +49,14 @@ public: } // Add a new message to the history - void addMessage(uint8_t path_len, const char* sender, const char* text) { + void addMessage(uint8_t channel_idx, uint8_t path_len, const char* sender, const char* text) { // Move to next slot in circular buffer _newestIdx = (_newestIdx + 1) % CHANNEL_MSG_HISTORY_SIZE; ChannelMessage* msg = &_messages[_newestIdx]; msg->timestamp = _rtc->getCurrentTime(); msg->path_len = path_len; + msg->channel_idx = channel_idx; msg->valid = true; // The text already contains "Sender: message" format, just store it @@ -60,52 +71,84 @@ public: _scrollPos = 0; } + // Get count of messages for the currently viewed channel + int getMessageCountForChannel() const { + int count = 0; + for (int i = 0; i < CHANNEL_MSG_HISTORY_SIZE; i++) { + if (_messages[i].valid && _messages[i].channel_idx == _viewChannelIdx) { + count++; + } + } + return count; + } + int getMessageCount() const { return _msgCount; } + + uint8_t getViewChannelIdx() const { return _viewChannelIdx; } + void setViewChannelIdx(uint8_t idx) { _viewChannelIdx = idx; _scrollPos = 0; } int render(DisplayDriver& display) override { - char tmp[32]; + char tmp[40]; - // Header + // Header - show current channel name display.setCursor(0, 0); display.setTextSize(1); display.setColor(DisplayDriver::GREEN); - display.print("Public Channel"); - // Message count on right - sprintf(tmp, "[%d]", _msgCount); + // Get channel name + ChannelDetails channel; + if (the_mesh.getChannel(_viewChannelIdx, channel)) { + display.print(channel.name); + } else { + sprintf(tmp, "Channel %d", _viewChannelIdx); + display.print(tmp); + } + + // Message count for this channel on right + int channelMsgCount = getMessageCountForChannel(); + sprintf(tmp, "[%d]", channelMsgCount); display.setCursor(display.width() - display.getTextWidth(tmp) - 2, 0); display.print(tmp); // Divider line display.drawRect(0, 11, display.width(), 1); - if (_msgCount == 0) { + if (channelMsgCount == 0) { display.setCursor(0, 25); display.setColor(DisplayDriver::LIGHT); display.print("No messages yet"); display.setCursor(0, 40); - display.print("Press C to compose"); + display.print("A/D: Switch channel"); + display.setCursor(0, 52); + display.print("C: Compose message"); } else { int lineHeight = 10; int headerHeight = 14; int footerHeight = 14; - int availableHeight = display.height() - headerHeight - footerHeight; // Calculate chars per line based on display width int charsPerLine = display.width() / 6; int y = headerHeight; - // Display messages from scroll position - int msgsDrawn = 0; - for (int i = 0; i + _scrollPos < _msgCount && y < display.height() - footerHeight - lineHeight; i++) { - // Calculate index in circular buffer - int idx = _newestIdx - _scrollPos - i; + // Build list of messages for this channel (newest first) + int channelMsgs[CHANNEL_MSG_HISTORY_SIZE]; + int numChannelMsgs = 0; + + for (int i = 0; i < _msgCount && numChannelMsgs < CHANNEL_MSG_HISTORY_SIZE; i++) { + int idx = _newestIdx - i; while (idx < 0) idx += CHANNEL_MSG_HISTORY_SIZE; idx = idx % CHANNEL_MSG_HISTORY_SIZE; - if (!_messages[idx].valid) continue; - + if (_messages[idx].valid && _messages[idx].channel_idx == _viewChannelIdx) { + channelMsgs[numChannelMsgs++] = idx; + } + } + + // Display messages from scroll position + int msgsDrawn = 0; + for (int i = _scrollPos; i < numChannelMsgs && y < display.height() - footerHeight - lineHeight; i++) { + int idx = channelMsgs[i]; ChannelMessage* msg = &_messages[idx]; // Time indicator with hop count @@ -125,23 +168,21 @@ public: display.print(tmp); y += lineHeight; - // Message text with word wrap - the text already contains "Sender: message" + // Message text with word wrap display.setColor(DisplayDriver::LIGHT); int textLen = strlen(msg->text); int pos = 0; int linesForThisMsg = 0; - int maxLinesPerMsg = 3; // Allow up to 3 lines per message + int maxLinesPerMsg = 3; while (pos < textLen && linesForThisMsg < maxLinesPerMsg && y < display.height() - footerHeight - 2) { display.setCursor(0, y); - // Find how much text fits on this line int lineEnd = pos + charsPerLine; if (lineEnd >= textLen) { lineEnd = textLen; } else { - // Try to break at a space int lastSpace = -1; for (int j = pos; j < lineEnd && j < textLen; j++) { if (msg->text[j] == ' ') lastSpace = j; @@ -149,7 +190,6 @@ public: if (lastSpace > pos) lineEnd = lastSpace; } - // Print this line segment char lineBuf[42]; int lineLen = lineEnd - pos; if (lineLen > 40) lineLen = 40; @@ -158,66 +198,51 @@ public: display.print(lineBuf); pos = lineEnd; - // Skip space at start of next line while (pos < textLen && msg->text[pos] == ' ') pos++; y += lineHeight; linesForThisMsg++; } - // If we truncated, show ellipsis indicator - if (pos < textLen && linesForThisMsg >= maxLinesPerMsg) { - // Message was truncated - could add "..." but space is tight - } - - y += 2; // Small gap between messages + y += 2; msgsDrawn++; _msgsPerPage = msgsDrawn; } } - // Footer with scroll indicator and controls + // Footer with controls int footerY = display.height() - 12; display.drawRect(0, footerY - 2, display.width(), 1); display.setCursor(0, footerY); display.setColor(DisplayDriver::YELLOW); - // Left side: Q:Exit - display.print("Q:Exit"); + // Left side: Q:Back A/D:Ch + display.print("Q:Back A/D:Ch"); - // Middle: scroll position indicator - if (_msgCount > _msgsPerPage) { - int endMsg = _scrollPos + _msgsPerPage; - if (endMsg > _msgCount) endMsg = _msgCount; - sprintf(tmp, "%d-%d/%d", _scrollPos + 1, endMsg, _msgCount); - // Center it roughly - int midX = display.width() / 2 - 15; - display.setCursor(midX, footerY); - display.print(tmp); - } - - // Right side: controls - sprintf(tmp, "W/S:Scrl C:New"); - display.setCursor(display.width() - display.getTextWidth(tmp) - 2, footerY); - display.print(tmp); + // Right side: C:New + const char* rightText = "C:New"; + display.setCursor(display.width() - display.getTextWidth(rightText) - 2, footerY); + display.print(rightText); #if AUTO_OFF_MILLIS == 0 // e-ink - return 5000; // Refresh every 5s + return 5000; #else - return 1000; // Refresh every 1s for time updates + return 1000; #endif } bool handleInput(char c) override { - // KEY_PREV (0xF2) or 'w' - scroll up (older messages) + int channelMsgCount = getMessageCountForChannel(); + + // W or KEY_PREV - scroll up (older messages) if (c == 0xF2 || c == 'w' || c == 'W') { - if (_scrollPos + _msgsPerPage < _msgCount) { + if (_scrollPos + _msgsPerPage < channelMsgCount) { _scrollPos++; return true; } } - // KEY_NEXT (0xF1) or 's' - scroll down (newer messages) + // S or KEY_NEXT - scroll down (newer messages) if (c == 0xF1 || c == 's' || c == 'S') { if (_scrollPos > 0) { _scrollPos--; @@ -225,8 +250,36 @@ public: } } - // KEY_ENTER or 'c' - compose (handled by main.cpp keyboard handler) - // 'q' - go back (handled by main.cpp keyboard handler) + // A - previous channel + if (c == 'a' || c == 'A') { + if (_viewChannelIdx > 0) { + _viewChannelIdx--; + } else { + // Wrap to last valid channel + for (uint8_t i = MAX_GROUP_CHANNELS - 1; i > 0; i--) { + ChannelDetails ch; + if (the_mesh.getChannel(i, ch) && ch.name[0] != '\0') { + _viewChannelIdx = i; + break; + } + } + } + _scrollPos = 0; + return true; + } + + // D - next channel + if (c == 'd' || c == 'D') { + ChannelDetails ch; + uint8_t nextIdx = _viewChannelIdx + 1; + if (the_mesh.getChannel(nextIdx, ch) && ch.name[0] != '\0') { + _viewChannelIdx = nextIdx; + } else { + _viewChannelIdx = 0; + } + _scrollPos = 0; + return true; + } return false; } @@ -235,4 +288,4 @@ public: void resetScroll() { _scrollPos = 0; } -}; \ No newline at end of file +}; \ No newline at end of file diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index b343911..306e993 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -633,8 +633,20 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i // Add to preview screen (for notifications) ((MsgPreviewScreen *) msg_preview)->addPreview(path_len, from_name, text); - // Also add to channel history screen - ((ChannelScreen *) channel_screen)->addMessage(path_len, from_name, text); + // Determine channel index by looking up the channel name + // For channel messages, from_name is the channel name + // For contact messages, from_name is the contact name (channel_idx = 0xFF) + uint8_t channel_idx = 0xFF; // Default: unknown/contact message + for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) { + ChannelDetails ch; + if (the_mesh.getChannel(i, ch) && strcmp(ch.name, from_name) == 0) { + channel_idx = i; + break; + } + } + + // Add to channel history screen with channel index + ((ChannelScreen *) channel_screen)->addMessage(channel_idx, path_len, from_name, text); setCurrScreen(msg_preview); @@ -950,4 +962,8 @@ void UITask::gotoChannelScreen() { } _auto_off = millis() + AUTO_OFF_MILLIS; _next_refresh = 100; +} + +uint8_t UITask::getChannelScreenViewIdx() const { + return ((ChannelScreen *) channel_screen)->getViewChannelIdx(); } \ No newline at end of file diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index bda85df..ac39714 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -80,6 +80,7 @@ public: bool hasDisplay() const { return _display != NULL; } bool isButtonPressed() const; bool isOnChannelScreen() const { return curr == channel_screen; } + uint8_t getChannelScreenViewIdx() const; void toggleBuzzer(); bool getGPSState();