mirror of
https://github.com/pelgraine/Meck.git
synced 2026-03-28 17:42:44 +01:00
Contacts now in - press N to access
This commit is contained in:
@@ -12,7 +12,7 @@
|
||||
#endif
|
||||
|
||||
#ifndef FIRMWARE_VERSION
|
||||
#define FIRMWARE_VERSION "Meck v0.8"
|
||||
#define FIRMWARE_VERSION "Meck v0.8.1"
|
||||
#endif
|
||||
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "TCA8418Keyboard.h"
|
||||
#include <SD.h>
|
||||
#include "TextReaderScreen.h"
|
||||
#include "ContactsScreen.h"
|
||||
extern SPIClass displaySpi; // From GxEPDDisplay.cpp, shared SPI bus
|
||||
|
||||
TCA8418Keyboard keyboard(I2C_ADDR_KEYBOARD, &Wire);
|
||||
@@ -639,11 +640,18 @@ void handleKeyboardInput() {
|
||||
ui_task.gotoTextReader();
|
||||
break;
|
||||
|
||||
case 'n':
|
||||
case 'N':
|
||||
// Open contacts list
|
||||
Serial.println("Opening contacts");
|
||||
ui_task.gotoContactsScreen();
|
||||
break;
|
||||
|
||||
case 'w':
|
||||
case 'W':
|
||||
// Navigate up/previous (scroll on channel screen)
|
||||
if (ui_task.isOnChannelScreen()) {
|
||||
ui_task.injectKey('w'); // Pass directly for channel switching
|
||||
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen()) {
|
||||
ui_task.injectKey('w'); // Pass directly for channel/contacts switching
|
||||
} else {
|
||||
Serial.println("Nav: Previous");
|
||||
ui_task.injectKey(0xF2); // KEY_PREV
|
||||
@@ -653,8 +661,8 @@ void handleKeyboardInput() {
|
||||
case 's':
|
||||
case 'S':
|
||||
// Navigate down/next (scroll on channel screen)
|
||||
if (ui_task.isOnChannelScreen()) {
|
||||
ui_task.injectKey('s'); // Pass directly for channel switching
|
||||
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen()) {
|
||||
ui_task.injectKey('s'); // Pass directly for channel/contacts switching
|
||||
} else {
|
||||
Serial.println("Nav: Next");
|
||||
ui_task.injectKey(0xF1); // KEY_NEXT
|
||||
@@ -664,8 +672,8 @@ void handleKeyboardInput() {
|
||||
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
|
||||
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen()) {
|
||||
ui_task.injectKey('a'); // Pass directly for channel/contacts switching
|
||||
} else {
|
||||
Serial.println("Nav: Previous");
|
||||
ui_task.injectKey(0xF2); // KEY_PREV
|
||||
@@ -675,8 +683,8 @@ void handleKeyboardInput() {
|
||||
case 'd':
|
||||
case 'D':
|
||||
// Navigate right or switch channel (on channel screen)
|
||||
if (ui_task.isOnChannelScreen()) {
|
||||
ui_task.injectKey('d'); // Pass directly for channel switching
|
||||
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen()) {
|
||||
ui_task.injectKey('d'); // Pass directly for channel/contacts switching
|
||||
} else {
|
||||
Serial.println("Nav: Next");
|
||||
ui_task.injectKey(0xF1); // KEY_NEXT
|
||||
|
||||
324
examples/companion_radio/ui-new/Contactsscreen.h
Normal file
324
examples/companion_radio/ui-new/Contactsscreen.h
Normal file
@@ -0,0 +1,324 @@
|
||||
#pragma once
|
||||
|
||||
#include <helpers/ui/UIScreen.h>
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
#include <MeshCore.h>
|
||||
|
||||
// Forward declarations
|
||||
class UITask;
|
||||
class MyMesh;
|
||||
extern MyMesh the_mesh;
|
||||
|
||||
class ContactsScreen : public UIScreen {
|
||||
public:
|
||||
// Filter modes for contact type
|
||||
enum FilterMode {
|
||||
FILTER_ALL = 0,
|
||||
FILTER_CHAT, // Companions / Chat nodes
|
||||
FILTER_REPEATER,
|
||||
FILTER_ROOM, // Room servers
|
||||
FILTER_SENSOR,
|
||||
FILTER_COUNT // keep last
|
||||
};
|
||||
|
||||
private:
|
||||
UITask* _task;
|
||||
mesh::RTCClock* _rtc;
|
||||
|
||||
int _scrollPos; // Index into filtered list (top visible row)
|
||||
FilterMode _filter; // Current filter mode
|
||||
|
||||
// Cached filtered contact indices for efficient scrolling
|
||||
// We rebuild this on filter change or when entering the screen
|
||||
static const int MAX_VISIBLE = 400; // matches MAX_CONTACTS build flag
|
||||
uint16_t _filteredIdx[MAX_VISIBLE]; // indices into contact table
|
||||
uint32_t _filteredTs[MAX_VISIBLE]; // cached last_advert_timestamp for sorting
|
||||
int _filteredCount; // how many contacts match current filter
|
||||
bool _cacheValid;
|
||||
|
||||
// How many rows fit on screen (computed during render)
|
||||
int _rowsPerPage;
|
||||
|
||||
// --- helpers ---
|
||||
|
||||
static const char* filterLabel(FilterMode f) {
|
||||
switch (f) {
|
||||
case FILTER_ALL: return "All";
|
||||
case FILTER_CHAT: return "Chat";
|
||||
case FILTER_REPEATER: return "Rptr";
|
||||
case FILTER_ROOM: return "Room";
|
||||
case FILTER_SENSOR: return "Sens";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
|
||||
static char typeChar(uint8_t adv_type) {
|
||||
switch (adv_type) {
|
||||
case ADV_TYPE_CHAT: return 'C';
|
||||
case ADV_TYPE_REPEATER: return 'R';
|
||||
case ADV_TYPE_ROOM: return 'S'; // Server
|
||||
default: return '?';
|
||||
}
|
||||
}
|
||||
|
||||
bool matchesFilter(uint8_t adv_type) const {
|
||||
switch (_filter) {
|
||||
case FILTER_ALL: return true;
|
||||
case FILTER_CHAT: return adv_type == ADV_TYPE_CHAT;
|
||||
case FILTER_REPEATER: return adv_type == ADV_TYPE_REPEATER;
|
||||
case FILTER_ROOM: return adv_type == ADV_TYPE_ROOM;
|
||||
case FILTER_SENSOR: return (adv_type != ADV_TYPE_CHAT &&
|
||||
adv_type != ADV_TYPE_REPEATER &&
|
||||
adv_type != ADV_TYPE_ROOM);
|
||||
default: return true;
|
||||
}
|
||||
}
|
||||
|
||||
void rebuildCache() {
|
||||
_filteredCount = 0;
|
||||
uint32_t numContacts = the_mesh.getNumContacts();
|
||||
ContactInfo contact;
|
||||
for (uint32_t i = 0; i < numContacts && _filteredCount < MAX_VISIBLE; i++) {
|
||||
if (the_mesh.getContactByIdx(i, contact)) {
|
||||
if (matchesFilter(contact.type)) {
|
||||
_filteredIdx[_filteredCount] = (uint16_t)i;
|
||||
_filteredTs[_filteredCount] = contact.last_advert_timestamp;
|
||||
_filteredCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Sort by last_advert_timestamp descending (most recently seen first)
|
||||
// Simple insertion sort — fine for up to 400 entries on ESP32
|
||||
for (int i = 1; i < _filteredCount; i++) {
|
||||
uint16_t tmpIdx = _filteredIdx[i];
|
||||
uint32_t tmpTs = _filteredTs[i];
|
||||
int j = i - 1;
|
||||
while (j >= 0 && _filteredTs[j] < tmpTs) {
|
||||
_filteredIdx[j + 1] = _filteredIdx[j];
|
||||
_filteredTs[j + 1] = _filteredTs[j];
|
||||
j--;
|
||||
}
|
||||
_filteredIdx[j + 1] = tmpIdx;
|
||||
_filteredTs[j + 1] = tmpTs;
|
||||
}
|
||||
_cacheValid = true;
|
||||
// Clamp scroll position
|
||||
if (_scrollPos >= _filteredCount) {
|
||||
_scrollPos = (_filteredCount > 0) ? _filteredCount - 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Format seconds-ago as compact string: "3s" "5m" "2h" "4d" "??"
|
||||
static void formatAge(char* buf, size_t bufLen, uint32_t now, uint32_t timestamp) {
|
||||
if (timestamp == 0) {
|
||||
strncpy(buf, "--", bufLen);
|
||||
return;
|
||||
}
|
||||
int secs = (int)(now - timestamp);
|
||||
if (secs < 0) secs = 0;
|
||||
if (secs < 60) {
|
||||
snprintf(buf, bufLen, "%ds", secs);
|
||||
} else if (secs < 3600) {
|
||||
snprintf(buf, bufLen, "%dm", secs / 60);
|
||||
} else if (secs < 86400) {
|
||||
snprintf(buf, bufLen, "%dh", secs / 3600);
|
||||
} else {
|
||||
snprintf(buf, bufLen, "%dd", secs / 86400);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
ContactsScreen(UITask* task, mesh::RTCClock* rtc)
|
||||
: _task(task), _rtc(rtc), _scrollPos(0), _filter(FILTER_ALL),
|
||||
_filteredCount(0), _cacheValid(false), _rowsPerPage(5) {}
|
||||
|
||||
void invalidateCache() { _cacheValid = false; }
|
||||
|
||||
void resetScroll() {
|
||||
_scrollPos = 0;
|
||||
_cacheValid = false;
|
||||
}
|
||||
|
||||
FilterMode getFilter() const { return _filter; }
|
||||
|
||||
// Get the raw contact table index for the currently highlighted item
|
||||
// Returns -1 if no valid selection
|
||||
int getSelectedContactIdx() const {
|
||||
if (_filteredCount == 0) return -1;
|
||||
return _filteredIdx[_scrollPos];
|
||||
}
|
||||
|
||||
int render(DisplayDriver& display) override {
|
||||
if (!_cacheValid) rebuildCache();
|
||||
|
||||
char tmp[48];
|
||||
|
||||
// === Header ===
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.setCursor(0, 0);
|
||||
snprintf(tmp, sizeof(tmp), "Contacts [%s]", filterLabel(_filter));
|
||||
display.print(tmp);
|
||||
|
||||
// Count on right
|
||||
snprintf(tmp, sizeof(tmp), "%d/%d", _filteredCount, (int)the_mesh.getNumContacts());
|
||||
display.setCursor(display.width() - display.getTextWidth(tmp) - 2, 0);
|
||||
display.print(tmp);
|
||||
|
||||
// Divider
|
||||
display.drawRect(0, 11, display.width(), 1);
|
||||
|
||||
// === Body - contact rows ===
|
||||
display.setTextSize(0); // tiny font for compact rows
|
||||
int lineHeight = 9; // 8px font + 1px gap
|
||||
int headerHeight = 14;
|
||||
int footerHeight = 14;
|
||||
int maxY = display.height() - footerHeight;
|
||||
int y = headerHeight;
|
||||
|
||||
uint32_t now = _rtc->getCurrentTime();
|
||||
int rowsDrawn = 0;
|
||||
|
||||
if (_filteredCount == 0) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setCursor(0, y);
|
||||
display.print("No contacts");
|
||||
display.setCursor(0, y + lineHeight);
|
||||
display.print("A/D: Change filter");
|
||||
} else {
|
||||
// Center visible window around selected item (TextReaderScreen pattern)
|
||||
int maxVisible = (maxY - headerHeight) / lineHeight;
|
||||
if (maxVisible < 3) maxVisible = 3;
|
||||
int startIdx = max(0, min(_scrollPos - maxVisible / 2,
|
||||
_filteredCount - maxVisible));
|
||||
int endIdx = min(_filteredCount, startIdx + maxVisible);
|
||||
|
||||
for (int i = startIdx; i < endIdx && y + lineHeight <= maxY; i++) {
|
||||
ContactInfo contact;
|
||||
if (!the_mesh.getContactByIdx(_filteredIdx[i], contact)) continue;
|
||||
|
||||
bool selected = (i == _scrollPos);
|
||||
|
||||
// Highlight: fill LIGHT rect first, then draw DARK text on top
|
||||
if (selected) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.fillRect(0, y + 5, display.width(), lineHeight);
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
} else {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
}
|
||||
|
||||
// Set cursor AFTER fillRect so text draws on top of highlight
|
||||
display.setCursor(0, y);
|
||||
|
||||
// Prefix: "> " for selected, type char + space for others
|
||||
char prefix[4];
|
||||
if (selected) {
|
||||
snprintf(prefix, sizeof(prefix), ">%c", typeChar(contact.type));
|
||||
} else {
|
||||
snprintf(prefix, sizeof(prefix), " %c", typeChar(contact.type));
|
||||
}
|
||||
display.print(prefix);
|
||||
|
||||
// Contact name (truncated to fit)
|
||||
char filteredName[32];
|
||||
display.translateUTF8ToBlocks(filteredName, contact.name, sizeof(filteredName));
|
||||
|
||||
// Reserve space for hops + age on right side
|
||||
char hopStr[6];
|
||||
if (contact.out_path_len == 0xFF || contact.out_path_len == 0) {
|
||||
strcpy(hopStr, "D"); // direct
|
||||
} else {
|
||||
snprintf(hopStr, sizeof(hopStr), "%d", contact.out_path_len);
|
||||
}
|
||||
|
||||
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);
|
||||
int rightWidth = display.getTextWidth(rightStr) + 2;
|
||||
|
||||
// Name region: after prefix + small gap, before right info
|
||||
int nameX = display.getTextWidth(prefix) + 2;
|
||||
int nameMaxW = display.width() - nameX - rightWidth - 2;
|
||||
display.drawTextEllipsized(nameX, y, nameMaxW, filteredName);
|
||||
|
||||
// Right-aligned: hops + age
|
||||
display.setCursor(display.width() - rightWidth, y);
|
||||
display.print(rightStr);
|
||||
|
||||
y += lineHeight;
|
||||
rowsDrawn++;
|
||||
}
|
||||
_rowsPerPage = (rowsDrawn > 0) ? rowsDrawn : 1;
|
||||
}
|
||||
|
||||
display.setTextSize(1); // restore for footer
|
||||
|
||||
// === Footer ===
|
||||
int footerY = display.height() - 12;
|
||||
display.drawRect(0, footerY - 2, display.width(), 1);
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
|
||||
// Left: Q:Back
|
||||
display.setCursor(0, footerY);
|
||||
display.print("Q:Back");
|
||||
|
||||
// Center: A/D:Filter
|
||||
const char* mid = "A/D:Filtr";
|
||||
display.setCursor((display.width() - display.getTextWidth(mid)) / 2, footerY);
|
||||
display.print(mid);
|
||||
|
||||
// Right: W/S:Scroll
|
||||
const char* right = "W/S:Scrll";
|
||||
display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY);
|
||||
display.print(right);
|
||||
|
||||
return 5000; // e-ink: next render after 5s
|
||||
}
|
||||
|
||||
bool handleInput(char c) override {
|
||||
// W - scroll up (previous contact)
|
||||
if (c == 'w' || c == 'W' || c == 0xF2) {
|
||||
if (_scrollPos > 0) {
|
||||
_scrollPos--;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// S - scroll down (next contact)
|
||||
if (c == 's' || c == 'S' || c == 0xF1) {
|
||||
if (_scrollPos < _filteredCount - 1) {
|
||||
_scrollPos++;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// A - previous filter
|
||||
if (c == 'a' || c == 'A') {
|
||||
_filter = (FilterMode)(((int)_filter + FILTER_COUNT - 1) % FILTER_COUNT);
|
||||
_scrollPos = 0;
|
||||
_cacheValid = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// D - next filter
|
||||
if (c == 'd' || c == 'D') {
|
||||
_filter = (FilterMode)(((int)_filter + 1) % FILTER_COUNT);
|
||||
_scrollPos = 0;
|
||||
_cacheValid = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Enter - select contact (future: open RepeaterAdmin for repeaters)
|
||||
if (c == 13 || c == KEY_ENTER) {
|
||||
// TODO Phase 3: if selected contact is a repeater, open RepeaterAdminScreen
|
||||
// For now, just acknowledge the selection
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -31,6 +31,7 @@
|
||||
|
||||
#include "icons.h"
|
||||
#include "ChannelScreen.h"
|
||||
#include "ContactsScreen.h"
|
||||
#include "TextReaderScreen.h"
|
||||
|
||||
class SplashScreen : public UIScreen {
|
||||
@@ -605,6 +606,7 @@ 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);
|
||||
contacts_screen = new ContactsScreen(this, &rtc_clock);
|
||||
text_reader = new TextReaderScreen(this);
|
||||
setCurrScreen(splash);
|
||||
}
|
||||
@@ -998,6 +1000,16 @@ void UITask::gotoChannelScreen() {
|
||||
_next_refresh = 100;
|
||||
}
|
||||
|
||||
void UITask::gotoContactsScreen() {
|
||||
((ContactsScreen *) contacts_screen)->resetScroll();
|
||||
setCurrScreen(contacts_screen);
|
||||
if (_display != NULL && !_display->isOn()) {
|
||||
_display->turnOn();
|
||||
}
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
_next_refresh = 100;
|
||||
}
|
||||
|
||||
void UITask::gotoTextReader() {
|
||||
TextReaderScreen* reader = (TextReaderScreen*)text_reader;
|
||||
if (_display != NULL) {
|
||||
|
||||
@@ -52,6 +52,7 @@ class UITask : public AbstractUITask {
|
||||
UIScreen* home;
|
||||
UIScreen* msg_preview;
|
||||
UIScreen* channel_screen; // Channel message history screen
|
||||
UIScreen* contacts_screen; // Contacts list screen
|
||||
UIScreen* text_reader; // *** NEW: Text reader screen ***
|
||||
UIScreen* curr;
|
||||
|
||||
@@ -76,6 +77,7 @@ public:
|
||||
|
||||
void gotoHomeScreen() { setCurrScreen(home); }
|
||||
void gotoChannelScreen(); // Navigate to channel message screen
|
||||
void gotoContactsScreen(); // Navigate to contacts list
|
||||
void gotoTextReader(); // *** NEW: Navigate to text reader ***
|
||||
void showAlert(const char* text, int duration_millis) override;
|
||||
void forceRefresh() override { _next_refresh = 100; }
|
||||
@@ -83,6 +85,7 @@ public:
|
||||
bool hasDisplay() const { return _display != NULL; }
|
||||
bool isButtonPressed() const;
|
||||
bool isOnChannelScreen() const { return curr == channel_screen; }
|
||||
bool isOnContactsScreen() const { return curr == contacts_screen; }
|
||||
bool isOnTextReader() const { return curr == text_reader; } // *** NEW ***
|
||||
uint8_t getChannelScreenViewIdx() const;
|
||||
|
||||
@@ -100,6 +103,7 @@ public:
|
||||
UIScreen* getCurrentScreen() const { return curr; }
|
||||
UIScreen* getMsgPreviewScreen() const { return msg_preview; }
|
||||
UIScreen* getTextReaderScreen() const { return text_reader; } // *** NEW ***
|
||||
UIScreen* getContactsScreen() const { return contacts_screen; }
|
||||
|
||||
// from AbstractUITask
|
||||
void msgRead(int msgcount) override;
|
||||
|
||||
Reference in New Issue
Block a user