mirror of
https://github.com/pelgraine/Meck.git
synced 2026-08-09 10:12:42 +02:00
roomserver additions stage 1; dms ui functionality improvements; removed t-deck plus device variant
This commit is contained in:
@@ -1588,6 +1588,13 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
uint8_t ch_idx = is_v3_ch ? out_frame[4] : out_frame[1];
|
||||
_ui->markChannelReadFromBLE(ch_idx);
|
||||
}
|
||||
|
||||
// Mark DM slot read when companion app syncs a contact (DM/room) message
|
||||
bool is_v3_dm = (out_frame[0] == RESP_CODE_CONTACT_MSG_RECV_V3);
|
||||
bool is_old_dm = (out_frame[0] == RESP_CODE_CONTACT_MSG_RECV);
|
||||
if (is_v3_dm || is_old_dm) {
|
||||
_ui->markChannelReadFromBLE(0xFF);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
|
||||
@@ -954,10 +954,38 @@ static void lastHeardToggleContact() {
|
||||
}
|
||||
#endif
|
||||
|
||||
// Channel screen: long press → compose to current channel
|
||||
// Channel screen: long press → compose to current channel (or DM actions on DM tab)
|
||||
if (ui_task.isOnChannelScreen()) {
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
uint8_t chIdx = ui_task.getChannelScreenViewIdx();
|
||||
if (chIdx == 0xFF) {
|
||||
ChannelScreen* chScr = (ChannelScreen*)ui_task.getChannelScreen();
|
||||
if (chScr->isDMInboxMode()) {
|
||||
// Inbox mode: long press = open selected conversation (same as Enter)
|
||||
return '\r';
|
||||
}
|
||||
// Conversation mode: long press = compose reply
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
const char* dmName = chScr->getDMFilterName();
|
||||
if (dmName && dmName[0]) {
|
||||
uint32_t numC = the_mesh.getNumContacts();
|
||||
ContactInfo ci;
|
||||
for (uint32_t j = 0; j < numC; j++) {
|
||||
if (the_mesh.getContactByIdx(j, ci) && strcmp(ci.name, dmName) == 0) {
|
||||
char label[40];
|
||||
snprintf(label, sizeof(label), "DM: %s", dmName);
|
||||
ui_task.showVirtualKeyboard(VKB_DM, label, "", 137, j);
|
||||
ui_task.clearDMUnread(j);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
ui_task.showAlert("Contact not found", 1000);
|
||||
return 0;
|
||||
#else
|
||||
return KEY_ENTER;
|
||||
#endif
|
||||
}
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
ChannelDetails ch;
|
||||
if (the_mesh.getChannel(chIdx, ch)) {
|
||||
char label[40];
|
||||
@@ -978,6 +1006,13 @@ static void lastHeardToggleContact() {
|
||||
uint8_t ctype = cs->getSelectedContactType();
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
if (idx >= 0 && ctype == ADV_TYPE_CHAT) {
|
||||
if (ui_task.hasDMUnread(idx)) {
|
||||
char cname[32];
|
||||
cs->getSelectedContactName(cname, sizeof(cname));
|
||||
ui_task.clearDMUnread(idx);
|
||||
ui_task.gotoDMConversation(cname);
|
||||
return 0;
|
||||
}
|
||||
char dname[32];
|
||||
cs->getSelectedContactName(dname, sizeof(dname));
|
||||
char label[40];
|
||||
@@ -987,6 +1022,16 @@ static void lastHeardToggleContact() {
|
||||
} else if (idx >= 0 && ctype == ADV_TYPE_REPEATER) {
|
||||
ui_task.gotoRepeaterAdmin(idx);
|
||||
return 0;
|
||||
} else if (idx >= 0 && ctype == ADV_TYPE_ROOM) {
|
||||
// Room server: open login (after login, auto-redirects to conversation)
|
||||
ui_task.gotoRepeaterAdmin(idx);
|
||||
return 0;
|
||||
} else if (idx >= 0 && ui_task.hasDMUnread(idx)) {
|
||||
char cname[32];
|
||||
cs->getSelectedContactName(cname, sizeof(cname));
|
||||
ui_task.clearDMUnread(idx);
|
||||
ui_task.gotoDMConversation(cname);
|
||||
return 0;
|
||||
}
|
||||
#else
|
||||
// T-Deck Pro: repeater admin works directly, DM via keyboard compose
|
||||
@@ -2110,13 +2155,37 @@ void loop() {
|
||||
} else if (ckb == '\r') {
|
||||
// Enter key — screen-specific compose or select
|
||||
if (ui_task.isOnChannelScreen()) {
|
||||
// Open VKB for channel message compose
|
||||
uint8_t chIdx = ui_task.getChannelScreenViewIdx();
|
||||
ChannelDetails ch;
|
||||
if (the_mesh.getChannel(chIdx, ch)) {
|
||||
char label[40];
|
||||
snprintf(label, sizeof(label), "To: %s", ch.name);
|
||||
ui_task.showVirtualKeyboard(VKB_CHANNEL_MSG, label, "", 137, chIdx);
|
||||
if (chIdx == 0xFF) {
|
||||
ChannelScreen* chScr = (ChannelScreen*)ui_task.getChannelScreen();
|
||||
if (chScr->isDMInboxMode()) {
|
||||
// Inbox mode: inject Enter to open conversation
|
||||
ui_task.injectKey('\r');
|
||||
} else {
|
||||
// Conversation mode: open VKB DM compose
|
||||
const char* dmName = chScr->getDMFilterName();
|
||||
if (dmName && dmName[0]) {
|
||||
uint32_t numC = the_mesh.getNumContacts();
|
||||
ContactInfo ci;
|
||||
for (uint32_t j = 0; j < numC; j++) {
|
||||
if (the_mesh.getContactByIdx(j, ci) && strcmp(ci.name, dmName) == 0) {
|
||||
char label[40];
|
||||
snprintf(label, sizeof(label), "DM: %s", dmName);
|
||||
ui_task.showVirtualKeyboard(VKB_DM, label, "", 137, j);
|
||||
ui_task.clearDMUnread(j);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Open VKB for channel message compose
|
||||
ChannelDetails ch;
|
||||
if (the_mesh.getChannel(chIdx, ch)) {
|
||||
char label[40];
|
||||
snprintf(label, sizeof(label), "To: %s", ch.name);
|
||||
ui_task.showVirtualKeyboard(VKB_CHANNEL_MSG, label, "", 137, chIdx);
|
||||
}
|
||||
}
|
||||
} else if (ui_task.isOnContactsScreen()) {
|
||||
// DM compose for chat contacts, admin for repeaters
|
||||
@@ -2125,13 +2194,28 @@ void loop() {
|
||||
int idx = cs->getSelectedContactIdx();
|
||||
uint8_t ctype = cs->getSelectedContactType();
|
||||
if (idx >= 0 && ctype == ADV_TYPE_CHAT) {
|
||||
char dname[32];
|
||||
cs->getSelectedContactName(dname, sizeof(dname));
|
||||
char label[40];
|
||||
snprintf(label, sizeof(label), "DM: %s", dname);
|
||||
ui_task.showVirtualKeyboard(VKB_DM, label, "", 137, idx);
|
||||
if (ui_task.hasDMUnread(idx)) {
|
||||
char cname[32];
|
||||
cs->getSelectedContactName(cname, sizeof(cname));
|
||||
ui_task.clearDMUnread(idx);
|
||||
ui_task.gotoDMConversation(cname);
|
||||
} else {
|
||||
char dname[32];
|
||||
cs->getSelectedContactName(dname, sizeof(dname));
|
||||
char label[40];
|
||||
snprintf(label, sizeof(label), "DM: %s", dname);
|
||||
ui_task.showVirtualKeyboard(VKB_DM, label, "", 137, idx);
|
||||
}
|
||||
} else if (idx >= 0 && ctype == ADV_TYPE_REPEATER) {
|
||||
ui_task.gotoRepeaterAdmin(idx);
|
||||
} else if (idx >= 0 && ctype == ADV_TYPE_ROOM) {
|
||||
// Room server: open login (auto-redirects to conversation)
|
||||
ui_task.gotoRepeaterAdmin(idx);
|
||||
} else if (idx >= 0 && ui_task.hasDMUnread(idx)) {
|
||||
char cname[32];
|
||||
cs->getSelectedContactName(cname, sizeof(cname));
|
||||
ui_task.clearDMUnread(idx);
|
||||
ui_task.gotoDMConversation(cname);
|
||||
}
|
||||
}
|
||||
} else if (ui_task.isOnRepeaterAdmin()) {
|
||||
@@ -2966,24 +3050,46 @@ void handleKeyboardInput() {
|
||||
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();
|
||||
// If unread DMs exist, go to conversation view to read first
|
||||
if (ui_task.hasDMUnread(idx)) {
|
||||
char cname[32];
|
||||
cs->getSelectedContactName(cname, sizeof(cname));
|
||||
ui_task.clearDMUnread(idx);
|
||||
ui_task.gotoDMConversation(cname);
|
||||
Serial.printf("Unread DMs from %s — opening conversation\n", cname);
|
||||
} else {
|
||||
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 && ctype == ADV_TYPE_REPEATER) {
|
||||
// Open repeater admin screen
|
||||
char rname[32];
|
||||
cs->getSelectedContactName(rname, sizeof(rname));
|
||||
Serial.printf("Opening repeater admin for %s (idx %d)\n", rname, idx);
|
||||
ui_task.gotoRepeaterAdmin(idx);
|
||||
} else if (idx >= 0 && ctype == ADV_TYPE_ROOM) {
|
||||
// Room server: open login screen (after login, auto-redirects to conversation)
|
||||
char rname[32];
|
||||
cs->getSelectedContactName(rname, sizeof(rname));
|
||||
Serial.printf("Room %s — opening login\n", rname);
|
||||
ui_task.gotoRepeaterAdmin(idx);
|
||||
} else if (idx >= 0) {
|
||||
// Non-chat, non-repeater contact (room, sensor, etc.) - future use
|
||||
Serial.printf("Selected contact type=%d idx=%d\n", ctype, idx);
|
||||
// Other contacts with unreads
|
||||
if (ui_task.hasDMUnread(idx)) {
|
||||
char cname[32];
|
||||
cs->getSelectedContactName(cname, sizeof(cname));
|
||||
ui_task.clearDMUnread(idx);
|
||||
ui_task.gotoDMConversation(cname);
|
||||
} else {
|
||||
Serial.printf("Selected contact type=%d idx=%d\n", ctype, idx);
|
||||
}
|
||||
}
|
||||
} else if (ui_task.isOnChannelScreen()) {
|
||||
// If path overlay is showing, Enter copies path text to compose buffer
|
||||
@@ -3010,6 +3116,42 @@ void handleKeyboardInput() {
|
||||
lastComposeRefresh = millis();
|
||||
break;
|
||||
}
|
||||
|
||||
// DM inbox mode: pass Enter to ChannelScreen to open the selected conversation
|
||||
if (chScr2 && chScr2->isDMInboxMode()) {
|
||||
ui_task.injectKey('\r');
|
||||
break;
|
||||
}
|
||||
|
||||
// DM conversation mode: Enter opens DM compose to the contact being viewed
|
||||
// (DM inbox mode Enter is handled by ChannelScreen::handleInput internally)
|
||||
if (chScr2 && chScr2->isDMConversation()) {
|
||||
const char* dmName = chScr2->getDMFilterName();
|
||||
if (dmName && dmName[0]) {
|
||||
uint32_t numC = the_mesh.getNumContacts();
|
||||
ContactInfo ci;
|
||||
for (uint32_t j = 0; j < numC; j++) {
|
||||
if (the_mesh.getContactByIdx(j, ci) && strcmp(ci.name, dmName) == 0) {
|
||||
composeDM = true;
|
||||
composeDMContactIdx = (int)j;
|
||||
strncpy(composeDMName, dmName, sizeof(composeDMName) - 1);
|
||||
composeDMName[sizeof(composeDMName) - 1] = '\0';
|
||||
composeMode = true;
|
||||
composeBuffer[0] = '\0';
|
||||
composePos = 0;
|
||||
ui_task.clearDMUnread(j);
|
||||
Serial.printf("DM conversation compose to %s (idx %d)\n", dmName, j);
|
||||
drawComposeScreen();
|
||||
lastComposeRefresh = millis();
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ui_task.showAlert("No contact selected", 1000);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
composeDM = false;
|
||||
composeDMContactIdx = -1;
|
||||
composeChannelIdx = ui_task.getChannelScreenViewIdx();
|
||||
@@ -3124,6 +3266,24 @@ void handleKeyboardInput() {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'l':
|
||||
// L = Login/Admin — from DM conversation, open repeater admin with auto-login
|
||||
if (ui_task.isOnChannelScreen()) {
|
||||
ChannelScreen* chScr = (ChannelScreen*)ui_task.getChannelScreen();
|
||||
Serial.printf("[L key] onChannelScreen=1, isDMConv=%d, perms=%d, cidx=%d\n",
|
||||
chScr ? chScr->isDMConversation() : -1,
|
||||
chScr ? chScr->getDMContactPerms() : -1,
|
||||
chScr ? chScr->getDMContactIdx() : -1);
|
||||
if (chScr && chScr->isDMConversation() && chScr->getDMContactPerms() > 0) {
|
||||
int cidx = chScr->getDMContactIdx();
|
||||
if (cidx >= 0) {
|
||||
ui_task.gotoRepeaterAdminDirect(cidx);
|
||||
Serial.printf("DM conversation: auto-login admin for idx %d\n", cidx);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'q':
|
||||
case '\b':
|
||||
// If channel screen reply select or path overlay is showing, dismiss it
|
||||
|
||||
@@ -84,6 +84,34 @@ private:
|
||||
int _replySelectPos; // Index into chronological channelMsgs[] (0=oldest)
|
||||
int _replyChannelMsgCount; // Cached count from last render (for input bounds)
|
||||
|
||||
// DM tab (channel_idx == 0xFF) two-level view:
|
||||
// Inbox mode: list of contacts you have DMs from
|
||||
// Conversation mode: messages filtered to one contact
|
||||
bool _dmInboxMode; // true = showing inbox list, false = conversation
|
||||
int _dmInboxScroll; // Scroll position in inbox list
|
||||
char _dmFilterName[32]; // Selected contact name for conversation view
|
||||
int _dmContactIdx; // Contact index for conversation (-1 if unknown)
|
||||
uint8_t _dmContactPerms; // Last login permissions for this contact (0=none/guest)
|
||||
const uint8_t* _dmUnreadPtr; // Pointer to per-contact DM unread array (from UITask)
|
||||
|
||||
// Helper: does a message belong to the current view?
|
||||
bool msgMatchesView(const ChannelMessage& msg) const {
|
||||
if (!msg.valid) return false;
|
||||
if (_viewChannelIdx != 0xFF) {
|
||||
return msg.channel_idx == _viewChannelIdx;
|
||||
}
|
||||
// DM tab in conversation mode: filter by sender name
|
||||
if (!_dmInboxMode && _dmFilterName[0] != '\0') {
|
||||
if (msg.channel_idx != 0xFF) return false;
|
||||
int nameLen = strlen(_dmFilterName);
|
||||
if (strncmp(msg.text, _dmFilterName, nameLen) != 0) return false;
|
||||
if (msg.text[nameLen] != ':') return false;
|
||||
return true;
|
||||
}
|
||||
// Inbox mode or no filter — match all DMs
|
||||
return msg.channel_idx == 0xFF;
|
||||
}
|
||||
|
||||
// Per-channel unread message counts (standalone mode)
|
||||
// Index 0..MAX_GROUP_CHANNELS-1 for channel messages
|
||||
// Index MAX_GROUP_CHANNELS for DMs (channel_idx == 0xFF)
|
||||
@@ -93,7 +121,9 @@ public:
|
||||
ChannelScreen(UITask* task, mesh::RTCClock* rtc)
|
||||
: _task(task), _rtc(rtc), _msgCount(0), _newestIdx(-1), _scrollPos(0),
|
||||
_msgsPerPage(6), _viewChannelIdx(0), _sdReady(false), _showPathOverlay(false), _pathScrollPos(0), _pathHopsVisible(20),
|
||||
_replySelectMode(false), _replySelectPos(-1), _replyChannelMsgCount(0) {
|
||||
_replySelectMode(false), _replySelectPos(-1), _replyChannelMsgCount(0),
|
||||
_dmInboxMode(true), _dmInboxScroll(0), _dmContactIdx(-1), _dmContactPerms(0), _dmUnreadPtr(nullptr) {
|
||||
_dmFilterName[0] = '\0';
|
||||
// Initialize all messages as invalid
|
||||
for (int i = 0; i < CHANNEL_MSG_HISTORY_SIZE; i++) {
|
||||
_messages[i].valid = false;
|
||||
@@ -158,7 +188,7 @@ public:
|
||||
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) {
|
||||
if (msgMatchesView(_messages[i])) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
@@ -173,11 +203,49 @@ public:
|
||||
_scrollPos = 0;
|
||||
_showPathOverlay = false;
|
||||
_pathScrollPos = 0;
|
||||
// Reset DM inbox state when entering DM tab
|
||||
if (idx == 0xFF) {
|
||||
_dmInboxMode = true;
|
||||
_dmInboxScroll = 0;
|
||||
_dmFilterName[0] = '\0';
|
||||
_dmContactIdx = -1;
|
||||
_dmContactPerms = 0;
|
||||
}
|
||||
markChannelRead(idx);
|
||||
}
|
||||
bool isDMTab() const { return _viewChannelIdx == 0xFF; }
|
||||
bool isDMInboxMode() const { return _viewChannelIdx == 0xFF && _dmInboxMode; }
|
||||
bool isDMConversation() const { return _viewChannelIdx == 0xFF && !_dmInboxMode; }
|
||||
const char* getDMFilterName() const { return _dmFilterName; }
|
||||
|
||||
// Open a specific contact's DM conversation directly (skipping inbox)
|
||||
void openConversation(const char* contactName, int contactIdx = -1, uint8_t perms = 0) {
|
||||
strncpy(_dmFilterName, contactName, sizeof(_dmFilterName) - 1);
|
||||
_dmFilterName[sizeof(_dmFilterName) - 1] = '\0';
|
||||
_dmInboxMode = false;
|
||||
_dmContactIdx = contactIdx;
|
||||
_dmContactPerms = perms;
|
||||
_scrollPos = 0;
|
||||
Serial.printf("[ChannelScreen] openConversation: name=%s, idx=%d, perms=%d\n",
|
||||
contactName, contactIdx, perms);
|
||||
}
|
||||
|
||||
int getDMContactIdx() const { return _dmContactIdx; }
|
||||
uint8_t getDMContactPerms() const { return _dmContactPerms; }
|
||||
void setDMContactPerms(uint8_t p) { _dmContactPerms = p; }
|
||||
bool isShowingPathOverlay() const { return _showPathOverlay; }
|
||||
void dismissPathOverlay() { _showPathOverlay = false; _pathScrollPos = 0; }
|
||||
|
||||
// Set pointer to per-contact DM unread array (called by UITask after allocation)
|
||||
void setDMUnreadPtr(const uint8_t* ptr) { _dmUnreadPtr = ptr; }
|
||||
|
||||
// Subtract a specific amount from the DM unread slot (used by per-contact clearing)
|
||||
void subtractDMUnread(int count) {
|
||||
int slot = MAX_GROUP_CHANNELS; // DM slot
|
||||
_unread[slot] -= count;
|
||||
if (_unread[slot] < 0) _unread[slot] = 0;
|
||||
}
|
||||
|
||||
// --- Reply select mode (R key → pick a message → Enter to @mention reply) ---
|
||||
bool isReplySelectMode() const { return _replySelectMode; }
|
||||
void exitReplySelect() { _replySelectMode = false; _replySelectPos = -1; }
|
||||
@@ -206,7 +274,7 @@ public:
|
||||
int idx = _newestIdx - i;
|
||||
while (idx < 0) idx += CHANNEL_MSG_HISTORY_SIZE;
|
||||
idx = idx % CHANNEL_MSG_HISTORY_SIZE;
|
||||
if (_messages[idx].valid && _messages[idx].channel_idx == _viewChannelIdx) {
|
||||
if (_messages[idx].valid && msgMatchesView(_messages[idx])) {
|
||||
rsMsgs[count++] = idx;
|
||||
}
|
||||
}
|
||||
@@ -230,7 +298,7 @@ public:
|
||||
int idx = _newestIdx - i;
|
||||
while (idx < 0) idx += CHANNEL_MSG_HISTORY_SIZE;
|
||||
idx = idx % CHANNEL_MSG_HISTORY_SIZE;
|
||||
if (_messages[idx].valid && _messages[idx].channel_idx == _viewChannelIdx) {
|
||||
if (_messages[idx].valid && msgMatchesView(_messages[idx])) {
|
||||
rsMsgs[count++] = idx;
|
||||
}
|
||||
}
|
||||
@@ -277,7 +345,7 @@ public:
|
||||
int idx = _newestIdx - i;
|
||||
while (idx < 0) idx += CHANNEL_MSG_HISTORY_SIZE;
|
||||
idx = idx % CHANNEL_MSG_HISTORY_SIZE;
|
||||
if (_messages[idx].valid && _messages[idx].channel_idx == _viewChannelIdx
|
||||
if (msgMatchesView(_messages[idx])
|
||||
&& _messages[idx].path_len != 0) {
|
||||
return &_messages[idx];
|
||||
}
|
||||
@@ -449,7 +517,15 @@ public:
|
||||
|
||||
// Get channel name
|
||||
ChannelDetails channel;
|
||||
if (the_mesh.getChannel(_viewChannelIdx, channel)) {
|
||||
if (_viewChannelIdx == 0xFF) {
|
||||
if (_dmInboxMode) {
|
||||
display.print("Direct Messages");
|
||||
} else {
|
||||
char hdr[40];
|
||||
snprintf(hdr, sizeof(hdr), "DM: %s", _dmFilterName);
|
||||
display.print(hdr);
|
||||
}
|
||||
} else if (the_mesh.getChannel(_viewChannelIdx, channel)) {
|
||||
display.print(channel.name);
|
||||
} else {
|
||||
sprintf(tmp, "Channel %d", _viewChannelIdx);
|
||||
@@ -464,6 +540,180 @@ public:
|
||||
|
||||
// Divider line
|
||||
display.drawRect(0, 11, display.width(), 1);
|
||||
|
||||
// === DM Inbox mode: show list of contacts with DMs ===
|
||||
if (_viewChannelIdx == 0xFF && _dmInboxMode) {
|
||||
#define DM_INBOX_MAX 20
|
||||
struct DMInboxEntry {
|
||||
char name[32];
|
||||
int msgCount;
|
||||
int unreadCount;
|
||||
uint32_t newestTs;
|
||||
};
|
||||
DMInboxEntry inbox[DM_INBOX_MAX];
|
||||
int inboxCount = 0;
|
||||
|
||||
// Scan all DMs and build unique sender list
|
||||
for (int i = 0; i < _msgCount && i < 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 || _messages[idx].channel_idx != 0xFF) continue;
|
||||
|
||||
char sender[32];
|
||||
if (!extractSenderName(_messages[idx].text, sender, sizeof(sender))) continue;
|
||||
|
||||
// Find existing entry
|
||||
int found = -1;
|
||||
for (int j = 0; j < inboxCount; j++) {
|
||||
if (strcmp(inbox[j].name, sender) == 0) { found = j; break; }
|
||||
}
|
||||
if (found < 0 && inboxCount < DM_INBOX_MAX) {
|
||||
found = inboxCount++;
|
||||
strncpy(inbox[found].name, sender, 31);
|
||||
inbox[found].name[31] = '\0';
|
||||
inbox[found].msgCount = 0;
|
||||
inbox[found].unreadCount = 0;
|
||||
inbox[found].newestTs = 0;
|
||||
}
|
||||
if (found >= 0) {
|
||||
inbox[found].msgCount++;
|
||||
if (_messages[idx].timestamp > inbox[found].newestTs)
|
||||
inbox[found].newestTs = _messages[idx].timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
// Look up unread counts from per-contact array
|
||||
if (_dmUnreadPtr) {
|
||||
for (int e = 0; e < inboxCount; e++) {
|
||||
uint32_t numC = the_mesh.getNumContacts();
|
||||
ContactInfo ci;
|
||||
for (uint32_t c = 0; c < numC; c++) {
|
||||
if (the_mesh.getContactByIdx(c, ci) && strcmp(ci.name, inbox[e].name) == 0) {
|
||||
inbox[e].unreadCount = _dmUnreadPtr[c];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by newest timestamp descending (insertion sort)
|
||||
for (int i = 1; i < inboxCount; i++) {
|
||||
DMInboxEntry tmp2 = inbox[i];
|
||||
int j = i - 1;
|
||||
while (j >= 0 && inbox[j].newestTs < tmp2.newestTs) {
|
||||
inbox[j + 1] = inbox[j];
|
||||
j--;
|
||||
}
|
||||
inbox[j + 1] = tmp2;
|
||||
}
|
||||
|
||||
// Render inbox list
|
||||
display.setTextSize(0);
|
||||
int lineH = 9;
|
||||
int headerH = 14;
|
||||
int footerH = 14;
|
||||
int maxY = display.height() - footerH;
|
||||
int y = headerH;
|
||||
int maxVisible = (maxY - headerH) / lineH;
|
||||
if (maxVisible < 3) maxVisible = 3;
|
||||
|
||||
// Clamp scroll
|
||||
if (_dmInboxScroll >= inboxCount) _dmInboxScroll = inboxCount > 0 ? inboxCount - 1 : 0;
|
||||
|
||||
if (inboxCount == 0) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setCursor(0, y);
|
||||
display.print("No direct messages");
|
||||
display.setCursor(0, y + lineH);
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.print("DMs from contacts appear here");
|
||||
#else
|
||||
display.print("A/D: Switch channel");
|
||||
#endif
|
||||
} else {
|
||||
int startIdx = max(0, min(_dmInboxScroll - maxVisible / 2,
|
||||
inboxCount - maxVisible));
|
||||
int endIdx = min(inboxCount, startIdx + maxVisible);
|
||||
|
||||
for (int i = startIdx; i < endIdx && y + lineH <= maxY; i++) {
|
||||
bool selected = (i == _dmInboxScroll);
|
||||
|
||||
if (selected) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.fillRect(0, y, display.width(), lineH);
|
||||
#else
|
||||
display.fillRect(0, y + 5, display.width(), lineH);
|
||||
#endif
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
} else {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
}
|
||||
|
||||
display.setCursor(0, y);
|
||||
|
||||
// Prefix: > for selected, unread indicator
|
||||
char prefix[6];
|
||||
if (inbox[i].unreadCount > 0) {
|
||||
snprintf(prefix, sizeof(prefix), "%s*%d", selected ? ">" : " ", inbox[i].unreadCount);
|
||||
} else {
|
||||
snprintf(prefix, sizeof(prefix), "%s ", selected ? ">" : " ");
|
||||
}
|
||||
display.print(prefix);
|
||||
|
||||
// Name (truncated)
|
||||
char filteredName[32];
|
||||
display.translateUTF8ToBlocks(filteredName, inbox[i].name, sizeof(filteredName));
|
||||
|
||||
// Right side: message count + age
|
||||
char ageStr[8];
|
||||
uint32_t age = _rtc->getCurrentTime() - inbox[i].newestTs;
|
||||
if (age < 60) snprintf(ageStr, sizeof(ageStr), "%ds", age);
|
||||
else if (age < 3600) snprintf(ageStr, sizeof(ageStr), "%dm", age / 60);
|
||||
else if (age < 86400) snprintf(ageStr, sizeof(ageStr), "%dh", age / 3600);
|
||||
else snprintf(ageStr, sizeof(ageStr), "%dd", age / 86400);
|
||||
|
||||
char rightStr[16];
|
||||
snprintf(rightStr, sizeof(rightStr), "(%d) %s", inbox[i].msgCount, ageStr);
|
||||
int rightW = display.getTextWidth(rightStr) + 2;
|
||||
|
||||
int nameX = display.getTextWidth(prefix) + 2;
|
||||
int nameMaxW = display.width() - nameX - rightW - 2;
|
||||
display.drawTextEllipsized(nameX, y, nameMaxW, filteredName);
|
||||
|
||||
display.setCursor(display.width() - rightW, y);
|
||||
display.print(rightStr);
|
||||
|
||||
y += lineH;
|
||||
}
|
||||
}
|
||||
|
||||
// Footer
|
||||
display.setTextSize(1);
|
||||
int footerY = display.height() - 12;
|
||||
display.drawRect(0, footerY - 2, display.width(), 1);
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.setCursor(0, footerY);
|
||||
display.print("Swipe:Nav");
|
||||
const char* rtInbox = "Hold:Open";
|
||||
display.setCursor(display.width() - display.getTextWidth(rtInbox) - 2, footerY);
|
||||
display.print(rtInbox);
|
||||
#else
|
||||
display.setCursor(0, footerY);
|
||||
display.print("Q:Bck A/D:Ch");
|
||||
const char* rtInbox = "Ent:Open";
|
||||
display.setCursor(display.width() - display.getTextWidth(rtInbox) - 2, footerY);
|
||||
display.print(rtInbox);
|
||||
#endif
|
||||
|
||||
#ifdef USE_EINK
|
||||
return 5000;
|
||||
#else
|
||||
return 1000;
|
||||
#endif
|
||||
}
|
||||
|
||||
// --- Path detail overlay ---
|
||||
if (_showPathOverlay) {
|
||||
@@ -667,18 +917,154 @@ public:
|
||||
display.setTextSize(0); // Tiny font for body text
|
||||
display.setCursor(0, 20);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.print("No messages yet");
|
||||
display.setCursor(0, 30);
|
||||
if (_viewChannelIdx == 0xFF) {
|
||||
char noMsg[48];
|
||||
snprintf(noMsg, sizeof(noMsg), "No messages from %s", _dmFilterName);
|
||||
display.print(noMsg);
|
||||
display.setCursor(0, 30);
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.print("Swipe: Switch channel");
|
||||
display.setCursor(0, 40);
|
||||
display.print("Long press: Compose");
|
||||
display.print("Hold: Compose reply");
|
||||
#else
|
||||
display.print("A/D: Switch channel");
|
||||
display.setCursor(0, 40);
|
||||
display.print("C: Compose message");
|
||||
display.print("Q: Back to inbox");
|
||||
display.setCursor(0, 40);
|
||||
display.print("Ent: Compose reply");
|
||||
#endif
|
||||
} else {
|
||||
display.print("No messages yet");
|
||||
display.setCursor(0, 30);
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.print("Swipe: Switch channel");
|
||||
display.setCursor(0, 40);
|
||||
display.print("Long press: Compose");
|
||||
#else
|
||||
display.print("A/D: Switch channel");
|
||||
display.setCursor(0, 40);
|
||||
display.print("C: Compose message");
|
||||
#endif
|
||||
}
|
||||
display.setTextSize(1); // Restore for footer
|
||||
} else if (_viewChannelIdx == 0xFF && _dmInboxMode) {
|
||||
// =================================================================
|
||||
// DM Inbox: list of contacts/rooms you have DM history with
|
||||
// =================================================================
|
||||
display.setTextSize(0);
|
||||
int lineHeight = 9;
|
||||
int headerHeight = 14;
|
||||
int footerHeight = 14;
|
||||
int maxY = display.height() - footerHeight;
|
||||
int y = headerHeight;
|
||||
|
||||
// Scan all DM messages and collect unique senders
|
||||
#define DM_INBOX_MAX 16
|
||||
struct InboxEntry {
|
||||
char name[24];
|
||||
int count;
|
||||
uint32_t newest_ts;
|
||||
};
|
||||
static InboxEntry inbox[DM_INBOX_MAX];
|
||||
int inboxCount = 0;
|
||||
|
||||
for (int i = 0; i < _msgCount; i++) {
|
||||
int idx = _newestIdx - i;
|
||||
while (idx < 0) idx += CHANNEL_MSG_HISTORY_SIZE;
|
||||
idx = idx % CHANNEL_MSG_HISTORY_SIZE;
|
||||
if (!_messages[idx].valid || _messages[idx].channel_idx != 0xFF) continue;
|
||||
|
||||
char sender[24];
|
||||
if (!extractSenderName(_messages[idx].text, sender, sizeof(sender))) continue;
|
||||
|
||||
// Find or add sender in inbox
|
||||
bool found = false;
|
||||
for (int j = 0; j < inboxCount; j++) {
|
||||
if (strcmp(inbox[j].name, sender) == 0) {
|
||||
inbox[j].count++;
|
||||
if (_messages[idx].timestamp > inbox[j].newest_ts)
|
||||
inbox[j].newest_ts = _messages[idx].timestamp;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found && inboxCount < DM_INBOX_MAX) {
|
||||
strncpy(inbox[inboxCount].name, sender, 23);
|
||||
inbox[inboxCount].name[23] = '\0';
|
||||
inbox[inboxCount].count = 1;
|
||||
inbox[inboxCount].newest_ts = _messages[idx].timestamp;
|
||||
inboxCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by newest timestamp descending (most recent first)
|
||||
for (int i = 1; i < inboxCount; i++) {
|
||||
InboxEntry tmp2 = inbox[i];
|
||||
int j = i - 1;
|
||||
while (j >= 0 && inbox[j].newest_ts < tmp2.newest_ts) {
|
||||
inbox[j + 1] = inbox[j];
|
||||
j--;
|
||||
}
|
||||
inbox[j + 1] = tmp2;
|
||||
}
|
||||
|
||||
if (inboxCount == 0) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setCursor(0, y);
|
||||
display.print("No conversations");
|
||||
} else {
|
||||
// Clamp scroll
|
||||
if (_dmInboxScroll >= inboxCount) _dmInboxScroll = inboxCount - 1;
|
||||
if (_dmInboxScroll < 0) _dmInboxScroll = 0;
|
||||
|
||||
int maxVisible = (maxY - headerHeight) / lineHeight;
|
||||
if (maxVisible < 3) maxVisible = 3;
|
||||
int startIdx = max(0, min(_dmInboxScroll - maxVisible / 2,
|
||||
inboxCount - maxVisible));
|
||||
int endIdx = min(inboxCount, startIdx + maxVisible);
|
||||
|
||||
uint32_t now = _rtc->getCurrentTime();
|
||||
for (int i = startIdx; i < endIdx && y + lineHeight <= maxY; i++) {
|
||||
bool selected = (i == _dmInboxScroll);
|
||||
|
||||
if (selected) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.fillRect(0, y, display.width(), lineHeight);
|
||||
#else
|
||||
display.fillRect(0, y + 5, display.width(), lineHeight);
|
||||
#endif
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
} else {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
}
|
||||
|
||||
display.setCursor(0, y);
|
||||
display.print(selected ? ">" : " ");
|
||||
|
||||
// Name (ellipsized)
|
||||
char filteredName[24];
|
||||
display.translateUTF8ToBlocks(filteredName, inbox[i].name, sizeof(filteredName));
|
||||
|
||||
// Right side: message count + age
|
||||
char ageStr[8];
|
||||
uint32_t age = now - inbox[i].newest_ts;
|
||||
if (age < 60) snprintf(ageStr, sizeof(ageStr), "%ds", age);
|
||||
else if (age < 3600) snprintf(ageStr, sizeof(ageStr), "%dm", age / 60);
|
||||
else if (age < 86400) snprintf(ageStr, sizeof(ageStr), "%dh", age / 3600);
|
||||
else snprintf(ageStr, sizeof(ageStr), "%dd", age / 86400);
|
||||
|
||||
char rightStr[16];
|
||||
snprintf(rightStr, sizeof(rightStr), "[%d] %s", inbox[i].count, ageStr);
|
||||
int rightW = display.getTextWidth(rightStr) + 2;
|
||||
|
||||
int nameX = display.getTextWidth(">") + 2;
|
||||
int nameMaxW = display.width() - nameX - rightW - 2;
|
||||
display.drawTextEllipsized(nameX, y, nameMaxW, filteredName);
|
||||
|
||||
display.setCursor(display.width() - rightW, y);
|
||||
display.print(rightStr);
|
||||
|
||||
y += lineHeight;
|
||||
}
|
||||
}
|
||||
display.setTextSize(1);
|
||||
} else {
|
||||
display.setTextSize(0); // Tiny font for message body
|
||||
int lineHeight = 9; // 8px font + 1px spacing
|
||||
@@ -701,7 +1087,7 @@ public:
|
||||
while (idx < 0) idx += CHANNEL_MSG_HISTORY_SIZE;
|
||||
idx = idx % CHANNEL_MSG_HISTORY_SIZE;
|
||||
|
||||
if (_messages[idx].valid && _messages[idx].channel_idx == _viewChannelIdx) {
|
||||
if (msgMatchesView(_messages[idx])) {
|
||||
channelMsgs[numChannelMsgs++] = idx;
|
||||
}
|
||||
}
|
||||
@@ -968,13 +1354,20 @@ public:
|
||||
|
||||
#if defined(LilyGo_T5S3_EPaper_Pro)
|
||||
display.setCursor(0, footerY);
|
||||
display.print("Swipe:Ch/Scroll");
|
||||
const char* midCh = "Tap:Path";
|
||||
display.setCursor((display.width() - display.getTextWidth(midCh)) / 2, footerY);
|
||||
display.print(midCh);
|
||||
const char* rtCh = "Hold:Compose";
|
||||
display.setCursor(display.width() - display.getTextWidth(rtCh) - 2, footerY);
|
||||
display.print(rtCh);
|
||||
if (_viewChannelIdx == 0xFF) {
|
||||
display.print("Swipe:Scroll");
|
||||
const char* rtCh = "Hold:Reply";
|
||||
display.setCursor(display.width() - display.getTextWidth(rtCh) - 2, footerY);
|
||||
display.print(rtCh);
|
||||
} else {
|
||||
display.print("Swipe:Ch/Scroll");
|
||||
const char* midCh = "Tap:Path";
|
||||
display.setCursor((display.width() - display.getTextWidth(midCh)) / 2, footerY);
|
||||
display.print(midCh);
|
||||
const char* rtCh = "Hold:Compose";
|
||||
display.setCursor(display.width() - display.getTextWidth(rtCh) - 2, footerY);
|
||||
display.print(rtCh);
|
||||
}
|
||||
#else
|
||||
// Left side: abbreviated controls
|
||||
if (_replySelectMode) {
|
||||
@@ -982,6 +1375,17 @@ public:
|
||||
const char* rightText = "Ent:Reply";
|
||||
display.setCursor(display.width() - display.getTextWidth(rightText) - 2, footerY);
|
||||
display.print(rightText);
|
||||
} else if (_viewChannelIdx == 0xFF) {
|
||||
Serial.printf("[DM Footer] perms=%d, contactIdx=%d, inboxMode=%d\n",
|
||||
_dmContactPerms, _dmContactIdx, _dmInboxMode);
|
||||
if (_dmContactPerms > 0) {
|
||||
display.print("Q:Exit L:Admin");
|
||||
} else {
|
||||
display.print("Q:Exit");
|
||||
}
|
||||
const char* rightText = "Ent:Reply";
|
||||
display.setCursor(display.width() - display.getTextWidth(rightText) - 2, footerY);
|
||||
display.print(rightText);
|
||||
} else {
|
||||
display.print("Q:Bck A/D:Ch R:Rply");
|
||||
const char* rightText = "Ent:New";
|
||||
@@ -1080,10 +1484,90 @@ public:
|
||||
return true; // Consume all other keys in reply select
|
||||
}
|
||||
|
||||
// --- DM Inbox mode (two-level DM view) ---
|
||||
if (_viewChannelIdx == 0xFF && _dmInboxMode) {
|
||||
// W - scroll up in inbox
|
||||
if (c == 'w' || c == 'W' || c == 0xF2) {
|
||||
if (_dmInboxScroll > 0) { _dmInboxScroll--; return true; }
|
||||
return false;
|
||||
}
|
||||
// S - scroll down in inbox
|
||||
if (c == 's' || c == 'S' || c == 0xF1) {
|
||||
_dmInboxScroll++; // Clamped during render
|
||||
return true;
|
||||
}
|
||||
// Enter - open conversation for selected sender
|
||||
if (c == '\r' || c == 13) {
|
||||
// Rebuild inbox to find the selected sender name
|
||||
int cur = 0;
|
||||
for (int i = 0; i < _msgCount; i++) {
|
||||
int idx = _newestIdx - i;
|
||||
while (idx < 0) idx += CHANNEL_MSG_HISTORY_SIZE;
|
||||
idx = idx % CHANNEL_MSG_HISTORY_SIZE;
|
||||
if (!_messages[idx].valid || _messages[idx].channel_idx != 0xFF) continue;
|
||||
char sender[24];
|
||||
if (!extractSenderName(_messages[idx].text, sender, sizeof(sender))) continue;
|
||||
// Check if we've seen this sender already
|
||||
bool dup = false;
|
||||
for (int k = 0; k < i; k++) {
|
||||
int ki = _newestIdx - k;
|
||||
while (ki < 0) ki += CHANNEL_MSG_HISTORY_SIZE;
|
||||
ki = ki % CHANNEL_MSG_HISTORY_SIZE;
|
||||
if (!_messages[ki].valid || _messages[ki].channel_idx != 0xFF) continue;
|
||||
char prev[24];
|
||||
if (extractSenderName(_messages[ki].text, prev, sizeof(prev))
|
||||
&& strcmp(prev, sender) == 0) { dup = true; break; }
|
||||
}
|
||||
if (dup) continue;
|
||||
if (cur == _dmInboxScroll) {
|
||||
strncpy(_dmFilterName, sender, sizeof(_dmFilterName) - 1);
|
||||
_dmFilterName[sizeof(_dmFilterName) - 1] = '\0';
|
||||
_dmInboxMode = false;
|
||||
_scrollPos = 0;
|
||||
// Look up contact index
|
||||
_dmContactIdx = -1;
|
||||
_dmContactPerms = 0;
|
||||
uint32_t numC = the_mesh.getNumContacts();
|
||||
ContactInfo ci;
|
||||
for (uint32_t c = 0; c < numC; c++) {
|
||||
if (the_mesh.getContactByIdx(c, ci) && strcmp(ci.name, sender) == 0) {
|
||||
_dmContactIdx = (int)c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
cur++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// Q - let main.cpp handle (back to home)
|
||||
if (c == 'q' || c == 'Q' || c == '\b') {
|
||||
return false;
|
||||
}
|
||||
// A/D pass through to channel switching below
|
||||
if (c == 'a' || c == 'A' || c == 'd' || c == 'D') {
|
||||
// Fall through to channel switching
|
||||
} else {
|
||||
return true; // Consume other keys
|
||||
}
|
||||
}
|
||||
|
||||
// --- DM Conversation mode: Q goes back to inbox ---
|
||||
if (_viewChannelIdx == 0xFF && !_dmInboxMode) {
|
||||
if (c == 'q' || c == 'Q' || c == '\b') {
|
||||
_dmInboxMode = true;
|
||||
_dmFilterName[0] = '\0';
|
||||
_scrollPos = 0;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
int channelMsgCount = getMessageCountForChannel();
|
||||
|
||||
// R - enter reply select mode
|
||||
// R - enter reply select mode (group channels only — DM tab uses Enter to reply)
|
||||
if (c == 'r' || c == 'R') {
|
||||
if (_viewChannelIdx == 0xFF) return false; // Not applicable on DM tab
|
||||
if (channelMsgCount > 0) {
|
||||
_replySelectMode = true;
|
||||
// Start with newest message selected
|
||||
@@ -1120,14 +1604,12 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
// A - previous channel
|
||||
// A - previous channel (includes DM tab at 0xFF)
|
||||
if (c == 'a' || c == 'A') {
|
||||
_replySelectMode = false;
|
||||
_replySelectPos = -1;
|
||||
if (_viewChannelIdx > 0) {
|
||||
_viewChannelIdx--;
|
||||
} else {
|
||||
// Wrap to last valid channel
|
||||
if (_viewChannelIdx == 0xFF) {
|
||||
// DM tab → go to last valid group channel
|
||||
for (uint8_t i = MAX_GROUP_CHANNELS - 1; i > 0; i--) {
|
||||
ChannelDetails ch;
|
||||
if (the_mesh.getChannel(i, ch) && ch.name[0] != '\0') {
|
||||
@@ -1135,22 +1617,39 @@ public:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (_viewChannelIdx > 0) {
|
||||
_viewChannelIdx--;
|
||||
} else {
|
||||
// Channel 0 → wrap to DM tab
|
||||
_viewChannelIdx = 0xFF;
|
||||
_dmInboxMode = true;
|
||||
_dmInboxScroll = 0;
|
||||
_dmFilterName[0] = '\0';
|
||||
}
|
||||
_scrollPos = 0;
|
||||
markChannelRead(_viewChannelIdx);
|
||||
return true;
|
||||
}
|
||||
|
||||
// D - next channel
|
||||
// D - next channel (includes DM tab at 0xFF)
|
||||
if (c == 'd' || c == 'D') {
|
||||
_replySelectMode = false;
|
||||
_replySelectPos = -1;
|
||||
ChannelDetails ch;
|
||||
uint8_t nextIdx = _viewChannelIdx + 1;
|
||||
if (the_mesh.getChannel(nextIdx, ch) && ch.name[0] != '\0') {
|
||||
_viewChannelIdx = nextIdx;
|
||||
} else {
|
||||
if (_viewChannelIdx == 0xFF) {
|
||||
// DM tab → wrap to channel 0
|
||||
_viewChannelIdx = 0;
|
||||
} else {
|
||||
ChannelDetails ch;
|
||||
uint8_t nextIdx = _viewChannelIdx + 1;
|
||||
if (the_mesh.getChannel(nextIdx, ch) && ch.name[0] != '\0') {
|
||||
_viewChannelIdx = nextIdx;
|
||||
} else {
|
||||
// Past last channel → go to DM tab
|
||||
_viewChannelIdx = 0xFF;
|
||||
_dmInboxMode = true;
|
||||
_dmInboxScroll = 0;
|
||||
_dmFilterName[0] = '\0';
|
||||
}
|
||||
}
|
||||
_scrollPos = 0;
|
||||
markChannelRead(_viewChannelIdx);
|
||||
|
||||
@@ -40,6 +40,9 @@ private:
|
||||
// How many rows fit on screen (computed during render)
|
||||
int _rowsPerPage;
|
||||
|
||||
// Pointer to per-contact DM unread array (owned by UITask, set via setter)
|
||||
const uint8_t* _dmUnread = nullptr;
|
||||
|
||||
// --- helpers ---
|
||||
|
||||
static const char* filterLabel(FilterMode f) {
|
||||
@@ -145,6 +148,9 @@ public:
|
||||
|
||||
void invalidateCache() { _cacheValid = false; }
|
||||
|
||||
// Set pointer to per-contact DM unread array (called by UITask after allocation)
|
||||
void setDMUnreadPtr(const uint8_t* ptr) { _dmUnread = ptr; }
|
||||
|
||||
void resetScroll() {
|
||||
_scrollPos = 0;
|
||||
_cacheValid = false;
|
||||
@@ -303,9 +309,14 @@ public:
|
||||
char ageStr[6];
|
||||
formatAge(ageStr, sizeof(ageStr), now, contact.last_advert_timestamp);
|
||||
|
||||
// Build right-side string: "hops age"
|
||||
char rightStr[14];
|
||||
snprintf(rightStr, sizeof(rightStr), "%sh %s", hopStr, ageStr);
|
||||
// Build right-side string: "*N hops age" if unread, else "hops age"
|
||||
int dmCount = (_dmUnread && _filteredIdx[i] < MAX_CONTACTS) ? _dmUnread[_filteredIdx[i]] : 0;
|
||||
char rightStr[20];
|
||||
if (dmCount > 0) {
|
||||
snprintf(rightStr, sizeof(rightStr), "*%d %sh %s", dmCount, hopStr, ageStr);
|
||||
} else {
|
||||
snprintf(rightStr, sizeof(rightStr), "%sh %s", hopStr, ageStr);
|
||||
}
|
||||
int rightWidth = display.getTextWidth(rightStr) + 2;
|
||||
|
||||
// Name region: after prefix + small gap, before right info
|
||||
|
||||
@@ -561,7 +561,9 @@ public:
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.setCursor(0, 0);
|
||||
snprintf(tmp, sizeof(tmp), "Admin: %.16s", _repeaterName);
|
||||
const char* hdrPrefix = (_state == STATE_PASSWORD_ENTRY || _state == STATE_LOGGING_IN)
|
||||
? "Login" : "Admin";
|
||||
snprintf(tmp, sizeof(tmp), "%s: %.16s", hdrPrefix, _repeaterName);
|
||||
display.print(tmp);
|
||||
|
||||
if (_state >= STATE_CATEGORY_MENU && _state <= STATE_RESPONSE_VIEW) {
|
||||
|
||||
@@ -1138,6 +1138,17 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
||||
|
||||
_node_prefs = node_prefs;
|
||||
|
||||
// Initialize message dedup ring buffer
|
||||
memset(_dedup, 0, sizeof(_dedup));
|
||||
_dedupIdx = 0;
|
||||
|
||||
// Allocate per-contact DM unread tracking (PSRAM if available)
|
||||
#if defined(ESP32) && defined(BOARD_HAS_PSRAM)
|
||||
_dmUnread = (uint8_t*)ps_calloc(MAX_CONTACTS, sizeof(uint8_t));
|
||||
#else
|
||||
_dmUnread = new uint8_t[MAX_CONTACTS]();
|
||||
#endif
|
||||
|
||||
#if ENV_INCLUDE_GPS == 1
|
||||
// Apply GPS preferences from stored prefs
|
||||
if (_sensors != NULL && _node_prefs != NULL) {
|
||||
@@ -1184,7 +1195,9 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
||||
home = new HomeScreen(this, &rtc_clock, sensors, node_prefs);
|
||||
msg_preview = new MsgPreviewScreen(this, &rtc_clock);
|
||||
channel_screen = new ChannelScreen(this, &rtc_clock);
|
||||
((ChannelScreen*)channel_screen)->setDMUnreadPtr(_dmUnread);
|
||||
contacts_screen = new ContactsScreen(this, &rtc_clock);
|
||||
((ContactsScreen*)contacts_screen)->setDMUnreadPtr(_dmUnread);
|
||||
text_reader = new TextReaderScreen(this);
|
||||
notes_screen = new NotesScreen(this);
|
||||
settings_screen = new SettingsScreen(this, &rtc_clock, node_prefs);
|
||||
@@ -1266,6 +1279,24 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i
|
||||
const uint8_t* path, int8_t snr) {
|
||||
_msgcount = msgcount;
|
||||
|
||||
// --- Dedup: suppress retry spam (same sender + text within 60s) ---
|
||||
uint32_t nameH = simpleHash(from_name);
|
||||
uint32_t textH = simpleHash(text);
|
||||
unsigned long now = millis();
|
||||
for (int i = 0; i < MSG_DEDUP_SIZE; i++) {
|
||||
if (_dedup[i].name_hash == nameH && _dedup[i].text_hash == textH &&
|
||||
(now - _dedup[i].millis) < MSG_DEDUP_WINDOW_MS) {
|
||||
// Duplicate — suppress UI notification but still queued for BLE sync
|
||||
Serial.printf("[Dedup] Suppressed duplicate from %s\n", from_name);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Record this message in the dedup ring
|
||||
_dedup[_dedupIdx].name_hash = nameH;
|
||||
_dedup[_dedupIdx].text_hash = textH;
|
||||
_dedup[_dedupIdx].millis = now;
|
||||
_dedupIdx = (_dedupIdx + 1) % MSG_DEDUP_SIZE;
|
||||
|
||||
// Add to preview screen (for notifications on non-keyboard devices)
|
||||
((MsgPreviewScreen *) msg_preview)->addPreview(path_len, from_name, text);
|
||||
|
||||
@@ -1282,7 +1313,16 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i
|
||||
}
|
||||
|
||||
// Add to channel history screen with channel index, path data, and SNR
|
||||
((ChannelScreen *) channel_screen)->addMessage(channel_idx, path_len, from_name, text, path, snr);
|
||||
// For DMs (channel_idx == 0xFF), prefix text with sender name so the
|
||||
// DM inbox can extract it later. Channel messages already arrive from
|
||||
// MeshCore in "Sender: message" format.
|
||||
if (channel_idx == 0xFF) {
|
||||
char dmFormatted[CHANNEL_MSG_TEXT_LEN];
|
||||
snprintf(dmFormatted, sizeof(dmFormatted), "%s: %s", from_name, text);
|
||||
((ChannelScreen *) channel_screen)->addMessage(channel_idx, path_len, from_name, dmFormatted, path, snr);
|
||||
} else {
|
||||
((ChannelScreen *) channel_screen)->addMessage(channel_idx, path_len, from_name, text, path, snr);
|
||||
}
|
||||
|
||||
// If user is currently viewing this channel, mark it as read immediately
|
||||
// (they can see the message arrive in real-time)
|
||||
@@ -1290,6 +1330,18 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i
|
||||
((ChannelScreen *) channel_screen)->getViewChannelIdx() == channel_idx) {
|
||||
((ChannelScreen *) channel_screen)->markChannelRead(channel_idx);
|
||||
}
|
||||
|
||||
// Per-contact DM unread tracking: find contact index by name
|
||||
if (channel_idx == 0xFF && _dmUnread) {
|
||||
uint32_t numContacts = the_mesh.getNumContacts();
|
||||
ContactInfo contact;
|
||||
for (uint32_t ci = 0; ci < numContacts; ci++) {
|
||||
if (the_mesh.getContactByIdx(ci, contact) && strcmp(contact.name, from_name) == 0) {
|
||||
if (_dmUnread[ci] < 255) _dmUnread[ci]++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(LilyGo_TDeck_Pro) || defined(LilyGo_T5S3_EPaper_Pro)
|
||||
// Don't interrupt user with popup - just show brief notification
|
||||
@@ -2180,6 +2232,30 @@ void UITask::gotoChannelScreen() {
|
||||
_next_refresh = 100;
|
||||
}
|
||||
|
||||
void UITask::gotoDMTab() {
|
||||
((ChannelScreen *) channel_screen)->setViewChannelIdx(0xFF); // switches + marks read
|
||||
((ChannelScreen *) channel_screen)->resetScroll();
|
||||
setCurrScreen(channel_screen);
|
||||
if (_display != NULL && !_display->isOn()) {
|
||||
_display->turnOn();
|
||||
}
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
_next_refresh = 100;
|
||||
}
|
||||
|
||||
void UITask::gotoDMConversation(const char* contactName, int contactIdx, uint8_t perms) {
|
||||
ChannelScreen* cs = (ChannelScreen*)channel_screen;
|
||||
cs->setViewChannelIdx(0xFF); // enters inbox mode + marks read
|
||||
cs->openConversation(contactName, contactIdx, perms); // switches to conversation mode
|
||||
cs->resetScroll();
|
||||
setCurrScreen(channel_screen);
|
||||
if (_display != NULL && !_display->isOn()) {
|
||||
_display->turnOn();
|
||||
}
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
_next_refresh = 100;
|
||||
}
|
||||
|
||||
void UITask::gotoContactsScreen() {
|
||||
((ContactsScreen *) contacts_screen)->resetScroll();
|
||||
setCurrScreen(contacts_screen);
|
||||
@@ -2288,10 +2364,34 @@ void UITask::addSentChannelMessage(uint8_t channel_idx, const char* sender, cons
|
||||
|
||||
void UITask::markChannelReadFromBLE(uint8_t channel_idx) {
|
||||
((ChannelScreen *) channel_screen)->markChannelRead(channel_idx);
|
||||
// If clearing DMs, also zero all per-contact DM counts
|
||||
if (channel_idx == 0xFF && _dmUnread) {
|
||||
memset(_dmUnread, 0, MAX_CONTACTS * sizeof(uint8_t));
|
||||
}
|
||||
// Trigger a refresh so the home screen unread count updates in real-time
|
||||
_next_refresh = millis() + 200;
|
||||
}
|
||||
|
||||
bool UITask::hasDMUnread(int contactIdx) const {
|
||||
if (!_dmUnread || contactIdx < 0 || contactIdx >= MAX_CONTACTS) return false;
|
||||
return _dmUnread[contactIdx] > 0;
|
||||
}
|
||||
|
||||
int UITask::getDMUnreadCount(int contactIdx) const {
|
||||
if (!_dmUnread || contactIdx < 0 || contactIdx >= MAX_CONTACTS) return 0;
|
||||
return _dmUnread[contactIdx];
|
||||
}
|
||||
|
||||
void UITask::clearDMUnread(int contactIdx) {
|
||||
if (!_dmUnread || contactIdx < 0 || contactIdx >= MAX_CONTACTS) return;
|
||||
int count = _dmUnread[contactIdx];
|
||||
if (count > 0) {
|
||||
_dmUnread[contactIdx] = 0;
|
||||
((ChannelScreen *) channel_screen)->subtractDMUnread(count);
|
||||
_next_refresh = millis() + 200;
|
||||
}
|
||||
}
|
||||
|
||||
void UITask::gotoRepeaterAdmin(int contactIdx) {
|
||||
// Lazy-initialize on first use (same pattern as audiobook player)
|
||||
if (repeater_admin == nullptr) {
|
||||
@@ -2317,6 +2417,17 @@ void UITask::gotoRepeaterAdmin(int contactIdx) {
|
||||
_next_refresh = 100;
|
||||
}
|
||||
|
||||
void UITask::gotoRepeaterAdminDirect(int contactIdx) {
|
||||
// Open admin and auto-submit cached password (skips password screen)
|
||||
_skipRoomRedirect = true; // Don't redirect back to conversation after login
|
||||
gotoRepeaterAdmin(contactIdx);
|
||||
RepeaterAdminScreen* admin = (RepeaterAdminScreen*)repeater_admin;
|
||||
if (admin && admin->getState() == RepeaterAdminScreen::STATE_PASSWORD_ENTRY) {
|
||||
// If password was pre-filled from cache, simulate Enter to submit login
|
||||
admin->handleInput('\r');
|
||||
}
|
||||
}
|
||||
|
||||
void UITask::gotoDiscoveryScreen() {
|
||||
((DiscoveryScreen*)discovery_screen)->resetScroll();
|
||||
setCurrScreen(discovery_screen);
|
||||
@@ -2381,6 +2492,28 @@ void UITask::onAdminLoginResult(bool success, uint8_t permissions, uint32_t serv
|
||||
if (repeater_admin && isOnRepeaterAdmin()) {
|
||||
((RepeaterAdminScreen*)repeater_admin)->onLoginResult(success, permissions, server_time);
|
||||
_next_refresh = 100; // trigger re-render
|
||||
|
||||
if (success) {
|
||||
int cidx = ((RepeaterAdminScreen*)repeater_admin)->getContactIdx();
|
||||
if (cidx >= 0) {
|
||||
clearDMUnread(cidx);
|
||||
|
||||
// Room server login: redirect to conversation view with stored permissions.
|
||||
// Admin users see L:Admin footer to access the admin panel.
|
||||
// Skip redirect if user explicitly pressed L to get to admin.
|
||||
if (!_skipRoomRedirect) {
|
||||
ContactInfo contact;
|
||||
if (the_mesh.getContactByIdx(cidx, contact) && contact.type == ADV_TYPE_ROOM) {
|
||||
uint8_t maskedPerms = permissions & 0x03;
|
||||
Serial.printf("[Admin] Room login (raw=%d, masked=%d) — opening conversation for %s\n",
|
||||
permissions, maskedPerms, contact.name);
|
||||
gotoDMConversation(contact.name, cidx, maskedPerms);
|
||||
return;
|
||||
}
|
||||
}
|
||||
_skipRoomRedirect = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -114,6 +114,26 @@ class UITask : public AbstractUITask {
|
||||
unsigned long _lastLockRefresh = 0; // Periodic lock screen clock update
|
||||
#endif
|
||||
|
||||
// --- Message dedup ring buffer (suppress retry spam at UI level) ---
|
||||
#define MSG_DEDUP_SIZE 8
|
||||
#define MSG_DEDUP_WINDOW_MS 60000 // 60 seconds
|
||||
struct MsgDedup {
|
||||
uint32_t name_hash;
|
||||
uint32_t text_hash;
|
||||
unsigned long millis;
|
||||
};
|
||||
MsgDedup _dedup[MSG_DEDUP_SIZE];
|
||||
int _dedupIdx = 0;
|
||||
|
||||
// --- Per-contact DM unread tracking ---
|
||||
uint8_t* _dmUnread = nullptr; // PSRAM-allocated, MAX_CONTACTS entries
|
||||
|
||||
static uint32_t simpleHash(const char* s) {
|
||||
uint32_t h = 5381;
|
||||
while (*s) { h = ((h << 5) + h) ^ (uint8_t)*s++; }
|
||||
return h;
|
||||
}
|
||||
|
||||
void userLedHandler();
|
||||
|
||||
// Button action handlers
|
||||
@@ -141,6 +161,8 @@ public:
|
||||
|
||||
void gotoHomeScreen();
|
||||
void gotoChannelScreen(); // Navigate to channel message screen
|
||||
void gotoDMTab(); // Navigate directly to DM tab on channel screen
|
||||
void gotoDMConversation(const char* contactName, int contactIdx = -1, uint8_t perms = 0);
|
||||
void gotoContactsScreen(); // Navigate to contacts list
|
||||
void gotoTextReader(); // *** NEW: Navigate to text reader ***
|
||||
void gotoNotesScreen(); // Navigate to notes editor
|
||||
@@ -148,6 +170,7 @@ public:
|
||||
void gotoOnboarding(); // Navigate to settings in onboarding mode
|
||||
void gotoAudiobookPlayer(); // Navigate to audiobook player
|
||||
void gotoRepeaterAdmin(int contactIdx); // Navigate to repeater admin
|
||||
void gotoRepeaterAdminDirect(int contactIdx); // Auto-login admin (L key from conversation)
|
||||
void gotoDiscoveryScreen(); // Navigate to node discovery scan
|
||||
void gotoLastHeardScreen(); // Navigate to last heard passive list
|
||||
#if HAS_GPS
|
||||
@@ -171,6 +194,14 @@ public:
|
||||
}
|
||||
int getMsgCount() const { return _msgcount; }
|
||||
int getUnreadMsgCount() const; // Per-channel unread tracking (standalone)
|
||||
|
||||
// Per-contact DM unread tracking
|
||||
bool hasDMUnread(int contactIdx) const;
|
||||
int getDMUnreadCount(int contactIdx) const;
|
||||
void clearDMUnread(int contactIdx);
|
||||
|
||||
// Flag: suppress room→conversation redirect on next login (L key admin access)
|
||||
bool _skipRoomRedirect = false;
|
||||
bool hasDisplay() const { return _display != NULL; }
|
||||
bool isButtonPressed() const;
|
||||
bool isOnChannelScreen() const { return curr == channel_screen; }
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
#include <Arduino.h>
|
||||
#include "TDeckBoard.h"
|
||||
|
||||
uint32_t deviceOnline = 0x00;
|
||||
|
||||
void TDeckBoard::begin() {
|
||||
|
||||
ESP32Board::begin();
|
||||
|
||||
// Enable peripheral power
|
||||
pinMode(PIN_PERF_POWERON, OUTPUT);
|
||||
digitalWrite(PIN_PERF_POWERON, HIGH);
|
||||
|
||||
// Configure user button
|
||||
pinMode(PIN_USER_BTN, INPUT);
|
||||
|
||||
// Configure LoRa Pins
|
||||
pinMode(P_LORA_MISO, INPUT_PULLUP);
|
||||
// pinMode(P_LORA_DIO_1, INPUT_PULLUP);
|
||||
|
||||
#ifdef P_LORA_TX_LED
|
||||
digitalWrite(P_LORA_TX_LED, HIGH); // inverted pin for SX1276 - HIGH for off
|
||||
#endif
|
||||
|
||||
esp_reset_reason_t reason = esp_reset_reason();
|
||||
if (reason == ESP_RST_DEEPSLEEP) {
|
||||
long wakeup_source = esp_sleep_get_ext1_wakeup_status();
|
||||
if (wakeup_source & (1 << P_LORA_DIO_1)) {
|
||||
startup_reason = BD_STARTUP_RX_PACKET; // received a LoRa packet (while in deep sleep)
|
||||
}
|
||||
|
||||
rtc_gpio_hold_dis((gpio_num_t)P_LORA_NSS);
|
||||
rtc_gpio_deinit((gpio_num_t)P_LORA_DIO_1);
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <Wire.h>
|
||||
#include <Arduino.h>
|
||||
#include "helpers/ESP32Board.h"
|
||||
#include <driver/rtc_io.h>
|
||||
|
||||
#define PIN_VBAT_READ 4
|
||||
#define BATTERY_SAMPLES 8
|
||||
#define ADC_MULTIPLIER (2.0f * 3.3f * 1000)
|
||||
|
||||
class TDeckBoard : public ESP32Board {
|
||||
public:
|
||||
void begin();
|
||||
|
||||
#ifdef P_LORA_TX_LED
|
||||
void onBeforeTransmit() override{
|
||||
digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED on - invert pin for SX1276
|
||||
}
|
||||
|
||||
void onAfterTransmit() override{
|
||||
digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED off - invert pin for SX1276
|
||||
}
|
||||
#endif
|
||||
|
||||
void enterDeepSleep(uint32_t secs, int pin_wake_btn) {
|
||||
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
|
||||
|
||||
// Make sure the DIO1 and NSS GPIOs are hold on required levels during deep sleep
|
||||
rtc_gpio_set_direction((gpio_num_t)P_LORA_DIO_1, RTC_GPIO_MODE_INPUT_ONLY);
|
||||
rtc_gpio_pulldown_en((gpio_num_t)P_LORA_DIO_1);
|
||||
|
||||
rtc_gpio_hold_en((gpio_num_t)P_LORA_NSS);
|
||||
|
||||
if (pin_wake_btn < 0) {
|
||||
esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet
|
||||
} else {
|
||||
esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1) | (1L << pin_wake_btn), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet OR wake btn
|
||||
}
|
||||
|
||||
if (secs > 0) {
|
||||
esp_sleep_enable_timer_wakeup(secs * 1000000);
|
||||
}
|
||||
|
||||
// Finally set ESP32 into sleep
|
||||
esp_deep_sleep_start(); // CPU halts here and never returns!
|
||||
}
|
||||
|
||||
uint16_t getBattMilliVolts() {
|
||||
#if defined(PIN_VBAT_READ) && defined(ADC_MULTIPLIER)
|
||||
analogReadResolution(12);
|
||||
|
||||
uint32_t raw = 0;
|
||||
for (int i = 0; i < BATTERY_SAMPLES; i++) {
|
||||
raw += analogRead(PIN_VBAT_READ);
|
||||
}
|
||||
|
||||
raw = raw / BATTERY_SAMPLES;
|
||||
return (ADC_MULTIPLIER * raw) / 4096;
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
const char* getManufacturerName() const{
|
||||
return "LilyGo T-Deck";
|
||||
}
|
||||
};
|
||||
@@ -1,115 +0,0 @@
|
||||
[LilyGo_TDeck]
|
||||
extends = esp32_base
|
||||
board = t-deck
|
||||
build_flags =
|
||||
${esp32_base.build_flags}
|
||||
${sensor_base.build_flags}
|
||||
-I variants/lilygo_tdeck
|
||||
-D LILYGO_TDECK
|
||||
-D BOARD_HAS_PSRAM=1
|
||||
-D CORE_DEBUG_LEVEL=1
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||
-D PIN_USER_BTN=0 ; Trackball button
|
||||
-D PIN_PERF_POWERON=10 ; Peripheral power pin
|
||||
-D RADIO_CLASS=CustomSX1262
|
||||
-D WRAPPER_CLASS=CustomSX1262Wrapper
|
||||
-D LORA_TX_POWER=22
|
||||
-D SX126X_DIO2_AS_RF_SWITCH=false
|
||||
-D SX126X_CURRENT_LIMIT=140
|
||||
-D SX126X_RX_BOOSTED_GAIN=1
|
||||
-D SX126X_DIO3_TCXO_VOLTAGE=1.8f
|
||||
-D P_LORA_DIO_1=45 ; LORA IRQ pin
|
||||
-D ENV_INCLUDE_GPS=1
|
||||
-D ENV_INCLUDE_AHTX0=0
|
||||
-D ENV_INCLUDE_BME280=0
|
||||
-D ENV_INCLUDE_BMP280=0
|
||||
-D ENV_INCLUDE_SHTC3=0
|
||||
-D ENV_INCLUDE_SHT4X=0
|
||||
-D ENV_INCLUDE_LPS22HB=0
|
||||
-D ENV_INCLUDE_INA3221=0
|
||||
-D ENV_INCLUDE_INA219=0
|
||||
-D ENV_INCLUDE_INA226=0
|
||||
-D ENV_INCLUDE_INA260=0
|
||||
-D ENV_INCLUDE_MLX90614=0
|
||||
-D ENV_INCLUDE_VL53L0X=0
|
||||
-D ENV_INCLUDE_BME680=0
|
||||
-D ENV_INCLUDE_BMP085=0
|
||||
-D P_LORA_NSS=9 ; LORA SS pin
|
||||
-D P_LORA_RESET=17 ; LORA RST pin
|
||||
-D P_LORA_BUSY=13 ; LORA Busy pin
|
||||
-D P_LORA_SCLK=40 ; LORA SCLK pin
|
||||
-D P_LORA_MISO=38 ; LORA MISO pin
|
||||
-D P_LORA_MOSI=41 ; LORA MOSI pin
|
||||
-D DISPLAY_CLASS=ST7789LCDDisplay
|
||||
-D DISPLAY_SCALE_X=2.5
|
||||
-D DISPLAY_SCALE_Y=3.75
|
||||
-D PIN_TFT_RST=-1
|
||||
-D PIN_TFT_VDD_CTL=-1
|
||||
-D PIN_TFT_LEDA_CTL=42
|
||||
-D PIN_TFT_CS=12
|
||||
-D PIN_TFT_DC=11
|
||||
-D PIN_TFT_SCL=40
|
||||
-D PIN_TFT_SDA=41
|
||||
-D PIN_GPS_RX=43
|
||||
-D PIN_GPS_TX=44
|
||||
-D GPS_BAUD_RATE=38400
|
||||
build_src_filter = ${esp32_base.build_src_filter}
|
||||
+<../variants/lilygo_tdeck>
|
||||
+<helpers/sensors/*.cpp>
|
||||
lib_deps =
|
||||
${esp32_base.lib_deps}
|
||||
${sensor_base.lib_deps}
|
||||
adafruit/Adafruit ST7735 and ST7789 Library @ ^1.11.0
|
||||
|
||||
[env:LilyGo_TDeck_companion_radio_usb]
|
||||
extends = LilyGo_TDeck
|
||||
build_flags =
|
||||
${LilyGo_TDeck.build_flags}
|
||||
-I examples/companion_radio/ui-new
|
||||
-D MAX_CONTACTS=350
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
-D OFFLINE_QUEUE_SIZE=256
|
||||
build_src_filter = ${LilyGo_TDeck.build_src_filter}
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
+<../examples/companion_radio/*.cpp>
|
||||
+<../examples/companion_radio/ui-new/*.cpp>
|
||||
+<helpers/ui/ST7789LCDDisplay.cpp>
|
||||
lib_deps =
|
||||
${LilyGo_TDeck.lib_deps}
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
|
||||
[env:LilyGo_TDeck_companion_radio_ble]
|
||||
extends = LilyGo_TDeck
|
||||
build_flags =
|
||||
${LilyGo_TDeck.build_flags}
|
||||
-I examples/companion_radio/ui-new
|
||||
-D MAX_CONTACTS=350
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
-D BLE_PIN_CODE=123456
|
||||
-D OFFLINE_QUEUE_SIZE=256
|
||||
build_src_filter = ${LilyGo_TDeck.build_src_filter}
|
||||
+<helpers/esp32/*.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
+<../examples/companion_radio/*.cpp>
|
||||
+<../examples/companion_radio/ui-new/*.cpp>
|
||||
+<helpers/ui/ST7789LCDDisplay.cpp>
|
||||
lib_deps =
|
||||
${LilyGo_TDeck.lib_deps}
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
|
||||
[env:LilyGo_TDeck_repeater]
|
||||
extends = LilyGo_TDeck
|
||||
build_flags =
|
||||
${LilyGo_TDeck.build_flags}
|
||||
-D ADVERT_NAME='"TDeck Repeater"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
build_src_filter = ${LilyGo_TDeck.build_src_filter}
|
||||
+<../examples/simple_repeater>
|
||||
+<helpers/ui/ST7789LCDDisplay.cpp>
|
||||
lib_deps =
|
||||
${LilyGo_TDeck.lib_deps}
|
||||
${esp32_ota.lib_deps}
|
||||
@@ -1,55 +0,0 @@
|
||||
#include <Arduino.h>
|
||||
#include "target.h"
|
||||
|
||||
TDeckBoard board;
|
||||
|
||||
#if defined(P_LORA_SCLK)
|
||||
static SPIClass spi;
|
||||
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, spi);
|
||||
#else
|
||||
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY);
|
||||
#endif
|
||||
|
||||
WRAPPER_CLASS radio_driver(radio, board);
|
||||
|
||||
ESP32RTCClock fallback_clock;
|
||||
AutoDiscoverRTCClock rtc_clock(fallback_clock);
|
||||
MicroNMEALocationProvider gps(Serial1, &rtc_clock);
|
||||
EnvironmentSensorManager sensors(gps);
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
DISPLAY_CLASS display;
|
||||
MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
|
||||
#endif
|
||||
|
||||
bool radio_init() {
|
||||
fallback_clock.begin();
|
||||
rtc_clock.begin(Wire);
|
||||
Wire.begin(18, 8);
|
||||
|
||||
#if defined(P_LORA_SCLK)
|
||||
return radio.std_init(&spi);
|
||||
#else
|
||||
return radio.std_init();
|
||||
#endif
|
||||
}
|
||||
|
||||
uint32_t radio_get_rng_seed() {
|
||||
return radio.random(0x7FFFFFFF);
|
||||
}
|
||||
|
||||
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) {
|
||||
radio.setFrequency(freq);
|
||||
radio.setSpreadingFactor(sf);
|
||||
radio.setBandwidth(bw);
|
||||
radio.setCodingRate(cr);
|
||||
}
|
||||
|
||||
void radio_set_tx_power(uint8_t dbm) {
|
||||
radio.setOutputPower(dbm);
|
||||
}
|
||||
|
||||
mesh::LocalIdentity radio_new_identity() {
|
||||
RadioNoiseListener rng(radio);
|
||||
return mesh::LocalIdentity(&rng); // create new random identity
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#define RADIOLIB_STATIC_ONLY 1
|
||||
#include <RadioLib.h>
|
||||
#include <helpers/radiolib/RadioLibWrappers.h>
|
||||
#include <helpers/radiolib/CustomSX1262Wrapper.h>
|
||||
#include <TDeckBoard.h>
|
||||
#include <helpers/AutoDiscoverRTCClock.h>
|
||||
#include <helpers/SensorManager.h>
|
||||
#ifdef DISPLAY_CLASS
|
||||
#include <helpers/ui/ST7789LCDDisplay.h>
|
||||
#include <helpers/ui/MomentaryButton.h>
|
||||
#endif
|
||||
#include "helpers/sensors/EnvironmentSensorManager.h"
|
||||
#include "helpers/sensors/MicroNMEALocationProvider.h"
|
||||
|
||||
extern TDeckBoard board;
|
||||
extern WRAPPER_CLASS radio_driver;
|
||||
extern AutoDiscoverRTCClock rtc_clock;
|
||||
extern EnvironmentSensorManager sensors;
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
extern DISPLAY_CLASS display;
|
||||
extern MomentaryButton user_btn;
|
||||
#endif
|
||||
|
||||
bool radio_init();
|
||||
uint32_t radio_get_rng_seed();
|
||||
void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr);
|
||||
void radio_set_tx_power(uint8_t dbm);
|
||||
mesh::LocalIdentity radio_new_identity();
|
||||
Reference in New Issue
Block a user