mirror of
https://github.com/pelgraine/Meck.git
synced 2026-03-28 17:42:44 +01:00
DMs now available - select contact in contacts list view by pressing Enter
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 ***
|
||||
|
||||
Reference in New Issue
Block a user