1
0
forked from iarv/Meck

DMs now available - select contact in contacts list view by pressing Enter

This commit is contained in:
pelgraine
2026-02-10 13:59:15 +11:00
parent f630cf3a5a
commit e030a61244
6 changed files with 170 additions and 43 deletions

View File

@@ -620,6 +620,36 @@ void MyMesh::queueSentChannelMessage(uint8_t channel_idx, uint32_t timestamp, co
}
}
bool MyMesh::uiSendDirectMessage(uint32_t contact_idx, const char* text) {
ContactInfo contact;
if (!getContactByIdx(contact_idx, contact)) return false;
ContactInfo* recipient = lookupContactByPubKey(contact.id.pub_key, PUB_KEY_SIZE);
if (!recipient) return false;
uint32_t timestamp = getRTCClock()->getCurrentTimeUnique();
uint32_t expected_ack, est_timeout;
int result = sendMessage(*recipient, timestamp, 0, text, expected_ack, est_timeout);
if (result == MSG_SEND_FAILED) {
MESH_DEBUG_PRINTLN("UI: DM send failed to %s", recipient->name);
return false;
}
// Track expected ACK for delivery confirmation
if (expected_ack) {
expected_ack_table[next_ack_idx].msg_sent = _ms->getMillis();
expected_ack_table[next_ack_idx].ack = expected_ack;
expected_ack_table[next_ack_idx].contact = recipient;
next_ack_idx = (next_ack_idx + 1) % EXPECTED_ACK_TABLE_SIZE;
}
MESH_DEBUG_PRINTLN("UI: DM sent to %s (%s), ack=0x%08X timeout=%dms",
recipient->name, result == MSG_SEND_SENT_FLOOD ? "flood" : "direct",
expected_ack, est_timeout);
return true;
}
uint8_t MyMesh::onContactRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data,
uint8_t len, uint8_t *reply) {
if (data[0] == REQ_TYPE_GET_TELEMETRY_DATA) {
@@ -1080,23 +1110,14 @@ void MyMesh::handleCmdFrame(size_t len) {
memcpy(&msg_timestamp, &cmd_frame[i], 4);
i += 4;
const char *text = (char *)&cmd_frame[i];
int text_len = len - i;
if (txt_type != TXT_TYPE_PLAIN) {
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
} else {
ChannelDetails channel;
bool success = getChannel(channel_idx, channel);
if (success && sendGroupMessage(msg_timestamp, channel.channel, _prefs.node_name, text, text_len)) {
if (success && sendGroupMessage(msg_timestamp, channel.channel, _prefs.node_name, text, len - i)) {
writeOKFrame();
#ifdef DISPLAY_CLASS
// Show BLE-app-sent message on device channel screen
if (_ui) {
// Null-terminate text from BLE frame (frame buffer may have residual data)
cmd_frame[i + text_len] = 0;
_ui->addSentChannelMessage(channel_idx, _prefs.node_name, text);
}
#endif
} else {
writeErrFrame(ERR_CODE_NOT_FOUND); // bad channel_idx
}

View File

@@ -12,7 +12,7 @@
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "Meck v0.8.1"
#define FIRMWARE_VERSION "Meck v0.8"
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
@@ -105,6 +105,9 @@ public:
// Queue a sent channel message for BLE app sync
void queueSentChannelMessage(uint8_t channel_idx, uint32_t timestamp, const char* sender, const char* text);
// Send a direct message from the UI (no BLE dependency)
bool uiSendDirectMessage(uint32_t contact_idx, const char* text);
protected:
float getAirtimeBudgetFactor() const override;
int getInterferenceThreshold() const override;

View File

@@ -22,6 +22,11 @@
static unsigned long lastComposeRefresh = 0;
static bool composeNeedsRefresh = false;
#define COMPOSE_REFRESH_INTERVAL 600 // ms between e-ink refreshes while typing (refresh takes ~650ms)
// DM compose mode (direct message to a specific contact)
static bool composeDM = false;
static int composeDMContactIdx = -1;
static char composeDMName[32];
// AGC reset - periodically re-assert RX boosted gain to prevent sensitivity drift
#define AGC_RESET_INTERVAL_MS 500
static unsigned long lastAGCReset = 0;
@@ -483,11 +488,18 @@ void handleKeyboardInput() {
if (composePos > 0) {
sendComposedMessage();
}
bool wasDM = composeDM;
composeMode = false;
emojiPickerMode = false;
composeDM = false;
composeDMContactIdx = -1;
composeBuffer[0] = '\0';
composePos = 0;
ui_task.gotoHomeScreen();
if (wasDM) {
ui_task.gotoContactsScreen();
} else {
ui_task.gotoHomeScreen();
}
return;
}
@@ -496,11 +508,18 @@ void handleKeyboardInput() {
if (keyboard.wasShiftRecentlyPressed(500)) {
// Shift+Backspace = Cancel (works anytime)
Serial.println("Compose: Shift+Backspace, cancelling...");
bool wasDM = composeDM;
composeMode = false;
emojiPickerMode = false;
composeDM = false;
composeDMContactIdx = -1;
composeBuffer[0] = '\0';
composePos = 0;
ui_task.gotoHomeScreen();
if (wasDM) {
ui_task.gotoContactsScreen();
} else {
ui_task.gotoHomeScreen();
}
return;
}
// Regular backspace - delete last character (or entire emoji including pads)
@@ -520,8 +539,8 @@ void handleKeyboardInput() {
return;
}
// A/D keys switch channels (only when buffer is empty or as special function)
if ((key == 'a' || key == 'A') && composePos == 0) {
// A/D keys switch channels (only when buffer is empty, not in DM mode)
if ((key == 'a' || key == 'A') && composePos == 0 && !composeDM) {
// Previous channel
if (composeChannelIdx > 0) {
composeChannelIdx--;
@@ -540,7 +559,7 @@ void handleKeyboardInput() {
return;
}
if ((key == 'd' || key == 'D') && composePos == 0) {
if ((key == 'd' || key == 'D') && composePos == 0 && !composeDM) {
// Next channel
ChannelDetails ch;
uint8_t nextIdx = composeChannelIdx + 1;
@@ -595,6 +614,8 @@ void handleKeyboardInput() {
// C key: allow entering compose mode from reader
if (key == 'c' || key == 'C') {
composeDM = false;
composeDMContactIdx = -1;
composeMode = true;
composeBuffer[0] = '\0';
composePos = 0;
@@ -613,17 +634,36 @@ void handleKeyboardInput() {
switch (key) {
case 'c':
case 'C':
// Enter compose mode
composeMode = true;
composeBuffer[0] = '\0';
composePos = 0;
// If on channel screen, sync compose channel with viewed channel
if (ui_task.isOnChannelScreen()) {
composeChannelIdx = ui_task.getChannelScreenViewIdx();
// Enter compose mode - DM if on contacts screen, channel otherwise
if (ui_task.isOnContactsScreen()) {
ContactsScreen* cs = (ContactsScreen*)ui_task.getContactsScreen();
int idx = cs->getSelectedContactIdx();
uint8_t ctype = cs->getSelectedContactType();
if (idx >= 0 && ctype == ADV_TYPE_CHAT) {
composeDM = true;
composeDMContactIdx = idx;
cs->getSelectedContactName(composeDMName, sizeof(composeDMName));
composeMode = true;
composeBuffer[0] = '\0';
composePos = 0;
Serial.printf("Entering DM compose to %s (idx %d)\n", composeDMName, idx);
drawComposeScreen();
lastComposeRefresh = millis();
}
} else {
composeDM = false;
composeDMContactIdx = -1;
composeMode = true;
composeBuffer[0] = '\0';
composePos = 0;
// 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();
lastComposeRefresh = millis();
}
Serial.printf("Entering compose mode, channel %d\n", composeChannelIdx);
drawComposeScreen();
lastComposeRefresh = millis();
break;
case 'm':
@@ -692,9 +732,29 @@ void handleKeyboardInput() {
break;
case '\r':
// Select/Enter
Serial.println("Nav: Enter/Select");
ui_task.injectKey(13); // KEY_ENTER
// Select/Enter - if on contacts screen, enter DM compose for chat contacts
if (ui_task.isOnContactsScreen()) {
ContactsScreen* cs = (ContactsScreen*)ui_task.getContactsScreen();
int idx = cs->getSelectedContactIdx();
uint8_t ctype = cs->getSelectedContactType();
if (idx >= 0 && ctype == ADV_TYPE_CHAT) {
composeDM = true;
composeDMContactIdx = idx;
cs->getSelectedContactName(composeDMName, sizeof(composeDMName));
composeMode = true;
composeBuffer[0] = '\0';
composePos = 0;
Serial.printf("Entering DM compose to %s (idx %d)\n", composeDMName, idx);
drawComposeScreen();
lastComposeRefresh = millis();
} else if (idx >= 0) {
// Non-chat contact selected (repeater, room, etc.) - future use
Serial.printf("Selected non-chat contact type=%d idx=%d\n", ctype, idx);
}
} else {
Serial.println("Nav: Enter/Select");
ui_task.injectKey(13); // KEY_ENTER
}
break;
case 'q':
@@ -725,12 +785,16 @@ void drawComposeScreen() {
display.setCursor(0, 0);
// 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);
if (composeDM) {
snprintf(headerBuf, sizeof(headerBuf), "DM: %s", composeDMName);
} else {
snprintf(headerBuf, sizeof(headerBuf), "To: Channel %d", composeChannelIdx);
ChannelDetails channel;
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);
@@ -855,27 +919,37 @@ void drawEmojiPicker() {
void sendComposedMessage() {
if (composePos == 0) return;
// Get the selected channel
// Convert escape bytes back to UTF-8 for mesh transmission and BLE app
char utf8Buf[512];
emojiUnescape(composeBuffer, utf8Buf, sizeof(utf8Buf));
if (composeDM) {
// Direct message to a specific contact
if (composeDMContactIdx >= 0) {
if (the_mesh.uiSendDirectMessage((uint32_t)composeDMContactIdx, utf8Buf)) {
ui_task.showAlert("DM sent!", 1500);
} else {
ui_task.showAlert("DM failed!", 1500);
}
} else {
ui_task.showAlert("No contact!", 1500);
}
return;
}
// Channel (group) message
ChannelDetails channel;
if (the_mesh.getChannel(composeChannelIdx, channel)) {
uint32_t timestamp = rtc_clock.getCurrentTime();
// Convert escape bytes back to UTF-8 for mesh transmission and BLE app
// Worst case: each escape byte → 8 bytes UTF-8 (flag emoji), plus ASCII chars
char utf8Buf[512];
emojiUnescape(composeBuffer, utf8Buf, sizeof(utf8Buf));
int utf8Len = strlen(utf8Buf);
// Send UTF-8 version to mesh (so other devices/apps see real emoji)
if (the_mesh.sendGroupMessage(timestamp, channel.channel,
the_mesh.getNodePrefs()->node_name,
utf8Buf, utf8Len)) {
// Add to local display (UTF-8 gets sanitized to escape bytes by addMessage)
ui_task.addSentChannelMessage(composeChannelIdx,
the_mesh.getNodePrefs()->node_name,
utf8Buf);
// Queue UTF-8 version for BLE app sync (so companion app shows real emoji)
the_mesh.queueSentChannelMessage(composeChannelIdx, timestamp,
the_mesh.getNodePrefs()->node_name,
utf8Buf);

View File

@@ -148,6 +148,26 @@ public:
return _filteredIdx[_scrollPos];
}
// Get the adv_type of the currently highlighted contact
// Returns 0xFF if no valid selection
uint8_t getSelectedContactType() const {
if (_filteredCount == 0) return 0xFF;
ContactInfo contact;
if (!the_mesh.getContactByIdx(_filteredIdx[_scrollPos], contact)) return 0xFF;
return contact.type;
}
// Copy the name of the currently highlighted contact into buf
// Returns false if no valid selection
bool getSelectedContactName(char* buf, size_t bufLen) const {
if (_filteredCount == 0) return false;
ContactInfo contact;
if (!the_mesh.getContactByIdx(_filteredIdx[_scrollPos], contact)) return false;
strncpy(buf, contact.name, bufLen);
buf[bufLen - 1] = '\0';
return true;
}
int render(DisplayDriver& display) override {
if (!_cacheValid) rebuildCache();

View File

@@ -990,6 +990,15 @@ void UITask::injectKey(char c) {
}
}
void UITask::gotoHomeScreen() {
setCurrScreen(home);
if (_display != NULL && !_display->isOn()) {
_display->turnOn();
}
_auto_off = millis() + AUTO_OFF_MILLIS;
_next_refresh = 100;
}
void UITask::gotoChannelScreen() {
((ChannelScreen *) channel_screen)->resetScroll();
setCurrScreen(channel_screen);

View File

@@ -75,7 +75,7 @@ public:
}
void begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs);
void gotoHomeScreen() { setCurrScreen(home); }
void gotoHomeScreen();
void gotoChannelScreen(); // Navigate to channel message screen
void gotoContactsScreen(); // Navigate to contacts list
void gotoTextReader(); // *** NEW: Navigate to text reader ***