1
0
forked from iarv/Meck

Compare commits

..

2 Commits

Author SHA1 Message Date
pelgraine
33c2758a87 updated readme 2026-02-10 16:04:03 +11:00
pelgraine
f644892b07 standalone device phase 1 complete - utc offset from gps homepage and gps timesync without ble enabled 2026-02-10 15:59:34 +11:00
8 changed files with 198 additions and 188 deletions

View File

@@ -19,6 +19,34 @@ The T-Deck Pro BLE companion firmware includes full keyboard support for standal
| R | Open e-book reader |
| Q | Back to home screen |
### Bluetooth (BLE)
BLE is **disabled by default** at boot to support standalone-first operation. The device is fully functional without a phone — you can send and receive messages, browse contacts, read e-books, and set your timezone directly from the keyboard.
To connect to the MeshCore companion app, navigate to the **Bluetooth** home page (use D to page through) and press **Enter** to toggle BLE on. The BLE PIN will be displayed on screen. Toggle it off again the same way when you're done.
### Clock & Timezone
The T-Deck Pro does not include a dedicated RTC chip, so after each reboot the device clock starts unset. The clock will appear in the nav bar (between node name and battery) once the time has been synced by one of two methods:
1. **GPS fix** (standalone) — Once the GPS acquires a satellite fix, the time is automatically synced from the NMEA data. No phone or BLE connection required. Typical time to first fix is 3090 seconds outdoors with clear sky.
2. **BLE companion app** — If BLE is enabled and connected to the MeshCore companion app, the app will push the current time to the device.
**Setting your timezone:**
Navigate to the **GPS** home page and press **U** to open the UTC offset editor.
| Key | Action |
|-----|--------|
| W | Increase offset (+1 hour) |
| S | Decrease offset (-1 hour) |
| Enter | Save and exit |
| Q | Cancel and exit |
The UTC offset is persisted to flash and survives reboots — you only need to set it once. The valid range is UTC-12 to UTC+14. For example, AEST is UTC+10 and AEDT is UTC+11.
The GPS page also shows the current time, satellite count, position, altitude, and your configured UTC offset for reference.
### Channel Message Screen
| Key | Action |
@@ -171,6 +199,8 @@ Download a copy of the Meck firmware bin from https://github.com/pelgraine/Meck/
The companion firmware can be connected to via BLE. USB is planned for a future update.
> **Note:** On the T-Deck Pro, BLE is disabled by default at boot. Navigate to the Bluetooth home page and press Enter to enable BLE before connecting with a companion app.
- Web: https://app.meshcore.nz
- Android: https://play.google.com/store/apps/details?id=com.liamcottle.meshcore.android
- iOS: https://apps.apple.com/us/app/meshcore/id6742354151?platform=iphone
@@ -204,6 +234,7 @@ There are a number of fairly major features in the pipeline, with no particular
- [X] Standalone DM functionality for Companion BLE firmware
- [X] Contacts list with filtering for Companion BLE firmware
- [X] Standalone repeater admin access for Companion BLE firmware
- [X] GPS time sync with on-device timezone setting
- [ ] Companion radio: USB
- [ ] Simple Repeater firmware for the T-Deck Pro
- [ ] Get pin 45 with the screen backlight functioning for the T-Deck Pro v1.1

View File

@@ -228,6 +228,7 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
file.read((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled)); // 85
file.read((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval)); // 86
file.read((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); // 87
file.read((uint8_t *)&_prefs.utc_offset_hours, sizeof(_prefs.utc_offset_hours)); // 88
file.close();
}
@@ -263,6 +264,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
file.write((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled)); // 85
file.write((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval)); // 86
file.write((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); // 87
file.write((uint8_t *)&_prefs.utc_offset_hours, sizeof(_prefs.utc_offset_hours)); // 88
file.close();
}
@@ -598,4 +600,4 @@ bool DataStore::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src
}
return false; // error
}
#endif
#endif

View File

@@ -523,13 +523,6 @@ void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint3
const char *text) {
markConnectionActive(from); // in case this is from a server, and we have a connection
queueMessage(from, TXT_TYPE_CLI_DATA, pkt, sender_timestamp, NULL, 0, text);
#ifdef DISPLAY_CLASS
// Route CLI responses to admin UI if active
if (_ui && _admin_contact_idx >= 0) {
_ui->onAdminCliResponse(from.name, text);
}
#endif
}
void MyMesh::onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t sender_timestamp,
@@ -657,55 +650,6 @@ bool MyMesh::uiSendDirectMessage(uint32_t contact_idx, const char* text) {
return true;
}
bool MyMesh::uiLoginToRepeater(uint32_t contact_idx, const char* password) {
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 est_timeout;
int result = sendLogin(*recipient, password, est_timeout);
if (result == MSG_SEND_FAILED) {
MESH_DEBUG_PRINTLN("UI: Admin login send failed to %s", recipient->name);
return false;
}
clearPendingReqs();
memcpy(&pending_login, recipient->id.pub_key, 4);
_admin_contact_idx = contact_idx;
MESH_DEBUG_PRINTLN("UI: Admin login sent to %s (%s), timeout=%dms",
recipient->name, result == MSG_SEND_SENT_FLOOD ? "flood" : "direct",
est_timeout);
return true;
}
bool MyMesh::uiSendCliCommand(uint32_t contact_idx, const char* command) {
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 est_timeout;
int result = sendCommandData(*recipient, timestamp, 0, command, est_timeout);
if (result == MSG_SEND_FAILED) {
MESH_DEBUG_PRINTLN("UI: CLI command send failed to %s: %s", recipient->name, command);
return false;
}
_admin_contact_idx = contact_idx;
MESH_DEBUG_PRINTLN("UI: CLI command sent to %s (%s): %s, timeout=%dms",
recipient->name, result == MSG_SEND_SENT_FLOOD ? "flood" : "direct",
command, 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) {
@@ -764,11 +708,6 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
out_frame[i++] = 0; // legacy: is_admin = false
memcpy(&out_frame[i], contact.id.pub_key, 6);
i += 6; // pub_key_prefix
#ifdef DISPLAY_CLASS
// Notify UI of successful legacy login
if (_ui) _ui->onAdminLoginResult(true, 0, tag);
#endif
} else if (data[4] == RESP_SERVER_LOGIN_OK) { // new login response
uint16_t keep_alive_secs = ((uint16_t)data[5]) * 16;
if (keep_alive_secs > 0) {
@@ -782,21 +721,11 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
i += 4; // NEW: include server timestamp
out_frame[i++] = data[7]; // NEW (v7): ACL permissions
out_frame[i++] = data[12]; // FIRMWARE_VER_LEVEL
#ifdef DISPLAY_CLASS
// Notify UI of successful login
if (_ui) _ui->onAdminLoginResult(true, data[6], tag);
#endif
} else {
out_frame[i++] = PUSH_CODE_LOGIN_FAIL;
out_frame[i++] = 0; // reserved
memcpy(&out_frame[i], contact.id.pub_key, 6);
i += 6; // pub_key_prefix
#ifdef DISPLAY_CLASS
// Notify UI of login failure
if (_ui) _ui->onAdminLoginResult(false, 0, 0);
#endif
}
_serial->writeFrame(out_frame, i);
} else if (len > 4 && // check for status response
@@ -968,7 +897,6 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
memset(send_scope.key, 0, sizeof(send_scope.key));
memset(_sent_track, 0, sizeof(_sent_track));
_sent_track_idx = 0;
_admin_contact_idx = -1;
// defaults
memset(&_prefs, 0, sizeof(_prefs));
@@ -1022,6 +950,7 @@ void MyMesh::begin(bool has_display) {
_prefs.buzzer_quiet = constrain(_prefs.buzzer_quiet, 0, 1); // Ensure boolean 0 or 1
_prefs.gps_enabled = constrain(_prefs.gps_enabled, 0, 1); // Ensure boolean 0 or 1
_prefs.gps_interval = constrain(_prefs.gps_interval, 0, 86400); // Max 24 hours
_prefs.utc_offset_hours = constrain(_prefs.utc_offset_hours, -12, 14); // Valid timezone range
#ifdef BLE_PIN_CODE // 123456 by default
if (_prefs.ble_pin == 0) {
@@ -1805,6 +1734,12 @@ void MyMesh::handleCmdFrame(size_t len) {
savePrefs();
}
#endif
// UTC offset for local clock display (works regardless of GPS)
if (strcmp(sp, "utc_offset") == 0) {
int offset = atoi(np);
_prefs.utc_offset_hours = constrain(offset, -12, 14);
savePrefs();
}
writeOKFrame();
} else {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);

View File

@@ -12,7 +12,7 @@
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "Meck v0.8.1"
#define FIRMWARE_VERSION "Meck v0.8.2"
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)

View File

@@ -28,4 +28,5 @@ struct NodePrefs { // persisted to file
uint8_t gps_enabled; // GPS enabled flag (0=disabled, 1=enabled)
uint32_t gps_interval; // GPS read interval in seconds
uint8_t autoadd_config; // bitmask for auto-add contacts config
int8_t utc_offset_hours; // UTC offset in hours (-12 to +14), default 0
};

View File

@@ -10,7 +10,6 @@
#include <SD.h>
#include "TextReaderScreen.h"
#include "ContactsScreen.h"
#include "RepeaterAdminScreen.h"
extern SPIClass displaySpi; // From GxEPDDisplay.cpp, shared SPI bus
TCA8418Keyboard keyboard(I2C_ADDR_KEYBOARD, &Wire);
@@ -380,6 +379,13 @@ void setup() {
MESH_DEBUG_PRINTLN("setup() - GPS enabled by default");
#endif
// T-Deck Pro: BLE starts disabled for standalone-first operation
// User can toggle it on from the Bluetooth home page (Enter or long-press)
#if defined(LilyGo_TDeck_Pro) && defined(BLE_PIN_CODE)
serial_interface.disable();
MESH_DEBUG_PRINTLN("setup() - BLE disabled at boot (standalone mode)");
#endif
MESH_DEBUG_PRINTLN("=== setup() - COMPLETE ===");
}
@@ -631,56 +637,6 @@ void handleKeyboardInput() {
return;
}
// *** REPEATER ADMIN MODE ***
if (ui_task.isOnRepeaterAdmin()) {
RepeaterAdminScreen* admin = (RepeaterAdminScreen*)ui_task.getRepeaterAdminScreen();
RepeaterAdminScreen::AdminState astate = admin->getState();
// In password entry, pass all printable chars and special keys through
// Q only navigates back if password is empty (handleInput returns false)
if (astate == RepeaterAdminScreen::STATE_PASSWORD_ENTRY) {
if (key == 'q' || key == 'Q') {
// Try passing to screen - if not handled (empty password), navigate back
bool handled = admin->handleInput(key);
if (!handled) {
Serial.println("Nav: Back to contacts from admin login");
ui_task.gotoContactsScreen();
}
ui_task.forceRefresh();
} else {
ui_task.injectKey(key);
}
return;
}
// In menu state
if (astate == RepeaterAdminScreen::STATE_MENU) {
if (key == 'q' || key == 'Q') {
Serial.println("Nav: Back to contacts from admin menu");
ui_task.gotoContactsScreen();
return;
}
// C key: allow entering compose mode from admin menu
if (key == 'c' || key == 'C') {
composeDM = false;
composeDMContactIdx = -1;
composeMode = true;
composeBuffer[0] = '\0';
composePos = 0;
drawComposeScreen();
lastComposeRefresh = millis();
return;
}
// All other keys pass to admin screen
ui_task.injectKey(key);
return;
}
// In waiting/response/error states, pass all keys through
ui_task.injectKey(key);
return;
}
// Normal mode - not composing
switch (key) {
case 'c':
@@ -741,7 +697,7 @@ void handleKeyboardInput() {
case 'w':
case 'W':
// Navigate up/previous (scroll on channel screen)
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen() || ui_task.isOnRepeaterAdmin()) {
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen()) {
ui_task.injectKey('w'); // Pass directly for channel/contacts switching
} else {
Serial.println("Nav: Previous");
@@ -752,7 +708,7 @@ void handleKeyboardInput() {
case 's':
case 'S':
// Navigate down/next (scroll on channel screen)
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen() || ui_task.isOnRepeaterAdmin()) {
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen()) {
ui_task.injectKey('s'); // Pass directly for channel/contacts switching
} else {
Serial.println("Nav: Next");
@@ -784,7 +740,6 @@ void handleKeyboardInput() {
case '\r':
// Select/Enter - if on contacts screen, enter DM compose for chat contacts
// or repeater admin for repeater contacts
if (ui_task.isOnContactsScreen()) {
ContactsScreen* cs = (ContactsScreen*)ui_task.getContactsScreen();
int idx = cs->getSelectedContactIdx();
@@ -799,15 +754,9 @@ void handleKeyboardInput() {
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) {
// Non-chat, non-repeater contact (room, sensor, etc.) - future use
Serial.printf("Selected contact type=%d idx=%d\n", ctype, idx);
// 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");
@@ -818,9 +767,21 @@ void handleKeyboardInput() {
case 'q':
case 'Q':
case '\b':
// Go back to home screen (admin mode handled above)
Serial.println("Nav: Back to home");
ui_task.gotoHomeScreen();
// If editing UTC offset on GPS page, pass through to cancel
if (ui_task.isEditingHomeScreen()) {
ui_task.injectKey('q');
} else {
// Go back to home screen
Serial.println("Nav: Back to home");
ui_task.gotoHomeScreen();
}
break;
case 'u':
case 'U':
// UTC offset editing (on GPS home page)
Serial.println("Nav: UTC offset");
ui_task.injectKey('u');
break;
case ' ':

View File

@@ -2,7 +2,6 @@
#include <helpers/TxtDataHelpers.h>
#include "../MyMesh.h"
#include "target.h"
#include "RepeaterAdminScreen.h"
#ifdef WIFI_SSID
#include <WiFi.h>
#endif
@@ -103,6 +102,8 @@ class HomeScreen : public UIScreen {
NodePrefs* _node_prefs;
uint8_t _page;
bool _shutdown_init;
bool _editing_utc;
int8_t _saved_utc_offset; // for cancel/undo
AdvertPath recent[UI_RECENT_LIST_SIZE];
@@ -184,7 +185,15 @@ void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts)
public:
HomeScreen(UITask* task, mesh::RTCClock* rtc, SensorManager* sensors, NodePrefs* node_prefs)
: _task(task), _rtc(rtc), _sensors(sensors), _node_prefs(node_prefs), _page(0),
_shutdown_init(false), sensors_lpp(200) { }
_shutdown_init(false), _editing_utc(false), _saved_utc_offset(0), sensors_lpp(200) { }
bool isEditingUTC() const { return _editing_utc; }
void cancelEditUTC() {
if (_editing_utc) {
_node_prefs->utc_offset_hours = _saved_utc_offset;
_editing_utc = false;
}
}
void poll() override {
if (_shutdown_init && !_task->isButtonPressed()) { // must wait for USR button to be released
@@ -205,6 +214,29 @@ public:
// battery voltage
renderBatteryIndicator(display, _task->getBattMilliVolts());
// centered clock (tinyfont) - only show when time is valid
{
uint32_t now = _rtc->getCurrentTime();
if (now > 1700000000) { // valid timestamp (after ~Nov 2023)
// Apply UTC offset from prefs
int32_t local = (int32_t)now + ((int32_t)_node_prefs->utc_offset_hours * 3600);
int hrs = (local / 3600) % 24;
if (hrs < 0) hrs += 24;
int mins = (local / 60) % 60;
if (mins < 0) mins += 60;
char timeBuf[6];
sprintf(timeBuf, "%02d:%02d", hrs, mins);
display.setTextSize(0); // tinyfont
display.setColor(DisplayDriver::LIGHT);
uint16_t tw = display.getTextWidth(timeBuf);
int clockX = (display.width() - tw) / 2;
display.setCursor(clockX, -3); // align with battery text Y
display.print(timeBuf);
display.setTextSize(1); // restore
}
}
// curr page indicator
int y = 14;
int x = display.width() / 2 - 5 * (HomePage::Count-1);
@@ -332,6 +364,42 @@ public:
display.drawTextRightAlign(display.width()-1, y, buf);
y = y + 12;
}
// Show RTC time and UTC offset on GPS page
{
uint32_t now = _rtc->getCurrentTime();
if (now > 1700000000) {
int32_t local = (int32_t)now + ((int32_t)_node_prefs->utc_offset_hours * 3600);
int hrs = (local / 3600) % 24;
if (hrs < 0) hrs += 24;
int mins = (local / 60) % 60;
if (mins < 0) mins += 60;
display.drawTextLeftAlign(0, y, "time(U)");
sprintf(buf, "%02d:%02d UTC%+d", hrs, mins, _node_prefs->utc_offset_hours);
display.drawTextRightAlign(display.width()-1, y, buf);
} else {
display.drawTextLeftAlign(0, y, "time(U)");
display.drawTextRightAlign(display.width()-1, y, "no sync");
}
}
// UTC offset editor overlay
if (_editing_utc) {
// Draw background box
int bx = 4, by = 20, bw = display.width() - 8, bh = 40;
display.setColor(DisplayDriver::DARK);
display.fillRect(bx, by, bw, bh);
display.setColor(DisplayDriver::LIGHT);
display.drawRect(bx, by, bw, bh);
// Show current offset value
display.setTextSize(2);
sprintf(buf, "UTC%+d", _node_prefs->utc_offset_hours);
display.drawTextCentered(display.width() / 2, by + 4, buf);
// Show controls hint
display.setTextSize(0);
display.drawTextCentered(display.width() / 2, by + bh - 10, "W/S:adj Enter:ok Q:cancel");
display.setTextSize(1);
}
#endif
#if UI_SENSORS_PAGE == 1
} else if (_page == HomePage::SENSORS) {
@@ -415,10 +483,44 @@ public:
display.drawTextCentered(display.width() / 2, 64 - 11, "hibernate:" PRESS_LABEL);
}
}
return 5000; // next render after 5000 ms
return _editing_utc ? 700 : 5000; // match e-ink refresh cycle while editing UTC
}
bool handleInput(char c) override {
// UTC offset editing mode - intercept all keys
if (_editing_utc) {
if (c == 'w' || c == KEY_PREV) {
// Increment offset
if (_node_prefs->utc_offset_hours < 14) {
_node_prefs->utc_offset_hours++;
}
return true;
}
if (c == 's' || c == KEY_NEXT) {
// Decrement offset
if (_node_prefs->utc_offset_hours > -12) {
_node_prefs->utc_offset_hours--;
}
return true;
}
if (c == KEY_ENTER) {
// Save and exit
Serial.printf("UTC offset saving: %d\n", _node_prefs->utc_offset_hours);
the_mesh.savePrefs();
_editing_utc = false;
_task->showAlert("UTC offset saved", 800);
Serial.println("UTC offset save complete");
return true;
}
if (c == 'q' || c == 'u') {
// Cancel - restore original value
_node_prefs->utc_offset_hours = _saved_utc_offset;
_editing_utc = false;
return true;
}
return true; // Consume all other keys while editing
}
if (c == KEY_LEFT || c == KEY_PREV) {
_page = (_page + HomePage::Count - 1) % HomePage::Count;
return true;
@@ -452,6 +554,11 @@ public:
_task->toggleGPS();
return true;
}
if (c == 'u' && _page == HomePage::GPS) {
_editing_utc = true;
_saved_utc_offset = _node_prefs->utc_offset_hours;
return true;
}
#endif
#if UI_SENSORS_PAGE == 1
if (c == KEY_ENTER && _page == HomePage::SENSORS) {
@@ -609,7 +716,6 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
channel_screen = new ChannelScreen(this, &rtc_clock);
contacts_screen = new ContactsScreen(this, &rtc_clock);
text_reader = new TextReaderScreen(this);
repeater_admin = new RepeaterAdminScreen(this, &rtc_clock);
setCurrScreen(splash);
}
@@ -651,8 +757,8 @@ switch(t){
void UITask::msgRead(int msgcount) {
_msgcount = msgcount;
if (msgcount == 0 && curr == msg_preview) {
gotoHomeScreen(); // only leave msg_preview when queue is empty
if (msgcount == 0) {
gotoHomeScreen();
}
}
@@ -988,11 +1094,22 @@ void UITask::injectKey(char c) {
}
curr->handleInput(c);
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
_next_refresh = 100; // trigger refresh
// Debounce refresh when editing UTC offset - e-ink takes 644ms per refresh
// so don't queue another render until the current one could have finished
if (isEditingHomeScreen()) {
unsigned long earliest = millis() + 700;
if (_next_refresh < earliest) {
_next_refresh = earliest;
}
} else {
_next_refresh = 100; // trigger refresh
}
}
}
void UITask::gotoHomeScreen() {
// Cancel any active editing state when navigating to home
((HomeScreen *) home)->cancelEditUTC();
setCurrScreen(home);
if (_display != NULL && !_display->isOn()) {
_display->turnOn();
@@ -1001,6 +1118,10 @@ void UITask::gotoHomeScreen() {
_next_refresh = 100;
}
bool UITask::isEditingHomeScreen() const {
return curr == home && ((HomeScreen *) home)->isEditingUTC();
}
void UITask::gotoChannelScreen() {
((ChannelScreen *) channel_screen)->resetScroll();
setCurrScreen(channel_screen);
@@ -1045,38 +1166,4 @@ void UITask::addSentChannelMessage(uint8_t channel_idx, const char* sender, cons
// Add to channel history with path_len=0 (local message)
((ChannelScreen *) channel_screen)->addMessage(channel_idx, 0, sender, formattedMsg);
}
void UITask::gotoRepeaterAdmin(int contactIdx) {
// Get contact name for the screen header
ContactInfo contact;
char name[32] = "Unknown";
if (the_mesh.getContactByIdx(contactIdx, contact)) {
strncpy(name, contact.name, sizeof(name) - 1);
name[sizeof(name) - 1] = '\0';
}
RepeaterAdminScreen* admin = (RepeaterAdminScreen*)repeater_admin;
admin->openForContact(contactIdx, name);
setCurrScreen(repeater_admin);
if (_display != NULL && !_display->isOn()) {
_display->turnOn();
}
_auto_off = millis() + AUTO_OFF_MILLIS;
_next_refresh = 100;
}
void UITask::onAdminLoginResult(bool success, uint8_t permissions, uint32_t server_time) {
if (isOnRepeaterAdmin()) {
((RepeaterAdminScreen*)repeater_admin)->onLoginResult(success, permissions, server_time);
_next_refresh = 100; // trigger re-render
}
}
void UITask::onAdminCliResponse(const char* from_name, const char* text) {
if (isOnRepeaterAdmin()) {
((RepeaterAdminScreen*)repeater_admin)->onCliResponse(text);
_next_refresh = 100; // trigger re-render
}
}

View File

@@ -54,7 +54,6 @@ class UITask : public AbstractUITask {
UIScreen* channel_screen; // Channel message history screen
UIScreen* contacts_screen; // Contacts list screen
UIScreen* text_reader; // *** NEW: Text reader screen ***
UIScreen* repeater_admin; // Repeater admin screen
UIScreen* curr;
void userLedHandler();
@@ -80,7 +79,6 @@ public:
void gotoChannelScreen(); // Navigate to channel message screen
void gotoContactsScreen(); // Navigate to contacts list
void gotoTextReader(); // *** NEW: Navigate to text reader ***
void gotoRepeaterAdmin(int contactIdx); // Navigate to repeater admin
void showAlert(const char* text, int duration_millis) override;
void forceRefresh() override { _next_refresh = 100; }
int getMsgCount() const { return _msgcount; }
@@ -89,7 +87,7 @@ public:
bool isOnChannelScreen() const { return curr == channel_screen; }
bool isOnContactsScreen() const { return curr == contacts_screen; }
bool isOnTextReader() const { return curr == text_reader; } // *** NEW ***
bool isOnRepeaterAdmin() const { return curr == repeater_admin; }
bool isEditingHomeScreen() const; // UTC offset editing on GPS page
uint8_t getChannelScreenViewIdx() const;
void toggleBuzzer();
@@ -101,17 +99,12 @@ public:
// Add a sent message to the channel screen history
void addSentChannelMessage(uint8_t channel_idx, const char* sender, const char* text) override;
// Repeater admin callbacks
void onAdminLoginResult(bool success, uint8_t permissions, uint32_t server_time) override;
void onAdminCliResponse(const char* from_name, const char* text) override;
// Get current screen for checking state
UIScreen* getCurrentScreen() const { return curr; }
UIScreen* getMsgPreviewScreen() const { return msg_preview; }
UIScreen* getTextReaderScreen() const { return text_reader; } // *** NEW ***
UIScreen* getContactsScreen() const { return contacts_screen; }
UIScreen* getRepeaterAdminScreen() const { return repeater_admin; }
// from AbstractUITask
void msgRead(int msgcount) override;