"Added additional channel compose functionality - can switch channels now. Minor ui changes for nav bar"

This commit is contained in:
pelgraine
2026-02-01 17:51:17 +11:00
parent 0be77ef759
commit b0003e1896
4 changed files with 233 additions and 77 deletions

View File

@@ -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);
}
}

View File

@@ -2,19 +2,27 @@
#include <helpers/ui/UIScreen.h>
#include <helpers/ui/DisplayDriver.h>
#include <helpers/ChannelDetails.h>
#include <MeshCore.h>
// 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;
}
};
};

View File

@@ -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();
}

View File

@@ -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();