mirror of
https://github.com/pelgraine/Meck.git
synced 2026-03-28 17:42:44 +01:00
Compare commits
6 Commits
dms-1
...
repeater-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f558b130f | ||
|
|
04462b93bc | ||
|
|
d42c283fb4 | ||
|
|
87a5f185d3 | ||
|
|
2972d1ffb4 | ||
|
|
fe1c1931ab |
29
README.md
29
README.md
@@ -36,13 +36,38 @@ Press **N** from the home screen to open the contacts list. All known mesh conta
|
||||
|-----|--------|
|
||||
| W / S | Scroll up / down through contacts |
|
||||
| A / D | Cycle filter: All → Chat → Repeater → Room → Sensor |
|
||||
| Enter / C | Open DM compose to selected chat contact |
|
||||
| Enter | Open DM compose (Chat contact) or repeater admin (Repeater contact) |
|
||||
| C | Open DM compose to selected chat contact |
|
||||
| Q | Back to home screen |
|
||||
|
||||
### Sending a Direct Message
|
||||
|
||||
Select a **Chat** contact in the contacts list and press **Enter** or **C** to start composing a direct message. The compose screen will show `DM: ContactName` in the header. Type your message and press **Enter** to send. The DM is sent encrypted directly to that contact (or flooded if no direct path is known). After sending or cancelling, you're returned to the contacts list.
|
||||
|
||||
### Repeater Admin Screen
|
||||
|
||||
Select a **Repeater** contact in the contacts list and press **Enter** to open the repeater admin screen. You'll be prompted for the repeater's admin password. Characters briefly appear as you type them before being masked, making it easier to enter symbols and numbers on the T-Deck Pro keyboard.
|
||||
|
||||
After a successful login, you'll see a menu with the following remote administration commands:
|
||||
|
||||
| Menu Item | Description |
|
||||
|-----------|-------------|
|
||||
| Clock Sync | Push your device's clock time to the repeater |
|
||||
| Send Advert | Trigger the repeater to broadcast an advertisement |
|
||||
| Neighbors | View other repeaters heard via zero-hop adverts |
|
||||
| Get Clock | Read the repeater's current clock value |
|
||||
| Version | Query the repeater's firmware version |
|
||||
| Get Status | Retrieve repeater status information |
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| W / S | Navigate menu items |
|
||||
| Enter | Execute selected command |
|
||||
| C | Enter compose mode (send raw CLI command) |
|
||||
| Q | Back to contacts (from menu) or cancel login |
|
||||
|
||||
Command responses are displayed in a scrollable view. Use **W / S** to scroll long responses and **Q** to return to the menu.
|
||||
|
||||
### Compose Mode
|
||||
|
||||
| Key | Action |
|
||||
@@ -178,7 +203,7 @@ There are a number of fairly major features in the pipeline, with no particular
|
||||
- [X] View and compose all channel messages Companion BLE firmware
|
||||
- [X] Standalone DM functionality for Companion BLE firmware
|
||||
- [X] Contacts list with filtering for Companion BLE firmware
|
||||
- [ ] Standalone repeater admin access for Companion BLE firmware
|
||||
- [X] Standalone repeater admin access for Companion BLE firmware
|
||||
- [ ] 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
|
||||
|
||||
@@ -46,4 +46,8 @@ public:
|
||||
virtual void showAlert(const char* text, int duration_millis) {}
|
||||
virtual void forceRefresh() {}
|
||||
virtual void addSentChannelMessage(uint8_t channel_idx, const char* sender, const char* text) {}
|
||||
|
||||
// Repeater admin callbacks (from MyMesh)
|
||||
virtual void onAdminLoginResult(bool success, uint8_t permissions, uint32_t server_time) {}
|
||||
virtual void onAdminCliResponse(const char* from_name, const char* text) {}
|
||||
};
|
||||
@@ -476,7 +476,7 @@ bool MyMesh::filterRecvFloodPacket(mesh::Packet* packet) {
|
||||
}
|
||||
}
|
||||
|
||||
return false; // never filter — let normal processing continue
|
||||
return false; // never filter  let normal processing continue
|
||||
}
|
||||
|
||||
void MyMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) {
|
||||
@@ -523,6 +523,13 @@ 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,
|
||||
@@ -650,6 +657,55 @@ 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) {
|
||||
@@ -708,6 +764,11 @@ 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) {
|
||||
@@ -721,11 +782,21 @@ 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
|
||||
@@ -897,6 +968,7 @@ 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));
|
||||
|
||||
@@ -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)
|
||||
@@ -108,6 +108,12 @@ public:
|
||||
// Send a direct message from the UI (no BLE dependency)
|
||||
bool uiSendDirectMessage(uint32_t contact_idx, const char* text);
|
||||
|
||||
// Repeater admin - UI-initiated operations
|
||||
bool uiLoginToRepeater(uint32_t contact_idx, const char* password);
|
||||
bool uiSendCliCommand(uint32_t contact_idx, const char* command);
|
||||
int getAdminContactIdx() const { return _admin_contact_idx; }
|
||||
|
||||
|
||||
protected:
|
||||
float getAirtimeBudgetFactor() const override;
|
||||
int getInterferenceThreshold() const override;
|
||||
@@ -247,6 +253,7 @@ private:
|
||||
};
|
||||
SentMsgTrack _sent_track[SENT_TRACK_SIZE];
|
||||
int _sent_track_idx; // next slot in circular buffer
|
||||
int _admin_contact_idx; // contact index for active admin session (-1 if none)
|
||||
};
|
||||
|
||||
extern MyMesh the_mesh;
|
||||
@@ -10,6 +10,7 @@
|
||||
#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);
|
||||
@@ -630,6 +631,56 @@ 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':
|
||||
@@ -690,7 +741,7 @@ void handleKeyboardInput() {
|
||||
case 'w':
|
||||
case 'W':
|
||||
// Navigate up/previous (scroll on channel screen)
|
||||
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen()) {
|
||||
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen() || ui_task.isOnRepeaterAdmin()) {
|
||||
ui_task.injectKey('w'); // Pass directly for channel/contacts switching
|
||||
} else {
|
||||
Serial.println("Nav: Previous");
|
||||
@@ -701,7 +752,7 @@ void handleKeyboardInput() {
|
||||
case 's':
|
||||
case 'S':
|
||||
// Navigate down/next (scroll on channel screen)
|
||||
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen()) {
|
||||
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen() || ui_task.isOnRepeaterAdmin()) {
|
||||
ui_task.injectKey('s'); // Pass directly for channel/contacts switching
|
||||
} else {
|
||||
Serial.println("Nav: Next");
|
||||
@@ -733,6 +784,7 @@ 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();
|
||||
@@ -747,9 +799,15 @@ 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 contact selected (repeater, room, etc.) - future use
|
||||
Serial.printf("Selected non-chat contact type=%d idx=%d\n", ctype, idx);
|
||||
// Non-chat, non-repeater contact (room, sensor, etc.) - future use
|
||||
Serial.printf("Selected contact type=%d idx=%d\n", ctype, idx);
|
||||
}
|
||||
} else {
|
||||
Serial.println("Nav: Enter/Select");
|
||||
@@ -760,7 +818,7 @@ void handleKeyboardInput() {
|
||||
case 'q':
|
||||
case 'Q':
|
||||
case '\b':
|
||||
// Go back to home screen
|
||||
// Go back to home screen (admin mode handled above)
|
||||
Serial.println("Nav: Back to home");
|
||||
ui_task.gotoHomeScreen();
|
||||
break;
|
||||
|
||||
615
examples/companion_radio/ui-new/Repeateradminscreen.h
Normal file
615
examples/companion_radio/ui-new/Repeateradminscreen.h
Normal file
@@ -0,0 +1,615 @@
|
||||
#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;
|
||||
|
||||
#define ADMIN_PASSWORD_MAX 32
|
||||
#define ADMIN_RESPONSE_MAX 512 // CLI responses can be multi-line
|
||||
#define ADMIN_TIMEOUT_MS 15000 // 15s timeout for login/commands
|
||||
|
||||
class RepeaterAdminScreen : public UIScreen {
|
||||
public:
|
||||
enum AdminState {
|
||||
STATE_PASSWORD_ENTRY, // Typing admin password
|
||||
STATE_LOGGING_IN, // Waiting for login response
|
||||
STATE_MENU, // Main admin menu
|
||||
STATE_COMMAND_PENDING, // Waiting for CLI response
|
||||
STATE_RESPONSE_VIEW, // Displaying CLI response
|
||||
STATE_ERROR // Error state (timeout, send fail)
|
||||
};
|
||||
|
||||
// Menu items
|
||||
enum MenuItem {
|
||||
MENU_CLOCK_SYNC = 0,
|
||||
MENU_ADVERT,
|
||||
MENU_NEIGHBORS,
|
||||
MENU_GET_CLOCK,
|
||||
MENU_GET_VER,
|
||||
MENU_GET_STATUS,
|
||||
MENU_COUNT
|
||||
};
|
||||
|
||||
private:
|
||||
UITask* _task;
|
||||
mesh::RTCClock* _rtc;
|
||||
|
||||
AdminState _state;
|
||||
int _contactIdx; // Contact table index of the repeater
|
||||
char _repeaterName[32]; // Cached repeater name
|
||||
uint8_t _permissions; // Login permissions (0=guest, 3=admin)
|
||||
uint32_t _serverTime; // Server timestamp from login response
|
||||
|
||||
// Password entry
|
||||
char _password[ADMIN_PASSWORD_MAX];
|
||||
int _pwdLen;
|
||||
unsigned long _lastCharAt; // millis() when last char typed (for brief reveal)
|
||||
|
||||
// Menu
|
||||
int _menuSel; // Currently selected menu item
|
||||
|
||||
// Response buffer
|
||||
char _response[ADMIN_RESPONSE_MAX];
|
||||
int _responseLen;
|
||||
int _responseScroll; // Scroll offset for long responses
|
||||
|
||||
// Timing
|
||||
unsigned long _cmdSentAt; // millis() when command was sent
|
||||
bool _waitingForLogin;
|
||||
|
||||
// Password cache - remembers passwords per repeater within session
|
||||
static const int PWD_CACHE_SIZE = 8;
|
||||
struct PwdCacheEntry {
|
||||
int contactIdx;
|
||||
char password[ADMIN_PASSWORD_MAX];
|
||||
};
|
||||
PwdCacheEntry _pwdCache[PWD_CACHE_SIZE];
|
||||
int _pwdCacheCount;
|
||||
|
||||
// Look up cached password for a contact, returns nullptr if not found
|
||||
const char* getCachedPassword(int contactIdx) {
|
||||
for (int i = 0; i < _pwdCacheCount; i++) {
|
||||
if (_pwdCache[i].contactIdx == contactIdx) return _pwdCache[i].password;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Save password to cache (update existing or add new, evict oldest if full)
|
||||
void cachePassword(int contactIdx, const char* pwd) {
|
||||
// Update existing entry
|
||||
for (int i = 0; i < _pwdCacheCount; i++) {
|
||||
if (_pwdCache[i].contactIdx == contactIdx) {
|
||||
strncpy(_pwdCache[i].password, pwd, ADMIN_PASSWORD_MAX - 1);
|
||||
_pwdCache[i].password[ADMIN_PASSWORD_MAX - 1] = '\0';
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Add new entry, evict oldest if full
|
||||
if (_pwdCacheCount < PWD_CACHE_SIZE) {
|
||||
int slot = _pwdCacheCount++;
|
||||
_pwdCache[slot].contactIdx = contactIdx;
|
||||
strncpy(_pwdCache[slot].password, pwd, ADMIN_PASSWORD_MAX - 1);
|
||||
_pwdCache[slot].password[ADMIN_PASSWORD_MAX - 1] = '\0';
|
||||
} else {
|
||||
// Shift entries down to evict oldest
|
||||
for (int i = 0; i < PWD_CACHE_SIZE - 1; i++) {
|
||||
_pwdCache[i] = _pwdCache[i + 1];
|
||||
}
|
||||
_pwdCache[PWD_CACHE_SIZE - 1].contactIdx = contactIdx;
|
||||
strncpy(_pwdCache[PWD_CACHE_SIZE - 1].password, pwd, ADMIN_PASSWORD_MAX - 1);
|
||||
_pwdCache[PWD_CACHE_SIZE - 1].password[ADMIN_PASSWORD_MAX - 1] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
static const char* menuLabel(MenuItem m) {
|
||||
switch (m) {
|
||||
case MENU_CLOCK_SYNC: return "Clock Sync";
|
||||
case MENU_ADVERT: return "Send Advert";
|
||||
case MENU_NEIGHBORS: return "Neighbors";
|
||||
case MENU_GET_CLOCK: return "Get Clock";
|
||||
case MENU_GET_VER: return "Version";
|
||||
case MENU_GET_STATUS: return "Get Status";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
|
||||
static const char* menuCommand(MenuItem m) {
|
||||
switch (m) {
|
||||
case MENU_CLOCK_SYNC: return "clock sync";
|
||||
case MENU_ADVERT: return "advert";
|
||||
case MENU_NEIGHBORS: return "neighbors";
|
||||
case MENU_GET_CLOCK: return "clock";
|
||||
case MENU_GET_VER: return "ver";
|
||||
case MENU_GET_STATUS: return "get status";
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
|
||||
// Format epoch as HH:MM:SS
|
||||
static void formatTime(char* buf, size_t bufLen, uint32_t epoch) {
|
||||
if (epoch == 0) {
|
||||
strncpy(buf, "--:--:--", bufLen);
|
||||
return;
|
||||
}
|
||||
uint32_t secs = epoch % 60;
|
||||
uint32_t mins = (epoch / 60) % 60;
|
||||
uint32_t hrs = (epoch / 3600) % 24;
|
||||
snprintf(buf, bufLen, "%02d:%02d:%02d", (int)hrs, (int)mins, (int)secs);
|
||||
}
|
||||
|
||||
public:
|
||||
RepeaterAdminScreen(UITask* task, mesh::RTCClock* rtc)
|
||||
: _task(task), _rtc(rtc), _state(STATE_PASSWORD_ENTRY),
|
||||
_contactIdx(-1), _permissions(0), _serverTime(0),
|
||||
_pwdLen(0), _lastCharAt(0), _menuSel(0),
|
||||
_responseLen(0), _responseScroll(0),
|
||||
_cmdSentAt(0), _waitingForLogin(false), _pwdCacheCount(0) {
|
||||
_password[0] = '\0';
|
||||
_repeaterName[0] = '\0';
|
||||
_response[0] = '\0';
|
||||
}
|
||||
|
||||
// Called when entering the screen for a specific repeater contact
|
||||
void openForContact(int contactIdx, const char* name) {
|
||||
_contactIdx = contactIdx;
|
||||
strncpy(_repeaterName, name, sizeof(_repeaterName) - 1);
|
||||
_repeaterName[sizeof(_repeaterName) - 1] = '\0';
|
||||
|
||||
// Reset state
|
||||
_state = STATE_PASSWORD_ENTRY;
|
||||
_lastCharAt = 0;
|
||||
_menuSel = 0;
|
||||
_permissions = 0;
|
||||
_serverTime = 0;
|
||||
_responseLen = 0;
|
||||
_responseScroll = 0;
|
||||
_response[0] = '\0';
|
||||
_waitingForLogin = false;
|
||||
|
||||
// Pre-fill from password cache if available
|
||||
const char* cached = getCachedPassword(contactIdx);
|
||||
if (cached) {
|
||||
strncpy(_password, cached, ADMIN_PASSWORD_MAX - 1);
|
||||
_password[ADMIN_PASSWORD_MAX - 1] = '\0';
|
||||
_pwdLen = strlen(_password);
|
||||
} else {
|
||||
_pwdLen = 0;
|
||||
_password[0] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
int getContactIdx() const { return _contactIdx; }
|
||||
AdminState getState() const { return _state; }
|
||||
|
||||
// Called by UITask when a login response is received
|
||||
void onLoginResult(bool success, uint8_t permissions, uint32_t server_time) {
|
||||
_waitingForLogin = false;
|
||||
if (success) {
|
||||
_permissions = permissions;
|
||||
_serverTime = server_time;
|
||||
_state = STATE_MENU;
|
||||
cachePassword(_contactIdx, _password); // remember for next time
|
||||
} else {
|
||||
snprintf(_response, sizeof(_response), "Login failed.\nCheck password.");
|
||||
_responseLen = strlen(_response);
|
||||
_state = STATE_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
// Called by UITask when a CLI response is received
|
||||
void onCliResponse(const char* text) {
|
||||
if (_state != STATE_COMMAND_PENDING) return;
|
||||
|
||||
int tlen = strlen(text);
|
||||
if (tlen >= ADMIN_RESPONSE_MAX) tlen = ADMIN_RESPONSE_MAX - 1;
|
||||
memcpy(_response, text, tlen);
|
||||
_response[tlen] = '\0';
|
||||
_responseLen = tlen;
|
||||
_responseScroll = 0;
|
||||
_state = STATE_RESPONSE_VIEW;
|
||||
}
|
||||
|
||||
// Poll for timeouts
|
||||
void poll() override {
|
||||
if ((_state == STATE_LOGGING_IN || _state == STATE_COMMAND_PENDING) &&
|
||||
_cmdSentAt > 0 && (millis() - _cmdSentAt) > ADMIN_TIMEOUT_MS) {
|
||||
snprintf(_response, sizeof(_response), "Timeout - no response.");
|
||||
_responseLen = strlen(_response);
|
||||
_state = STATE_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
int render(DisplayDriver& display) override {
|
||||
char tmp[64];
|
||||
|
||||
// === Header ===
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.setCursor(0, 0);
|
||||
|
||||
// Truncate name if needed to fit header
|
||||
snprintf(tmp, sizeof(tmp), "Admin: %.16s", _repeaterName);
|
||||
display.print(tmp);
|
||||
|
||||
// Show permissions if logged in
|
||||
if (_state >= STATE_MENU && _state <= STATE_RESPONSE_VIEW) {
|
||||
const char* perm = (_permissions & 0x03) >= 3 ? "ADM" :
|
||||
(_permissions & 0x03) >= 2 ? "R/W" : "R/O";
|
||||
display.setCursor(display.width() - display.getTextWidth(perm) - 2, 0);
|
||||
display.print(perm);
|
||||
}
|
||||
|
||||
display.drawRect(0, 11, display.width(), 1); // divider
|
||||
|
||||
// === Body - depends on state ===
|
||||
int bodyY = 14;
|
||||
int footerY = display.height() - 12;
|
||||
int bodyHeight = footerY - bodyY - 4;
|
||||
|
||||
switch (_state) {
|
||||
case STATE_PASSWORD_ENTRY:
|
||||
renderPasswordEntry(display, bodyY);
|
||||
break;
|
||||
case STATE_LOGGING_IN:
|
||||
renderWaiting(display, bodyY, "Logging in...");
|
||||
break;
|
||||
case STATE_MENU:
|
||||
renderMenu(display, bodyY, bodyHeight);
|
||||
break;
|
||||
case STATE_COMMAND_PENDING:
|
||||
renderWaiting(display, bodyY, "Waiting...");
|
||||
break;
|
||||
case STATE_RESPONSE_VIEW:
|
||||
renderResponse(display, bodyY, bodyHeight);
|
||||
break;
|
||||
case STATE_ERROR:
|
||||
renderResponse(display, bodyY, bodyHeight); // reuse response renderer for errors
|
||||
break;
|
||||
}
|
||||
|
||||
// === Footer ===
|
||||
display.drawRect(0, footerY - 2, display.width(), 1);
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
display.setTextSize(1);
|
||||
display.setCursor(0, footerY);
|
||||
|
||||
switch (_state) {
|
||||
case STATE_PASSWORD_ENTRY:
|
||||
display.print("Q:Back");
|
||||
{
|
||||
const char* right = "Enter:Login";
|
||||
display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY);
|
||||
display.print(right);
|
||||
}
|
||||
break;
|
||||
case STATE_LOGGING_IN:
|
||||
case STATE_COMMAND_PENDING:
|
||||
display.print("Q:Cancel");
|
||||
break;
|
||||
case STATE_MENU:
|
||||
display.print("Q:Back");
|
||||
{
|
||||
const char* mid = "W/S:Sel";
|
||||
display.setCursor((display.width() - display.getTextWidth(mid)) / 2, footerY);
|
||||
display.print(mid);
|
||||
const char* right = "Ent:Run";
|
||||
display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY);
|
||||
display.print(right);
|
||||
}
|
||||
break;
|
||||
case STATE_RESPONSE_VIEW:
|
||||
case STATE_ERROR:
|
||||
display.print("Q:Menu");
|
||||
if (_responseLen > bodyHeight / 9) { // if scrollable
|
||||
const char* right = "W/S:Scrll";
|
||||
display.setCursor(display.width() - display.getTextWidth(right) - 2, footerY);
|
||||
display.print(right);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (_state == STATE_LOGGING_IN || _state == STATE_COMMAND_PENDING) return 1000;
|
||||
// During password reveal, refresh when the reveal expires
|
||||
if (_state == STATE_PASSWORD_ENTRY && _lastCharAt > 0 && (millis() - _lastCharAt) < 800) {
|
||||
return _lastCharAt + 800 - millis() + 50; // refresh shortly after reveal ends
|
||||
}
|
||||
return 5000;
|
||||
}
|
||||
|
||||
bool handleInput(char c) override {
|
||||
switch (_state) {
|
||||
case STATE_PASSWORD_ENTRY:
|
||||
return handlePasswordInput(c);
|
||||
case STATE_LOGGING_IN:
|
||||
case STATE_COMMAND_PENDING:
|
||||
// Q to cancel and go back
|
||||
if (c == 'q' || c == 'Q') {
|
||||
_state = (_state == STATE_LOGGING_IN) ? STATE_PASSWORD_ENTRY : STATE_MENU;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
case STATE_MENU:
|
||||
return handleMenuInput(c);
|
||||
case STATE_RESPONSE_VIEW:
|
||||
case STATE_ERROR:
|
||||
return handleResponseInput(c);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
// --- Password Entry ---
|
||||
void renderPasswordEntry(DisplayDriver& display, int y) {
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setCursor(0, y);
|
||||
display.print("Password:");
|
||||
|
||||
y += 14;
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
display.setCursor(0, y);
|
||||
|
||||
// Show asterisks for password characters, with brief reveal of last char
|
||||
char masked[ADMIN_PASSWORD_MAX];
|
||||
int i;
|
||||
bool revealing = (_pwdLen > 0 && (millis() - _lastCharAt) < 800);
|
||||
int revealIdx = revealing ? _pwdLen - 1 : -1;
|
||||
for (i = 0; i < _pwdLen && i < ADMIN_PASSWORD_MAX - 1; i++) {
|
||||
masked[i] = (i == revealIdx) ? _password[i] : '*';
|
||||
}
|
||||
masked[i] = '\0';
|
||||
display.print(masked);
|
||||
|
||||
// Cursor indicator
|
||||
display.print("_");
|
||||
}
|
||||
|
||||
bool handlePasswordInput(char c) {
|
||||
// Q without any password typed = go back (return false to signal "not handled")
|
||||
if ((c == 'q' || c == 'Q') && _pwdLen == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Enter to submit
|
||||
if (c == '\r' || c == '\n' || c == KEY_ENTER) {
|
||||
if (_pwdLen > 0) {
|
||||
return doLogin();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Backspace
|
||||
if (c == 0x08 || c == 0x7F) {
|
||||
if (_pwdLen > 0) {
|
||||
_pwdLen--;
|
||||
_password[_pwdLen] = '\0';
|
||||
_lastCharAt = 0; // no reveal after delete
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Printable character
|
||||
if (c >= 32 && c < 127 && _pwdLen < ADMIN_PASSWORD_MAX - 1) {
|
||||
_password[_pwdLen++] = c;
|
||||
_password[_pwdLen] = '\0';
|
||||
_lastCharAt = millis(); // start brief reveal
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool doLogin(); // Defined below, calls into MyMesh
|
||||
|
||||
// --- Menu ---
|
||||
void renderMenu(DisplayDriver& display, int y, int bodyHeight) {
|
||||
display.setTextSize(0); // tiny font for compact rows
|
||||
int lineHeight = 9;
|
||||
|
||||
// Show server time comparison if available
|
||||
if (_serverTime > 0) {
|
||||
char ourTime[12], srvTime[12];
|
||||
uint32_t now = _rtc->getCurrentTime();
|
||||
formatTime(ourTime, sizeof(ourTime), now);
|
||||
formatTime(srvTime, sizeof(srvTime), _serverTime);
|
||||
|
||||
int drift = (int)(now - _serverTime);
|
||||
char driftStr[24];
|
||||
if (abs(drift) < 2) {
|
||||
snprintf(driftStr, sizeof(driftStr), "Synced");
|
||||
} else {
|
||||
snprintf(driftStr, sizeof(driftStr), "Drift:%+ds", drift);
|
||||
}
|
||||
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setCursor(0, y);
|
||||
char info[48];
|
||||
snprintf(info, sizeof(info), "Rpt:%s Us:%s %s", srvTime, ourTime, driftStr);
|
||||
display.print(info);
|
||||
y += lineHeight + 2;
|
||||
}
|
||||
|
||||
// Menu items
|
||||
for (int i = 0; i < MENU_COUNT && y + lineHeight <= display.height() - 16; i++) {
|
||||
bool selected = (i == _menuSel);
|
||||
|
||||
if (selected) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.fillRect(0, y + 5, display.width(), lineHeight);
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
} else {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
}
|
||||
|
||||
display.setCursor(2, y);
|
||||
char label[32];
|
||||
snprintf(label, sizeof(label), "%s %s", selected ? ">" : " ", menuLabel((MenuItem)i));
|
||||
display.print(label);
|
||||
|
||||
y += lineHeight;
|
||||
}
|
||||
|
||||
display.setTextSize(1);
|
||||
}
|
||||
|
||||
bool handleMenuInput(char c) {
|
||||
// W/up - scroll up
|
||||
if (c == 'w' || c == 'W' || c == 0xF2) {
|
||||
if (_menuSel > 0) _menuSel--;
|
||||
return true;
|
||||
}
|
||||
// S/down - scroll down
|
||||
if (c == 's' || c == 'S' || c == 0xF1) {
|
||||
if (_menuSel < MENU_COUNT - 1) _menuSel++;
|
||||
return true;
|
||||
}
|
||||
// Enter - execute selected command
|
||||
if (c == '\r' || c == '\n' || c == KEY_ENTER) {
|
||||
return executeMenuCommand((MenuItem)_menuSel);
|
||||
}
|
||||
// Q - back to contacts
|
||||
if (c == 'q' || c == 'Q') {
|
||||
return false; // let UITask handle back navigation
|
||||
}
|
||||
// Number keys for quick selection
|
||||
if (c >= '1' && c <= '0' + MENU_COUNT) {
|
||||
_menuSel = c - '1';
|
||||
return executeMenuCommand((MenuItem)_menuSel);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool executeMenuCommand(MenuItem item); // Defined below, calls into MyMesh
|
||||
|
||||
// --- Response View ---
|
||||
void renderResponse(DisplayDriver& display, int y, int bodyHeight) {
|
||||
display.setTextSize(0); // tiny font for more content
|
||||
int lineHeight = 9;
|
||||
|
||||
display.setColor((_state == STATE_ERROR) ? DisplayDriver::YELLOW : DisplayDriver::LIGHT);
|
||||
|
||||
// Render response text with word wrapping and scroll support
|
||||
int maxLines = bodyHeight / lineHeight;
|
||||
int lineCount = 0;
|
||||
int skipLines = _responseScroll;
|
||||
|
||||
const char* p = _response;
|
||||
char lineBuf[80];
|
||||
int lineWidth = display.width() - 4;
|
||||
|
||||
while (*p && lineCount < maxLines + skipLines) {
|
||||
// Extract next line (up to newline or screen width)
|
||||
int i = 0;
|
||||
while (*p && *p != '\n' && i < 79) {
|
||||
lineBuf[i++] = *p++;
|
||||
}
|
||||
lineBuf[i] = '\0';
|
||||
if (*p == '\n') p++;
|
||||
|
||||
if (lineCount >= skipLines && lineCount < skipLines + maxLines) {
|
||||
display.setCursor(2, y);
|
||||
display.print(lineBuf);
|
||||
y += lineHeight;
|
||||
}
|
||||
lineCount++;
|
||||
}
|
||||
|
||||
display.setTextSize(1);
|
||||
}
|
||||
|
||||
bool handleResponseInput(char c) {
|
||||
// W/up - scroll up
|
||||
if (c == 'w' || c == 'W' || c == 0xF2) {
|
||||
if (_responseScroll > 0) {
|
||||
_responseScroll--;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// S/down - scroll down
|
||||
if (c == 's' || c == 'S' || c == 0xF1) {
|
||||
_responseScroll++;
|
||||
return true;
|
||||
}
|
||||
// Q - back to menu (or back to password on error)
|
||||
if (c == 'q' || c == 'Q') {
|
||||
if (_state == STATE_ERROR && _permissions == 0) {
|
||||
// Not yet logged in, go back to password
|
||||
_state = STATE_PASSWORD_ENTRY;
|
||||
} else {
|
||||
_state = STATE_MENU;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// Enter - also go back to menu
|
||||
if (c == '\r' || c == '\n' || c == KEY_ENTER) {
|
||||
_state = STATE_MENU;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- Waiting spinner ---
|
||||
void renderWaiting(DisplayDriver& display, int y, const char* msg) {
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
|
||||
int cx = (display.width() - display.getTextWidth(msg)) / 2;
|
||||
int cy = y + 20;
|
||||
display.setCursor(cx, cy);
|
||||
display.print(msg);
|
||||
|
||||
// Show elapsed time
|
||||
if (_cmdSentAt > 0) {
|
||||
char elapsed[16];
|
||||
unsigned long secs = (millis() - _cmdSentAt) / 1000;
|
||||
snprintf(elapsed, sizeof(elapsed), "%lus", secs);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setCursor((display.width() - display.getTextWidth(elapsed)) / 2, cy + 14);
|
||||
display.print(elapsed);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// --- Implementations that require MyMesh (the_mesh is declared extern above) ---
|
||||
|
||||
inline bool RepeaterAdminScreen::doLogin() {
|
||||
if (_contactIdx < 0 || _pwdLen == 0) return false;
|
||||
|
||||
if (the_mesh.uiLoginToRepeater(_contactIdx, _password)) {
|
||||
_state = STATE_LOGGING_IN;
|
||||
_cmdSentAt = millis();
|
||||
_waitingForLogin = true;
|
||||
return true;
|
||||
} else {
|
||||
snprintf(_response, sizeof(_response), "Send failed.\nCheck contact path.");
|
||||
_responseLen = strlen(_response);
|
||||
_state = STATE_ERROR;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
inline bool RepeaterAdminScreen::executeMenuCommand(MenuItem item) {
|
||||
if (_contactIdx < 0) return false;
|
||||
|
||||
const char* cmd = menuCommand(item);
|
||||
if (cmd[0] == '\0') return false;
|
||||
|
||||
if (the_mesh.uiSendCliCommand(_contactIdx, cmd)) {
|
||||
_state = STATE_COMMAND_PENDING;
|
||||
_cmdSentAt = millis();
|
||||
_response[0] = '\0';
|
||||
_responseLen = 0;
|
||||
_responseScroll = 0;
|
||||
return true;
|
||||
} else {
|
||||
snprintf(_response, sizeof(_response), "Send failed.");
|
||||
_responseLen = strlen(_response);
|
||||
_state = STATE_ERROR;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <helpers/TxtDataHelpers.h>
|
||||
#include "../MyMesh.h"
|
||||
#include "target.h"
|
||||
#include "RepeaterAdminScreen.h"
|
||||
#ifdef WIFI_SSID
|
||||
#include <WiFi.h>
|
||||
#endif
|
||||
@@ -608,6 +609,7 @@ 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);
|
||||
}
|
||||
|
||||
@@ -649,8 +651,8 @@ switch(t){
|
||||
|
||||
void UITask::msgRead(int msgcount) {
|
||||
_msgcount = msgcount;
|
||||
if (msgcount == 0) {
|
||||
gotoHomeScreen();
|
||||
if (msgcount == 0 && curr == msg_preview) {
|
||||
gotoHomeScreen(); // only leave msg_preview when queue is empty
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1043,4 +1045,38 @@ 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
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,7 @@ 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();
|
||||
@@ -79,6 +80,7 @@ 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; }
|
||||
@@ -87,6 +89,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; }
|
||||
uint8_t getChannelScreenViewIdx() const;
|
||||
|
||||
void toggleBuzzer();
|
||||
@@ -98,12 +101,17 @@ 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;
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
|
||||
#include "TechoBoard.h"
|
||||
|
||||
#ifdef LILYGO_TECHO
|
||||
|
||||
void TechoBoard::begin() {
|
||||
NRF52Board::begin();
|
||||
|
||||
Wire.begin();
|
||||
|
||||
pinMode(SX126X_POWER_EN, OUTPUT);
|
||||
digitalWrite(SX126X_POWER_EN, HIGH);
|
||||
delay(10); // give sx1262 some time to power up
|
||||
}
|
||||
|
||||
uint16_t TechoBoard::getBattMilliVolts() {
|
||||
int adcvalue = 0;
|
||||
|
||||
analogReference(AR_INTERNAL_3_0);
|
||||
analogReadResolution(12);
|
||||
delay(10);
|
||||
|
||||
// ADC range is 0..3000mV and resolution is 12-bit (0..4095)
|
||||
adcvalue = analogRead(PIN_VBAT_READ);
|
||||
// Convert the raw value to compensated mv, taking the resistor-
|
||||
// divider into account (providing the actual LIPO voltage)
|
||||
return (uint16_t)((float)adcvalue * REAL_VBAT_MV_PER_LSB);
|
||||
}
|
||||
#endif
|
||||
@@ -1,44 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <MeshCore.h>
|
||||
#include <Arduino.h>
|
||||
#include <helpers/NRF52Board.h>
|
||||
|
||||
// built-ins
|
||||
#define VBAT_MV_PER_LSB (0.73242188F) // 3.0V ADC range and 12-bit ADC resolution = 3000mV/4096
|
||||
|
||||
#define VBAT_DIVIDER (0.5F) // 150K + 150K voltage divider on VBAT
|
||||
#define VBAT_DIVIDER_COMP (2.0F) // Compensation factor for the VBAT divider
|
||||
|
||||
#define PIN_VBAT_READ (4)
|
||||
#define REAL_VBAT_MV_PER_LSB (VBAT_DIVIDER_COMP * VBAT_MV_PER_LSB)
|
||||
|
||||
class TechoBoard : public NRF52BoardOTA {
|
||||
public:
|
||||
TechoBoard() : NRF52BoardOTA("TECHO_OTA") {}
|
||||
void begin();
|
||||
uint16_t getBattMilliVolts() override;
|
||||
|
||||
const char* getManufacturerName() const override {
|
||||
return "LilyGo T-Echo";
|
||||
}
|
||||
|
||||
void powerOff() override {
|
||||
#ifdef LED_RED
|
||||
digitalWrite(LED_RED, LOW);
|
||||
#endif
|
||||
#ifdef LED_GREEN
|
||||
digitalWrite(LED_GREEN, LOW);
|
||||
#endif
|
||||
#ifdef LED_BLUE
|
||||
digitalWrite(LED_BLUE, LOW);
|
||||
#endif
|
||||
#ifdef DISP_BACKLIGHT
|
||||
digitalWrite(DISP_BACKLIGHT, LOW);
|
||||
#endif
|
||||
#ifdef PIN_PWR_EN
|
||||
digitalWrite(PIN_PWR_EN, LOW);
|
||||
#endif
|
||||
sd_power_system_off();
|
||||
}
|
||||
};
|
||||
@@ -1,98 +0,0 @@
|
||||
[LilyGo_T-Echo-Lite]
|
||||
extends = nrf52_base
|
||||
board = t-echo
|
||||
board_build.ldscript = boards/nrf52840_s140_v6.ld
|
||||
build_flags = ${nrf52_base.build_flags}
|
||||
-I variants/lilygo_techo_lite
|
||||
-I src/helpers/nrf52
|
||||
-I lib/nrf52/s140_nrf52_6.1.1_API/include
|
||||
-I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52
|
||||
-D LILYGO_TECHO
|
||||
-D RADIO_CLASS=CustomSX1262
|
||||
-D WRAPPER_CLASS=CustomSX1262Wrapper
|
||||
-D LORA_TX_POWER=22
|
||||
-D SX126X_POWER_EN=30
|
||||
-D SX126X_CURRENT_LIMIT=140
|
||||
-D SX126X_RX_BOOSTED_GAIN=1
|
||||
-D P_LORA_TX_LED=LED_GREEN
|
||||
-D DISABLE_DIAGNOSTIC_OUTPUT
|
||||
-D ENV_INCLUDE_GPS=1
|
||||
-D GPS_BAUD_RATE=9600
|
||||
-D PIN_GPS_EN=GPS_EN
|
||||
-D DISPLAY_CLASS=GxEPDDisplay
|
||||
-D EINK_DISPLAY_MODEL=GxEPD2_122_T61
|
||||
-D EINK_SCALE_X=1.5f
|
||||
-D EINK_SCALE_Y=2.0f
|
||||
-D EINK_X_OFFSET=0
|
||||
-D EINK_Y_OFFSET=10
|
||||
-D DISPLAY_ROTATION=4
|
||||
-D AUTO_OFF_MILLIS=0
|
||||
build_src_filter = ${nrf52_base.build_src_filter}
|
||||
+<helpers/*.cpp>
|
||||
+<TechoBoard.cpp>
|
||||
+<helpers/sensors/EnvironmentSensorManager.cpp>
|
||||
+<helpers/ui/GxEPDDisplay.cpp>
|
||||
+<helpers/ui/MomentaryButton.cpp>
|
||||
+<../variants/lilygo_techo_lite>
|
||||
lib_deps =
|
||||
${nrf52_base.lib_deps}
|
||||
stevemarple/MicroNMEA @ ^2.0.6
|
||||
adafruit/Adafruit BME280 Library @ ^2.3.0
|
||||
https://github.com/SoulOfNoob/GxEPD2.git
|
||||
bakercp/CRC32 @ ^2.0.0
|
||||
debug_tool = jlink
|
||||
upload_protocol = nrfutil
|
||||
|
||||
[env:LilyGo_T-Echo-Lite_repeater]
|
||||
extends = LilyGo_T-Echo-Lite
|
||||
build_src_filter = ${LilyGo_T-Echo-Lite.build_src_filter}
|
||||
+<../examples/simple_repeater>
|
||||
build_flags =
|
||||
${LilyGo_T-Echo-Lite.build_flags}
|
||||
-D ADVERT_NAME='"T-Echo-Lite Repeater"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
|
||||
[env:LilyGo_T-Echo-Lite_room_server]
|
||||
extends = LilyGo_T-Echo-Lite
|
||||
build_src_filter = ${LilyGo_T-Echo-Lite.build_src_filter}
|
||||
+<../examples/simple_room_server>
|
||||
build_flags =
|
||||
${LilyGo_T-Echo-Lite.build_flags}
|
||||
-D ADVERT_NAME='"T-Echo-Lite Room"'
|
||||
-D ADVERT_LAT=0.0
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
|
||||
[env:LilyGo_T-Echo-Lite_companion_radio_ble]
|
||||
extends = LilyGo_T-Echo-Lite
|
||||
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld
|
||||
board_upload.maximum_size = 712704
|
||||
build_flags =
|
||||
${LilyGo_T-Echo-Lite.build_flags}
|
||||
-I src/helpers/ui
|
||||
-I examples/companion_radio/ui-new
|
||||
-D MAX_CONTACTS=350
|
||||
-D MAX_GROUP_CHANNELS=40
|
||||
; -D QSPIFLASH=1
|
||||
-D BLE_PIN_CODE=123456
|
||||
; -D BLE_DEBUG_LOGGING=1
|
||||
-D OFFLINE_QUEUE_SIZE=256
|
||||
-D UI_RECENT_LIST_SIZE=9
|
||||
-D UI_SENSORS_PAGE=1
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
-D AUTO_SHUTDOWN_MILLIVOLTS=3300
|
||||
build_src_filter = ${LilyGo_T-Echo-Lite.build_src_filter}
|
||||
+<helpers/nrf52/SerialBLEInterface.cpp>
|
||||
+<../examples/companion_radio/*.cpp>
|
||||
+<../examples/companion_radio/ui-new/*.cpp>
|
||||
lib_deps =
|
||||
${LilyGo_T-Echo-Lite.lib_deps}
|
||||
densaugeo/base64 @ ~1.4.0
|
||||
@@ -1,52 +0,0 @@
|
||||
#include <Arduino.h>
|
||||
#include "target.h"
|
||||
#include <helpers/ArduinoHelpers.h>
|
||||
#include <helpers/sensors/MicroNMEALocationProvider.h>
|
||||
|
||||
TechoBoard board;
|
||||
|
||||
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI);
|
||||
|
||||
WRAPPER_CLASS radio_driver(radio, board);
|
||||
|
||||
VolatileRTCClock fallback_clock;
|
||||
AutoDiscoverRTCClock rtc_clock(fallback_clock);
|
||||
|
||||
#ifdef ENV_INCLUDE_GPS
|
||||
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock);
|
||||
EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea);
|
||||
#else
|
||||
EnvironmentSensorManager sensors = EnvironmentSensorManager();
|
||||
#endif
|
||||
|
||||
#ifdef DISPLAY_CLASS
|
||||
DISPLAY_CLASS display;
|
||||
MomentaryButton user_btn(PIN_USER_BTN, 1000, true);
|
||||
#endif
|
||||
|
||||
bool radio_init() {
|
||||
rtc_clock.begin(Wire);
|
||||
|
||||
return radio.std_init(&SPI);
|
||||
}
|
||||
|
||||
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 <TechoBoard.h>
|
||||
#include <helpers/radiolib/CustomSX1262Wrapper.h>
|
||||
#include <helpers/AutoDiscoverRTCClock.h>
|
||||
#include <helpers/SensorManager.h>
|
||||
#include <helpers/sensors/EnvironmentSensorManager.h>
|
||||
#include <helpers/sensors/LocationProvider.h>
|
||||
#ifdef DISPLAY_CLASS
|
||||
#include <helpers/ui/GxEPDDisplay.h>
|
||||
#include <helpers/ui/MomentaryButton.h>
|
||||
#endif
|
||||
|
||||
extern TechoBoard 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();
|
||||
@@ -1,39 +0,0 @@
|
||||
#include "variant.h"
|
||||
#include "wiring_constants.h"
|
||||
#include "wiring_digital.h"
|
||||
|
||||
const int MISO = PIN_SPI1_MISO;
|
||||
const int MOSI = PIN_SPI1_MOSI;
|
||||
const int SCK = PIN_SPI1_SCK;
|
||||
|
||||
const uint32_t g_ADigitalPinMap[] = {
|
||||
0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,
|
||||
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26,
|
||||
27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39,
|
||||
40, 41, 42, 43, 44, 45, 46, 47
|
||||
};
|
||||
|
||||
void initVariant() {
|
||||
pinMode(PIN_PWR_EN, OUTPUT);
|
||||
digitalWrite(PIN_PWR_EN, HIGH);
|
||||
|
||||
pinMode(PIN_BUTTON1, INPUT_PULLUP);
|
||||
pinMode(PIN_BUTTON2, INPUT_PULLUP);
|
||||
|
||||
pinMode(LED_RED, OUTPUT);
|
||||
pinMode(LED_GREEN, OUTPUT);
|
||||
pinMode(LED_BLUE, OUTPUT);
|
||||
digitalWrite(LED_BLUE, HIGH);
|
||||
digitalWrite(LED_GREEN, HIGH);
|
||||
digitalWrite(LED_RED, HIGH);
|
||||
|
||||
// pinMode(PIN_TXCO, OUTPUT);
|
||||
// digitalWrite(PIN_TXCO, HIGH);
|
||||
|
||||
pinMode(DISP_POWER, OUTPUT);
|
||||
digitalWrite(DISP_POWER, LOW);
|
||||
|
||||
// shutdown gps
|
||||
pinMode(GPS_EN, OUTPUT);
|
||||
digitalWrite(GPS_EN, LOW);
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
/*
|
||||
* variant.h
|
||||
* Copyright (C) 2023 Seeed K.K.
|
||||
* MIT License
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#define _PINNUM(port, pin) ((port) * 32 + (pin))
|
||||
|
||||
#include "WVariant.h"
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Low frequency clock source
|
||||
|
||||
#define USE_LFXO // 32.768 kHz crystal oscillator
|
||||
#define VARIANT_MCK (64000000ul)
|
||||
|
||||
#define WIRE_INTERFACES_COUNT (1)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Power
|
||||
|
||||
#define PIN_PWR_EN _PINNUM(0, 30) // RT9080_EN
|
||||
|
||||
#define BATTERY_PIN _PINNUM(0, 2)
|
||||
#define ADC_MULTIPLIER (4.90F)
|
||||
|
||||
#define ADC_RESOLUTION (14)
|
||||
#define BATTERY_SENSE_RES (12)
|
||||
|
||||
#define AREF_VOLTAGE (3.0)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Number of pins
|
||||
|
||||
#define PINS_COUNT (48)
|
||||
#define NUM_DIGITAL_PINS (48)
|
||||
#define NUM_ANALOG_INPUTS (1)
|
||||
#define NUM_ANALOG_OUTPUTS (0)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// UART pin definition
|
||||
|
||||
#define PIN_SERIAL1_RX PIN_GPS_TX
|
||||
#define PIN_SERIAL1_TX PIN_GPS_RX
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// I2C pin definition
|
||||
|
||||
#define PIN_WIRE_SDA _PINNUM(0, 4) // (SDA)
|
||||
#define PIN_WIRE_SCL _PINNUM(0, 2) // (SCL)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// SPI pin definition
|
||||
|
||||
#define SPI_INTERFACES_COUNT _PINNUM(0, 2)
|
||||
|
||||
#define PIN_SPI_MISO _PINNUM(0, 17) // (MISO)
|
||||
#define PIN_SPI_MOSI _PINNUM(0, 15) // (MOSI)
|
||||
#define PIN_SPI_SCK _PINNUM(0, 13) // (SCK)
|
||||
#define PIN_SPI_NSS (-1)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// QSPI FLASH
|
||||
|
||||
#define PIN_QSPI_SCK _PINNUM(0, 4)
|
||||
#define PIN_QSPI_CS _PINNUM(0, 12)
|
||||
#define PIN_QSPI_IO0 _PINNUM(0, 6)
|
||||
#define PIN_QSPI_IO1 _PINNUM(0, 8)
|
||||
#define PIN_QSPI_IO2 _PINNUM(1, 9)
|
||||
#define PIN_QSPI_IO3 _PINNUM(0, 26)
|
||||
|
||||
#define EXTERNAL_FLASH_DEVICES ZD25WQ32CEIGR
|
||||
#define EXTERNAL_FLASH_USE_QSPI
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Builtin LEDs
|
||||
|
||||
#define LED_RED _PINNUM(1, 14) // LED_3
|
||||
#define LED_BLUE _PINNUM(1, 5) // LED_2
|
||||
#define LED_GREEN _PINNUM(1, 7) // LED_1
|
||||
|
||||
//#define PIN_STATUS_LED LED_BLUE
|
||||
#define LED_BUILTIN (-1)
|
||||
#define LED_PIN LED_BUILTIN
|
||||
#define LED_STATE_ON LOW
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Builtin buttons
|
||||
|
||||
#define PIN_BUTTON1 _PINNUM(0, 24) // BOOT
|
||||
#define BUTTON_PIN PIN_BUTTON1
|
||||
#define PIN_USER_BTN BUTTON_PIN
|
||||
|
||||
#define PIN_BUTTON2 _PINNUM(0, 18)
|
||||
#define BUTTON_PIN2 PIN_BUTTON2
|
||||
|
||||
#define EXTERNAL_FLASH_DEVICES MX25R1635F
|
||||
#define EXTERNAL_FLASH_USE_QSPI
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Lora
|
||||
|
||||
#define USE_SX1262
|
||||
#define LORA_CS _PINNUM(0, 11)
|
||||
#define SX126X_POWER_EN _PINNUM(0, 30)
|
||||
#define SX126X_DIO1 _PINNUM(1, 8)
|
||||
#define SX126X_BUSY _PINNUM(0, 14)
|
||||
#define SX126X_RESET _PINNUM(0, 7)
|
||||
#define SX126X_RF_VC1 _PINNUM(0, 27)
|
||||
#define SX126X_RF_VC2 _PINNUM(0, 33)
|
||||
|
||||
#define P_LORA_DIO_1 SX126X_DIO1
|
||||
#define P_LORA_NSS LORA_CS
|
||||
#define P_LORA_RESET SX126X_RESET
|
||||
#define P_LORA_BUSY SX126X_BUSY
|
||||
#define P_LORA_SCLK PIN_SPI_SCK
|
||||
#define P_LORA_MISO PIN_SPI_MISO
|
||||
#define P_LORA_MOSI PIN_SPI_MOSI
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// SPI1
|
||||
|
||||
#define PIN_SPI1_MISO (-1) // Not used for Display
|
||||
#define PIN_SPI1_MOSI _PINNUM(0, 20)
|
||||
#define PIN_SPI1_SCK _PINNUM(0, 19)
|
||||
|
||||
// GxEPD2 needs that for a panel that is not even used !
|
||||
extern const int MISO;
|
||||
extern const int MOSI;
|
||||
extern const int SCK;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Display
|
||||
|
||||
// #define DISP_MISO (-1) // Not used for Display
|
||||
#define DISP_MOSI _PINNUM(0, 20)
|
||||
#define DISP_SCLK _PINNUM(0, 19)
|
||||
#define DISP_CS _PINNUM(0, 22)
|
||||
#define DISP_DC _PINNUM(0, 21)
|
||||
#define DISP_RST _PINNUM(0, 28)
|
||||
#define DISP_BUSY _PINNUM(0, 3)
|
||||
#define DISP_POWER _PINNUM(1, 12)
|
||||
// #define DISP_BACKLIGHT (-1) // Display has no backlight
|
||||
|
||||
#define PIN_DISPLAY_CS DISP_CS
|
||||
#define PIN_DISPLAY_DC DISP_DC
|
||||
#define PIN_DISPLAY_RST DISP_RST
|
||||
#define PIN_DISPLAY_BUSY DISP_BUSY
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// GPS
|
||||
|
||||
#define PIN_GPS_RX _PINNUM(1, 13) // RXD
|
||||
#define PIN_GPS_TX _PINNUM(1, 15) // TXD
|
||||
#define GPS_EN _PINNUM(1, 11) // POWER_RT9080_EN
|
||||
#define PIN_GPS_STANDBY _PINNUM(1, 10)
|
||||
#define PIN_GPS_PPS _PINNUM(0, 29) // 1PPS
|
||||
Reference in New Issue
Block a user