mirror of
https://github.com/pelgraine/Meck.git
synced 2026-03-28 17:42:44 +01:00
New settings screen and key remapping for menu screens
This commit is contained in:
@@ -12,7 +12,7 @@
|
||||
#endif
|
||||
|
||||
#ifndef FIRMWARE_VERSION
|
||||
#define FIRMWARE_VERSION "Meck v0.8.2"
|
||||
#define FIRMWARE_VERSION "Meck v0.8.3"
|
||||
#endif
|
||||
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
@@ -180,6 +180,18 @@ public:
|
||||
backupSettingsToSD();
|
||||
#endif
|
||||
}
|
||||
void saveChannels() {
|
||||
_store->saveChannels(this);
|
||||
#if defined(LilyGo_TDeck_Pro) && defined(HAS_SDCARD)
|
||||
backupSettingsToSD();
|
||||
#endif
|
||||
}
|
||||
void saveContacts() {
|
||||
_store->saveContacts(this);
|
||||
#if defined(LilyGo_TDeck_Pro) && defined(HAS_SDCARD)
|
||||
backupSettingsToSD();
|
||||
#endif
|
||||
}
|
||||
|
||||
private:
|
||||
void writeOKFrame();
|
||||
@@ -199,20 +211,6 @@ private:
|
||||
void checkCLIRescueCmd();
|
||||
void checkSerialInterface();
|
||||
|
||||
// helpers, short-cuts
|
||||
void saveChannels() {
|
||||
_store->saveChannels(this);
|
||||
#if defined(LilyGo_TDeck_Pro) && defined(HAS_SDCARD)
|
||||
backupSettingsToSD();
|
||||
#endif
|
||||
}
|
||||
void saveContacts() {
|
||||
_store->saveContacts(this);
|
||||
#if defined(LilyGo_TDeck_Pro) && defined(HAS_SDCARD)
|
||||
backupSettingsToSD();
|
||||
#endif
|
||||
}
|
||||
|
||||
DataStore* _store;
|
||||
NodePrefs _prefs;
|
||||
uint32_t pending_login;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "TextReaderScreen.h"
|
||||
#include "ContactsScreen.h"
|
||||
#include "ChannelScreen.h"
|
||||
#include "SettingsScreen.h"
|
||||
extern SPIClass displaySpi; // From GxEPDDisplay.cpp, shared SPI bus
|
||||
|
||||
TCA8418Keyboard keyboard(I2C_ADDR_KEYBOARD, &Wire);
|
||||
@@ -447,6 +448,12 @@ void setup() {
|
||||
the_mesh.startInterface(serial_interface);
|
||||
MESH_DEBUG_PRINTLN("setup() - the_mesh.startInterface() done");
|
||||
|
||||
// T-Deck Pro: default BLE to OFF on boot (user can toggle with Bluetooth page)
|
||||
#if defined(LilyGo_TDeck_Pro)
|
||||
serial_interface.disable();
|
||||
MESH_DEBUG_PRINTLN("setup() - BLE disabled by default (toggle via home screen)");
|
||||
#endif
|
||||
|
||||
#else
|
||||
#error "need to define filesystem"
|
||||
#endif
|
||||
@@ -503,6 +510,23 @@ void setup() {
|
||||
}
|
||||
#endif
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// First-boot onboarding detection
|
||||
// Check if node name is still the default hex prefix (first 4 bytes of pub key)
|
||||
// If so, launch onboarding wizard to set name and radio preset
|
||||
// ---------------------------------------------------------------------------
|
||||
#if defined(LilyGo_TDeck_Pro)
|
||||
{
|
||||
char defaultName[10];
|
||||
mesh::Utils::toHex(defaultName, the_mesh.self_id.pub_key, 4);
|
||||
NodePrefs* prefs = the_mesh.getNodePrefs();
|
||||
if (strcmp(prefs->node_name, defaultName) == 0) {
|
||||
MESH_DEBUG_PRINTLN("setup() - Default node name detected, launching onboarding");
|
||||
ui_task.gotoOnboarding();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Enable GPS by default on T-Deck Pro
|
||||
#if HAS_GPS
|
||||
// Set GPS enabled in both sensor manager and node prefs
|
||||
@@ -745,20 +769,28 @@ void handleKeyboardInput() {
|
||||
return;
|
||||
}
|
||||
|
||||
// C key: allow entering compose mode from reader
|
||||
if (key == 'c' || key == 'C') {
|
||||
composeDM = false;
|
||||
composeDMContactIdx = -1;
|
||||
composeMode = true;
|
||||
composeBuffer[0] = '\0';
|
||||
composePos = 0;
|
||||
Serial.printf("Entering compose mode from reader, channel %d\n", composeChannelIdx);
|
||||
drawComposeScreen();
|
||||
lastComposeRefresh = millis();
|
||||
// All other keys pass through to the reader screen
|
||||
ui_task.injectKey(key);
|
||||
return;
|
||||
}
|
||||
|
||||
// *** SETTINGS MODE ***
|
||||
if (ui_task.isOnSettingsScreen()) {
|
||||
SettingsScreen* settings = (SettingsScreen*)ui_task.getSettingsScreen();
|
||||
|
||||
// Q key: exit settings (when not editing)
|
||||
if (!settings->isEditing() && (key == 'q' || key == 'Q')) {
|
||||
if (settings->hasRadioChanges()) {
|
||||
// Let settings show "apply changes?" confirm dialog
|
||||
ui_task.injectKey(key);
|
||||
} else {
|
||||
Serial.println("Exiting settings");
|
||||
ui_task.gotoHomeScreen();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// All other keys pass through to the reader screen
|
||||
|
||||
// All other keys → settings screen via injectKey (no forceRefresh)
|
||||
ui_task.injectKey(key);
|
||||
return;
|
||||
}
|
||||
@@ -767,38 +799,11 @@ void handleKeyboardInput() {
|
||||
switch (key) {
|
||||
case 'c':
|
||||
case 'C':
|
||||
// Enter compose mode - DM if on contacts screen, channel otherwise
|
||||
if (ui_task.isOnContactsScreen()) {
|
||||
ContactsScreen* cs = (ContactsScreen*)ui_task.getContactsScreen();
|
||||
int idx = cs->getSelectedContactIdx();
|
||||
uint8_t ctype = cs->getSelectedContactType();
|
||||
if (idx >= 0 && ctype == ADV_TYPE_CHAT) {
|
||||
composeDM = true;
|
||||
composeDMContactIdx = idx;
|
||||
cs->getSelectedContactName(composeDMName, sizeof(composeDMName));
|
||||
composeMode = true;
|
||||
composeBuffer[0] = '\0';
|
||||
composePos = 0;
|
||||
Serial.printf("Entering DM compose to %s (idx %d)\n", composeDMName, idx);
|
||||
drawComposeScreen();
|
||||
lastComposeRefresh = millis();
|
||||
}
|
||||
} else {
|
||||
composeDM = false;
|
||||
composeDMContactIdx = -1;
|
||||
composeMode = true;
|
||||
composeBuffer[0] = '\0';
|
||||
composePos = 0;
|
||||
// If on channel screen, sync compose channel with viewed channel
|
||||
if (ui_task.isOnChannelScreen()) {
|
||||
composeChannelIdx = ui_task.getChannelScreenViewIdx();
|
||||
}
|
||||
Serial.printf("Entering compose mode, channel %d\n", composeChannelIdx);
|
||||
drawComposeScreen();
|
||||
lastComposeRefresh = millis();
|
||||
}
|
||||
// Open contacts list
|
||||
Serial.println("Opening contacts");
|
||||
ui_task.gotoContactsScreen();
|
||||
break;
|
||||
|
||||
|
||||
case 'm':
|
||||
case 'M':
|
||||
// Go to channel message screen
|
||||
@@ -806,18 +811,22 @@ void handleKeyboardInput() {
|
||||
ui_task.gotoChannelScreen();
|
||||
break;
|
||||
|
||||
case 'r':
|
||||
case 'R':
|
||||
// Open text reader
|
||||
case 'e':
|
||||
case 'E':
|
||||
// Open text reader (ebooks)
|
||||
Serial.println("Opening text reader");
|
||||
ui_task.gotoTextReader();
|
||||
break;
|
||||
|
||||
case 'n':
|
||||
case 'N':
|
||||
// Open contacts list
|
||||
Serial.println("Opening contacts");
|
||||
ui_task.gotoContactsScreen();
|
||||
case 's':
|
||||
case 'S':
|
||||
// Open settings (from home), or navigate down on channel/contacts
|
||||
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen()) {
|
||||
ui_task.injectKey('s'); // Pass directly for channel/contacts scrolling
|
||||
} else {
|
||||
Serial.println("Opening settings");
|
||||
ui_task.gotoSettingsScreen();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'w':
|
||||
@@ -831,17 +840,6 @@ void handleKeyboardInput() {
|
||||
}
|
||||
break;
|
||||
|
||||
case 's':
|
||||
case 'S':
|
||||
// Navigate down/next (scroll on channel screen)
|
||||
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
|
||||
}
|
||||
break;
|
||||
|
||||
case 'a':
|
||||
case 'A':
|
||||
// Navigate left or switch channel (on channel screen)
|
||||
@@ -865,7 +863,7 @@ void handleKeyboardInput() {
|
||||
break;
|
||||
|
||||
case '\r':
|
||||
// Select/Enter - if on contacts screen, enter DM compose for chat contacts
|
||||
// Enter = compose (only from channel or contacts screen)
|
||||
if (ui_task.isOnContactsScreen()) {
|
||||
ContactsScreen* cs = (ContactsScreen*)ui_task.getContactsScreen();
|
||||
int idx = cs->getSelectedContactIdx();
|
||||
@@ -881,12 +879,21 @@ void handleKeyboardInput() {
|
||||
drawComposeScreen();
|
||||
lastComposeRefresh = millis();
|
||||
} else if (idx >= 0) {
|
||||
// Non-chat contact selected (repeater, room, etc.) - future use
|
||||
Serial.printf("Selected non-chat contact type=%d idx=%d\n", ctype, idx);
|
||||
}
|
||||
} else if (ui_task.isOnChannelScreen()) {
|
||||
composeDM = false;
|
||||
composeDMContactIdx = -1;
|
||||
composeChannelIdx = ui_task.getChannelScreenViewIdx();
|
||||
composeMode = true;
|
||||
composeBuffer[0] = '\0';
|
||||
composePos = 0;
|
||||
Serial.printf("Entering compose mode, channel %d\n", composeChannelIdx);
|
||||
drawComposeScreen();
|
||||
lastComposeRefresh = millis();
|
||||
} else {
|
||||
Serial.println("Nav: Enter/Select");
|
||||
ui_task.injectKey(13); // KEY_ENTER
|
||||
// Other screens: pass Enter as generic select
|
||||
ui_task.injectKey(13);
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ private:
|
||||
public:
|
||||
ChannelScreen(UITask* task, mesh::RTCClock* rtc)
|
||||
: _task(task), _rtc(rtc), _msgCount(0), _newestIdx(-1), _scrollPos(0),
|
||||
_msgsPerPage(3), _viewChannelIdx(0), _sdReady(false) {
|
||||
_msgsPerPage(CHANNEL_MSG_HISTORY_SIZE), _viewChannelIdx(0), _sdReady(false) {
|
||||
// Initialize all messages as invalid
|
||||
for (int i = 0; i < CHANNEL_MSG_HISTORY_SIZE; i++) {
|
||||
_messages[i].valid = false;
|
||||
@@ -327,6 +327,7 @@ public:
|
||||
|
||||
// Display messages oldest-to-newest (top to bottom)
|
||||
int msgsDrawn = 0;
|
||||
bool screenFull = false;
|
||||
for (int i = startIdx; i < numChannelMsgs && y + lineHeight <= maxY; i++) {
|
||||
int idx = channelMsgs[i];
|
||||
ChannelMessage* msg = &_messages[idx];
|
||||
@@ -456,6 +457,13 @@ public:
|
||||
|
||||
y += 2; // Small gap between messages
|
||||
msgsDrawn++;
|
||||
if (y + lineHeight > maxY) screenFull = true;
|
||||
}
|
||||
|
||||
// Only update _msgsPerPage when the screen actually filled up.
|
||||
// If we ran out of messages before filling the screen, keep the
|
||||
// previous (higher) value so startIdx doesn't under-count.
|
||||
if (screenFull && msgsDrawn > 0) {
|
||||
_msgsPerPage = msgsDrawn;
|
||||
}
|
||||
|
||||
@@ -471,8 +479,8 @@ public:
|
||||
// Left side: Q:Back A/D:Ch
|
||||
display.print("Q:Back A/D:Ch");
|
||||
|
||||
// Right side: C:New
|
||||
const char* rightText = "C:New";
|
||||
// Right side: Entr:New
|
||||
const char* rightText = "Entr:New";
|
||||
display.setCursor(display.width() - display.getTextWidth(rightText) - 2, footerY);
|
||||
display.print(rightText);
|
||||
|
||||
|
||||
849
examples/companion_radio/ui-new/Settingsscreen.h
Normal file
849
examples/companion_radio/ui-new/Settingsscreen.h
Normal file
@@ -0,0 +1,849 @@
|
||||
#pragma once
|
||||
|
||||
#include <helpers/ui/UIScreen.h>
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
#include <helpers/ChannelDetails.h>
|
||||
#include <MeshCore.h>
|
||||
#include "../NodePrefs.h"
|
||||
|
||||
// Forward declarations
|
||||
class UITask;
|
||||
class MyMesh;
|
||||
extern MyMesh the_mesh;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Radio presets
|
||||
// ---------------------------------------------------------------------------
|
||||
struct RadioPreset {
|
||||
const char* name;
|
||||
float freq;
|
||||
float bw;
|
||||
uint8_t sf;
|
||||
uint8_t cr;
|
||||
uint8_t tx_power;
|
||||
};
|
||||
|
||||
static const RadioPreset RADIO_PRESETS[] = {
|
||||
{ "MeshCore Default", 915.0f, 250.0f, 10, 5, 20 },
|
||||
{ "Long Range", 915.0f, 125.0f, 12, 8, 20 },
|
||||
{ "Fast/Short", 915.0f, 500.0f, 7, 5, 20 },
|
||||
{ "EU Default", 869.4f, 250.0f, 10, 5, 14 },
|
||||
};
|
||||
#define NUM_RADIO_PRESETS (sizeof(RADIO_PRESETS) / sizeof(RADIO_PRESETS[0]))
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Settings row types
|
||||
// ---------------------------------------------------------------------------
|
||||
enum SettingsRowType : uint8_t {
|
||||
ROW_NAME, // Device name (text editor)
|
||||
ROW_RADIO_PRESET, // Radio preset picker
|
||||
ROW_FREQ, // Frequency (float)
|
||||
ROW_BW, // Bandwidth (float)
|
||||
ROW_SF, // Spreading factor (5-12)
|
||||
ROW_CR, // Coding rate (5-8)
|
||||
ROW_TX_POWER, // TX power (1-20 dBm)
|
||||
ROW_UTC_OFFSET, // UTC offset (-12 to +14)
|
||||
ROW_CH_HEADER, // "--- Channels ---" separator
|
||||
ROW_CHANNEL, // A channel entry (dynamic, index stored separately)
|
||||
ROW_ADD_CHANNEL, // "+ Add Hashtag Channel"
|
||||
ROW_INFO_HEADER, // "--- Info ---" separator
|
||||
ROW_PUB_KEY, // Public key display
|
||||
ROW_FIRMWARE, // Firmware version
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Editing modes
|
||||
// ---------------------------------------------------------------------------
|
||||
enum EditMode : uint8_t {
|
||||
EDIT_NONE, // Just browsing
|
||||
EDIT_TEXT, // Typing into a text buffer (name, channel name)
|
||||
EDIT_PICKER, // A/D cycles options (radio preset)
|
||||
EDIT_NUMBER, // W/S adjusts value (freq, BW, SF, CR, TX, UTC)
|
||||
EDIT_CONFIRM, // Confirmation dialog (delete channel, apply radio)
|
||||
};
|
||||
|
||||
// Max rows in the settings list
|
||||
#define SETTINGS_MAX_ROWS 40
|
||||
#define SETTINGS_TEXT_BUF 33 // 32 chars + null
|
||||
|
||||
class SettingsScreen : public UIScreen {
|
||||
private:
|
||||
UITask* _task;
|
||||
mesh::RTCClock* _rtc;
|
||||
NodePrefs* _prefs;
|
||||
|
||||
// Row table — rebuilt whenever channels change
|
||||
struct Row {
|
||||
SettingsRowType type;
|
||||
uint8_t param; // channel index for ROW_CHANNEL, preset index for ROW_RADIO_PRESET
|
||||
};
|
||||
Row _rows[SETTINGS_MAX_ROWS];
|
||||
int _numRows;
|
||||
|
||||
// Cursor & scroll
|
||||
int _cursor; // selected row
|
||||
int _scrollTop; // first visible row
|
||||
|
||||
// Editing state
|
||||
EditMode _editMode;
|
||||
char _editBuf[SETTINGS_TEXT_BUF];
|
||||
int _editPos;
|
||||
int _editPickerIdx; // for preset picker
|
||||
float _editFloat; // for freq/BW editing
|
||||
int _editInt; // for SF/CR/TX/UTC editing
|
||||
int _confirmAction; // 0=none, 1=delete channel, 2=apply radio
|
||||
|
||||
// Onboarding mode
|
||||
bool _onboarding;
|
||||
|
||||
// Dirty flag for radio params — prompt to apply
|
||||
bool _radioChanged;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Row table management
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void rebuildRows() {
|
||||
_numRows = 0;
|
||||
|
||||
addRow(ROW_NAME);
|
||||
addRow(ROW_RADIO_PRESET);
|
||||
addRow(ROW_FREQ);
|
||||
addRow(ROW_BW);
|
||||
addRow(ROW_SF);
|
||||
addRow(ROW_CR);
|
||||
addRow(ROW_TX_POWER);
|
||||
addRow(ROW_UTC_OFFSET);
|
||||
addRow(ROW_CH_HEADER);
|
||||
|
||||
// Enumerate current channels
|
||||
for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) {
|
||||
ChannelDetails ch;
|
||||
if (the_mesh.getChannel(i, ch) && ch.name[0] != '\0') {
|
||||
addRow(ROW_CHANNEL, i);
|
||||
} else {
|
||||
break; // channels are contiguous
|
||||
}
|
||||
}
|
||||
|
||||
addRow(ROW_ADD_CHANNEL);
|
||||
addRow(ROW_INFO_HEADER);
|
||||
addRow(ROW_PUB_KEY);
|
||||
addRow(ROW_FIRMWARE);
|
||||
|
||||
// Clamp cursor
|
||||
if (_cursor >= _numRows) _cursor = _numRows - 1;
|
||||
if (_cursor < 0) _cursor = 0;
|
||||
skipNonSelectable(1);
|
||||
}
|
||||
|
||||
void addRow(SettingsRowType type, uint8_t param = 0) {
|
||||
if (_numRows < SETTINGS_MAX_ROWS) {
|
||||
_rows[_numRows].type = type;
|
||||
_rows[_numRows].param = param;
|
||||
_numRows++;
|
||||
}
|
||||
}
|
||||
|
||||
bool isSelectable(int idx) const {
|
||||
if (idx < 0 || idx >= _numRows) return false;
|
||||
SettingsRowType t = _rows[idx].type;
|
||||
return t != ROW_CH_HEADER && t != ROW_INFO_HEADER;
|
||||
}
|
||||
|
||||
void skipNonSelectable(int dir) {
|
||||
while (_cursor >= 0 && _cursor < _numRows && !isSelectable(_cursor)) {
|
||||
_cursor += dir;
|
||||
}
|
||||
if (_cursor < 0) _cursor = 0;
|
||||
if (_cursor >= _numRows) _cursor = _numRows - 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Radio preset detection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
int detectCurrentPreset() const {
|
||||
for (int i = 0; i < (int)NUM_RADIO_PRESETS; i++) {
|
||||
const RadioPreset& p = RADIO_PRESETS[i];
|
||||
if (fabsf(_prefs->freq - p.freq) < 0.01f &&
|
||||
fabsf(_prefs->bw - p.bw) < 0.01f &&
|
||||
_prefs->sf == p.sf &&
|
||||
_prefs->cr == p.cr &&
|
||||
_prefs->tx_power_dbm == p.tx_power) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1; // Custom
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hashtag channel creation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void createHashtagChannel(const char* name) {
|
||||
// Build channel name with # prefix if not already present
|
||||
char chanName[32];
|
||||
if (name[0] == '#') {
|
||||
strncpy(chanName, name, sizeof(chanName));
|
||||
} else {
|
||||
chanName[0] = '#';
|
||||
strncpy(&chanName[1], name, sizeof(chanName) - 1);
|
||||
}
|
||||
chanName[31] = '\0';
|
||||
|
||||
// Generate 128-bit PSK from SHA-256 of channel name
|
||||
ChannelDetails newCh;
|
||||
memset(&newCh, 0, sizeof(newCh));
|
||||
strncpy(newCh.name, chanName, sizeof(newCh.name));
|
||||
newCh.name[31] = '\0';
|
||||
|
||||
// SHA-256 the channel name → first 16 bytes become the secret
|
||||
uint8_t hash[32];
|
||||
mesh::Utils::sha256(hash, 32, (const uint8_t*)chanName, strlen(chanName));
|
||||
memcpy(newCh.channel.secret, hash, 16);
|
||||
// Upper 16 bytes left as zero → setChannel uses 128-bit mode
|
||||
|
||||
// Find next empty slot
|
||||
for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) {
|
||||
ChannelDetails existing;
|
||||
if (!the_mesh.getChannel(i, existing) || existing.name[0] == '\0') {
|
||||
if (the_mesh.setChannel(i, newCh)) {
|
||||
the_mesh.saveChannels();
|
||||
Serial.printf("Settings: Created hashtag channel '%s' at idx %d\n", chanName, i);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void deleteChannel(uint8_t idx) {
|
||||
// Clear the channel by writing an empty ChannelDetails
|
||||
// Then compact: shift all channels above it down by one
|
||||
ChannelDetails empty;
|
||||
memset(&empty, 0, sizeof(empty));
|
||||
|
||||
// Find total channel count
|
||||
int total = 0;
|
||||
for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) {
|
||||
ChannelDetails ch;
|
||||
if (the_mesh.getChannel(i, ch) && ch.name[0] != '\0') {
|
||||
total = i + 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Shift channels down
|
||||
for (int i = idx; i < total - 1; i++) {
|
||||
ChannelDetails next;
|
||||
if (the_mesh.getChannel(i + 1, next)) {
|
||||
the_mesh.setChannel(i, next);
|
||||
}
|
||||
}
|
||||
// Clear the last slot
|
||||
the_mesh.setChannel(total - 1, empty);
|
||||
the_mesh.saveChannels();
|
||||
Serial.printf("Settings: Deleted channel at idx %d, compacted %d channels\n", idx, total);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Apply radio parameters live
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void applyRadioParams() {
|
||||
radio_set_params(_prefs->freq, _prefs->bw, _prefs->sf, _prefs->cr);
|
||||
radio_set_tx_power(_prefs->tx_power_dbm);
|
||||
the_mesh.savePrefs();
|
||||
_radioChanged = false;
|
||||
Serial.printf("Settings: Radio params applied - %.3f/%g/%d/%d TX:%d\n",
|
||||
_prefs->freq, _prefs->bw, _prefs->sf, _prefs->cr, _prefs->tx_power_dbm);
|
||||
}
|
||||
|
||||
public:
|
||||
SettingsScreen(UITask* task, mesh::RTCClock* rtc, NodePrefs* prefs)
|
||||
: _task(task), _rtc(rtc), _prefs(prefs),
|
||||
_numRows(0), _cursor(0), _scrollTop(0),
|
||||
_editMode(EDIT_NONE), _editPos(0), _editPickerIdx(0),
|
||||
_editFloat(0), _editInt(0), _confirmAction(0),
|
||||
_onboarding(false), _radioChanged(false) {
|
||||
memset(_editBuf, 0, sizeof(_editBuf));
|
||||
}
|
||||
|
||||
void enter() {
|
||||
_editMode = EDIT_NONE;
|
||||
_cursor = 0;
|
||||
_scrollTop = 0;
|
||||
_radioChanged = false;
|
||||
rebuildRows();
|
||||
}
|
||||
|
||||
void enterOnboarding() {
|
||||
enter();
|
||||
_onboarding = true;
|
||||
// Start editing the device name immediately
|
||||
_cursor = 0; // ROW_NAME
|
||||
startEditText(_prefs->node_name);
|
||||
}
|
||||
|
||||
bool isOnboarding() const { return _onboarding; }
|
||||
bool isEditing() const { return _editMode != EDIT_NONE; }
|
||||
bool hasRadioChanges() const { return _radioChanged; }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Edit mode starters
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
void startEditText(const char* initial) {
|
||||
_editMode = EDIT_TEXT;
|
||||
strncpy(_editBuf, initial, SETTINGS_TEXT_BUF - 1);
|
||||
_editBuf[SETTINGS_TEXT_BUF - 1] = '\0';
|
||||
_editPos = strlen(_editBuf);
|
||||
}
|
||||
|
||||
void startEditPicker(int initialIdx) {
|
||||
_editMode = EDIT_PICKER;
|
||||
_editPickerIdx = initialIdx;
|
||||
}
|
||||
|
||||
void startEditFloat(float initial) {
|
||||
_editMode = EDIT_NUMBER;
|
||||
_editFloat = initial;
|
||||
}
|
||||
|
||||
void startEditInt(int initial) {
|
||||
_editMode = EDIT_NUMBER;
|
||||
_editInt = initial;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Rendering
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
int render(DisplayDriver& display) override {
|
||||
char tmp[64];
|
||||
|
||||
// === Header ===
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::GREEN);
|
||||
display.setCursor(0, 0);
|
||||
if (_onboarding) {
|
||||
display.print("Welcome! Setup");
|
||||
} else {
|
||||
display.print("Settings");
|
||||
}
|
||||
|
||||
// Right side: row indicator
|
||||
snprintf(tmp, sizeof(tmp), "%d/%d", _cursor + 1, _numRows);
|
||||
display.setCursor(display.width() - display.getTextWidth(tmp) - 2, 0);
|
||||
display.print(tmp);
|
||||
|
||||
display.drawRect(0, 11, display.width(), 1);
|
||||
|
||||
// === Body ===
|
||||
display.setTextSize(0); // tiny font
|
||||
int lineHeight = 9;
|
||||
int headerH = 14;
|
||||
int footerH = 14;
|
||||
int maxY = display.height() - footerH;
|
||||
|
||||
// Center scroll window around cursor
|
||||
int maxVisible = (maxY - headerH) / lineHeight;
|
||||
if (maxVisible < 3) maxVisible = 3;
|
||||
_scrollTop = max(0, min(_cursor - maxVisible / 2, _numRows - maxVisible));
|
||||
int endIdx = min(_numRows, _scrollTop + maxVisible);
|
||||
|
||||
int y = headerH;
|
||||
|
||||
for (int i = _scrollTop; i < endIdx && y + lineHeight <= maxY; i++) {
|
||||
bool selected = (i == _cursor);
|
||||
bool editing = selected && (_editMode != EDIT_NONE);
|
||||
|
||||
// Selection highlight
|
||||
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(0, y);
|
||||
|
||||
switch (_rows[i].type) {
|
||||
case ROW_NAME:
|
||||
if (editing && _editMode == EDIT_TEXT) {
|
||||
snprintf(tmp, sizeof(tmp), "Name: %s_", _editBuf);
|
||||
} else {
|
||||
snprintf(tmp, sizeof(tmp), "Name: %s", _prefs->node_name);
|
||||
}
|
||||
display.print(tmp);
|
||||
break;
|
||||
|
||||
case ROW_RADIO_PRESET: {
|
||||
int preset = detectCurrentPreset();
|
||||
if (editing && _editMode == EDIT_PICKER) {
|
||||
if (_editPickerIdx >= 0 && _editPickerIdx < (int)NUM_RADIO_PRESETS) {
|
||||
snprintf(tmp, sizeof(tmp), "< %s >", RADIO_PRESETS[_editPickerIdx].name);
|
||||
} else {
|
||||
strcpy(tmp, "< Custom >");
|
||||
}
|
||||
} else {
|
||||
if (preset >= 0) {
|
||||
snprintf(tmp, sizeof(tmp), "Preset: %s", RADIO_PRESETS[preset].name);
|
||||
} else {
|
||||
strcpy(tmp, "Preset: Custom");
|
||||
}
|
||||
}
|
||||
display.print(tmp);
|
||||
break;
|
||||
}
|
||||
|
||||
case ROW_FREQ:
|
||||
if (editing && _editMode == EDIT_NUMBER) {
|
||||
snprintf(tmp, sizeof(tmp), "Freq: %.3f <W/S>", _editFloat);
|
||||
} else {
|
||||
snprintf(tmp, sizeof(tmp), "Freq: %.3f MHz", _prefs->freq);
|
||||
}
|
||||
display.print(tmp);
|
||||
break;
|
||||
|
||||
case ROW_BW:
|
||||
if (editing && _editMode == EDIT_NUMBER) {
|
||||
snprintf(tmp, sizeof(tmp), "BW: %.1f <W/S>", _editFloat);
|
||||
} else {
|
||||
snprintf(tmp, sizeof(tmp), "BW: %.1f kHz", _prefs->bw);
|
||||
}
|
||||
display.print(tmp);
|
||||
break;
|
||||
|
||||
case ROW_SF:
|
||||
if (editing && _editMode == EDIT_NUMBER) {
|
||||
snprintf(tmp, sizeof(tmp), "SF: %d <W/S>", _editInt);
|
||||
} else {
|
||||
snprintf(tmp, sizeof(tmp), "SF: %d", _prefs->sf);
|
||||
}
|
||||
display.print(tmp);
|
||||
break;
|
||||
|
||||
case ROW_CR:
|
||||
if (editing && _editMode == EDIT_NUMBER) {
|
||||
snprintf(tmp, sizeof(tmp), "CR: %d <W/S>", _editInt);
|
||||
} else {
|
||||
snprintf(tmp, sizeof(tmp), "CR: %d", _prefs->cr);
|
||||
}
|
||||
display.print(tmp);
|
||||
break;
|
||||
|
||||
case ROW_TX_POWER:
|
||||
if (editing && _editMode == EDIT_NUMBER) {
|
||||
snprintf(tmp, sizeof(tmp), "TX: %d dBm <W/S>", _editInt);
|
||||
} else {
|
||||
snprintf(tmp, sizeof(tmp), "TX: %d dBm", _prefs->tx_power_dbm);
|
||||
}
|
||||
display.print(tmp);
|
||||
break;
|
||||
|
||||
case ROW_UTC_OFFSET:
|
||||
if (editing && _editMode == EDIT_NUMBER) {
|
||||
snprintf(tmp, sizeof(tmp), "UTC: %+d <W/S>", _editInt);
|
||||
} else {
|
||||
snprintf(tmp, sizeof(tmp), "UTC Offset: %+d", _prefs->utc_offset_hours);
|
||||
}
|
||||
display.print(tmp);
|
||||
break;
|
||||
|
||||
case ROW_CH_HEADER:
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
display.print("--- Channels ---");
|
||||
break;
|
||||
|
||||
case ROW_CHANNEL: {
|
||||
uint8_t chIdx = _rows[i].param;
|
||||
ChannelDetails ch;
|
||||
if (the_mesh.getChannel(chIdx, ch)) {
|
||||
if (chIdx == 0) {
|
||||
// Public channel - not deletable
|
||||
snprintf(tmp, sizeof(tmp), " %s", ch.name);
|
||||
} else {
|
||||
snprintf(tmp, sizeof(tmp), " %s", ch.name);
|
||||
if (selected) {
|
||||
// Show delete hint on right
|
||||
const char* hint = "Del:X";
|
||||
int hintW = display.getTextWidth(hint);
|
||||
display.setCursor(display.width() - hintW - 2, y);
|
||||
display.print(hint);
|
||||
display.setCursor(0, y);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
snprintf(tmp, sizeof(tmp), " (empty)");
|
||||
}
|
||||
display.print(tmp);
|
||||
break;
|
||||
}
|
||||
|
||||
case ROW_ADD_CHANNEL:
|
||||
if (editing && _editMode == EDIT_TEXT) {
|
||||
snprintf(tmp, sizeof(tmp), "# %s_", _editBuf);
|
||||
} else {
|
||||
display.setColor(selected ? DisplayDriver::DARK : DisplayDriver::GREEN);
|
||||
strcpy(tmp, "+ Add Hashtag Channel");
|
||||
}
|
||||
display.print(tmp);
|
||||
break;
|
||||
|
||||
case ROW_INFO_HEADER:
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
display.print("--- Device Info ---");
|
||||
break;
|
||||
|
||||
case ROW_PUB_KEY: {
|
||||
// Show first 8 bytes of pub key as hex (16 chars)
|
||||
char hexBuf[17];
|
||||
mesh::Utils::toHex(hexBuf, the_mesh.self_id.pub_key, 8);
|
||||
snprintf(tmp, sizeof(tmp), "ID: %s", hexBuf);
|
||||
display.print(tmp);
|
||||
break;
|
||||
}
|
||||
|
||||
case ROW_FIRMWARE:
|
||||
snprintf(tmp, sizeof(tmp), "FW: %s", FIRMWARE_VERSION);
|
||||
display.print(tmp);
|
||||
break;
|
||||
}
|
||||
|
||||
y += lineHeight;
|
||||
}
|
||||
|
||||
display.setTextSize(1);
|
||||
|
||||
// === Confirmation overlay ===
|
||||
if (_editMode == EDIT_CONFIRM) {
|
||||
int bx = 4, by = 30, bw = display.width() - 8, bh = 36;
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
display.fillRect(bx, by, bw, bh);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawRect(bx, by, bw, bh);
|
||||
|
||||
display.setTextSize(0);
|
||||
if (_confirmAction == 1) {
|
||||
uint8_t chIdx = _rows[_cursor].param;
|
||||
ChannelDetails ch;
|
||||
the_mesh.getChannel(chIdx, ch);
|
||||
snprintf(tmp, sizeof(tmp), "Delete %s?", ch.name);
|
||||
display.drawTextCentered(display.width() / 2, by + 4, tmp);
|
||||
} else if (_confirmAction == 2) {
|
||||
display.drawTextCentered(display.width() / 2, by + 4, "Apply radio changes?");
|
||||
}
|
||||
display.drawTextCentered(display.width() / 2, by + bh - 14, "Enter:Yes Q:No");
|
||||
display.setTextSize(1);
|
||||
}
|
||||
|
||||
// === Footer ===
|
||||
int footerY = display.height() - 12;
|
||||
display.drawRect(0, footerY - 2, display.width(), 1);
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
display.setCursor(0, footerY);
|
||||
|
||||
if (_editMode == EDIT_TEXT) {
|
||||
display.print("Type, Enter:Ok Q:Cancel");
|
||||
} else if (_editMode == EDIT_PICKER) {
|
||||
display.print("A/D:Choose Enter:Ok");
|
||||
} else if (_editMode == EDIT_NUMBER) {
|
||||
display.print("W/S:Adj Enter:Ok Q:Cancel");
|
||||
} else if (_editMode == EDIT_CONFIRM) {
|
||||
// Footer already covered by overlay
|
||||
} else {
|
||||
display.print("Q:Bck");
|
||||
const char* r = "W/S:Up/Dwn Entr:Chng";
|
||||
display.setCursor(display.width() - display.getTextWidth(r) - 2, footerY);
|
||||
display.print(r);
|
||||
}
|
||||
|
||||
return _editMode != EDIT_NONE ? 700 : 1000;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Input handling
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Handle a keyboard character. Returns true if the screen consumed the input.
|
||||
bool handleKeyInput(char c) {
|
||||
// --- Confirmation dialog ---
|
||||
if (_editMode == EDIT_CONFIRM) {
|
||||
if (c == '\r' || c == 13) {
|
||||
if (_confirmAction == 1) {
|
||||
// Delete channel
|
||||
uint8_t chIdx = _rows[_cursor].param;
|
||||
deleteChannel(chIdx);
|
||||
rebuildRows();
|
||||
} else if (_confirmAction == 2) {
|
||||
applyRadioParams();
|
||||
}
|
||||
_editMode = EDIT_NONE;
|
||||
_confirmAction = 0;
|
||||
return true;
|
||||
}
|
||||
if (c == 'q' || c == 'Q') {
|
||||
_editMode = EDIT_NONE;
|
||||
_confirmAction = 0;
|
||||
return true;
|
||||
}
|
||||
return true; // consume all keys in confirm mode
|
||||
}
|
||||
|
||||
// --- Text editing mode ---
|
||||
if (_editMode == EDIT_TEXT) {
|
||||
if (c == '\r' || c == 13) {
|
||||
// Confirm text edit
|
||||
SettingsRowType type = _rows[_cursor].type;
|
||||
if (type == ROW_NAME) {
|
||||
if (_editPos > 0) {
|
||||
strncpy(_prefs->node_name, _editBuf, sizeof(_prefs->node_name));
|
||||
_prefs->node_name[31] = '\0';
|
||||
the_mesh.savePrefs();
|
||||
Serial.printf("Settings: Name set to '%s'\n", _prefs->node_name);
|
||||
}
|
||||
_editMode = EDIT_NONE;
|
||||
if (_onboarding) {
|
||||
// Move to radio preset selection
|
||||
_cursor = 1; // ROW_RADIO_PRESET
|
||||
startEditPicker(max(0, detectCurrentPreset()));
|
||||
}
|
||||
} else if (type == ROW_ADD_CHANNEL) {
|
||||
if (_editPos > 0) {
|
||||
createHashtagChannel(_editBuf);
|
||||
rebuildRows();
|
||||
}
|
||||
_editMode = EDIT_NONE;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (c == 'q' || c == 'Q' || c == 27) {
|
||||
_editMode = EDIT_NONE;
|
||||
return true;
|
||||
}
|
||||
if (c == '\b') {
|
||||
if (_editPos > 0) {
|
||||
_editPos--;
|
||||
_editBuf[_editPos] = '\0';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// Printable character
|
||||
if (c >= 32 && c < 127 && _editPos < SETTINGS_TEXT_BUF - 1) {
|
||||
_editBuf[_editPos++] = c;
|
||||
_editBuf[_editPos] = '\0';
|
||||
return true;
|
||||
}
|
||||
return true; // consume all keys in text edit
|
||||
}
|
||||
|
||||
// --- Picker mode (radio preset) ---
|
||||
if (_editMode == EDIT_PICKER) {
|
||||
if (c == 'a' || c == 'A') {
|
||||
_editPickerIdx--;
|
||||
if (_editPickerIdx < 0) _editPickerIdx = (int)NUM_RADIO_PRESETS - 1;
|
||||
return true;
|
||||
}
|
||||
if (c == 'd' || c == 'D') {
|
||||
_editPickerIdx++;
|
||||
if (_editPickerIdx >= (int)NUM_RADIO_PRESETS) _editPickerIdx = 0;
|
||||
return true;
|
||||
}
|
||||
if (c == '\r' || c == 13) {
|
||||
// Apply preset
|
||||
if (_editPickerIdx >= 0 && _editPickerIdx < (int)NUM_RADIO_PRESETS) {
|
||||
const RadioPreset& p = RADIO_PRESETS[_editPickerIdx];
|
||||
_prefs->freq = p.freq;
|
||||
_prefs->bw = p.bw;
|
||||
_prefs->sf = p.sf;
|
||||
_prefs->cr = p.cr;
|
||||
_prefs->tx_power_dbm = p.tx_power;
|
||||
_radioChanged = true;
|
||||
}
|
||||
_editMode = EDIT_NONE;
|
||||
if (_onboarding) {
|
||||
// Apply and finish onboarding
|
||||
applyRadioParams();
|
||||
_onboarding = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (c == 'q' || c == 'Q') {
|
||||
_editMode = EDIT_NONE;
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Number editing mode ---
|
||||
if (_editMode == EDIT_NUMBER) {
|
||||
SettingsRowType type = _rows[_cursor].type;
|
||||
|
||||
if (c == 'w' || c == 'W') {
|
||||
switch (type) {
|
||||
case ROW_FREQ: _editFloat += 0.1f; break;
|
||||
case ROW_BW:
|
||||
// Cycle through common bandwidths
|
||||
if (_editFloat < 31.25f) _editFloat = 31.25f;
|
||||
else if (_editFloat < 62.5f) _editFloat = 62.5f;
|
||||
else if (_editFloat < 125.0f) _editFloat = 125.0f;
|
||||
else if (_editFloat < 250.0f) _editFloat = 250.0f;
|
||||
else _editFloat = 500.0f;
|
||||
break;
|
||||
case ROW_SF: if (_editInt < 12) _editInt++; break;
|
||||
case ROW_CR: if (_editInt < 8) _editInt++; break;
|
||||
case ROW_TX_POWER: if (_editInt < MAX_LORA_TX_POWER) _editInt++; break;
|
||||
case ROW_UTC_OFFSET: if (_editInt < 14) _editInt++; break;
|
||||
default: break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (c == 's' || c == 'S') {
|
||||
switch (type) {
|
||||
case ROW_FREQ: _editFloat -= 0.1f; break;
|
||||
case ROW_BW:
|
||||
if (_editFloat > 250.0f) _editFloat = 250.0f;
|
||||
else if (_editFloat > 125.0f) _editFloat = 125.0f;
|
||||
else if (_editFloat > 62.5f) _editFloat = 62.5f;
|
||||
else _editFloat = 31.25f;
|
||||
break;
|
||||
case ROW_SF: if (_editInt > 5) _editInt--; break;
|
||||
case ROW_CR: if (_editInt > 5) _editInt--; break;
|
||||
case ROW_TX_POWER: if (_editInt > 1) _editInt--; break;
|
||||
case ROW_UTC_OFFSET: if (_editInt > -12) _editInt--; break;
|
||||
default: break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (c == '\r' || c == 13) {
|
||||
// Confirm number edit
|
||||
switch (type) {
|
||||
case ROW_FREQ:
|
||||
_prefs->freq = constrain(_editFloat, 400.0f, 2500.0f);
|
||||
_radioChanged = true;
|
||||
break;
|
||||
case ROW_BW:
|
||||
_prefs->bw = _editFloat;
|
||||
_radioChanged = true;
|
||||
break;
|
||||
case ROW_SF:
|
||||
_prefs->sf = (uint8_t)constrain(_editInt, 5, 12);
|
||||
_radioChanged = true;
|
||||
break;
|
||||
case ROW_CR:
|
||||
_prefs->cr = (uint8_t)constrain(_editInt, 5, 8);
|
||||
_radioChanged = true;
|
||||
break;
|
||||
case ROW_TX_POWER:
|
||||
_prefs->tx_power_dbm = (uint8_t)constrain(_editInt, 1, MAX_LORA_TX_POWER);
|
||||
_radioChanged = true;
|
||||
break;
|
||||
case ROW_UTC_OFFSET:
|
||||
_prefs->utc_offset_hours = (int8_t)constrain(_editInt, -12, 14);
|
||||
the_mesh.savePrefs();
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
_editMode = EDIT_NONE;
|
||||
return true;
|
||||
}
|
||||
if (c == 'q' || c == 'Q') {
|
||||
_editMode = EDIT_NONE;
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Normal browsing mode ---
|
||||
|
||||
// W/S: navigate
|
||||
if (c == 'w' || c == 'W') {
|
||||
if (_cursor > 0) {
|
||||
_cursor--;
|
||||
skipNonSelectable(-1);
|
||||
}
|
||||
Serial.printf("Settings: cursor=%d/%d row=%d\n", _cursor, _numRows, _rows[_cursor].type);
|
||||
return true;
|
||||
}
|
||||
if (c == 's' || c == 'S') {
|
||||
if (_cursor < _numRows - 1) {
|
||||
_cursor++;
|
||||
skipNonSelectable(1);
|
||||
}
|
||||
Serial.printf("Settings: cursor=%d/%d row=%d\n", _cursor, _numRows, _rows[_cursor].type);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Enter: start editing the selected row
|
||||
if (c == '\r' || c == 13) {
|
||||
SettingsRowType type = _rows[_cursor].type;
|
||||
switch (type) {
|
||||
case ROW_NAME:
|
||||
startEditText(_prefs->node_name);
|
||||
break;
|
||||
case ROW_RADIO_PRESET:
|
||||
startEditPicker(max(0, detectCurrentPreset()));
|
||||
break;
|
||||
case ROW_FREQ:
|
||||
startEditFloat(_prefs->freq);
|
||||
break;
|
||||
case ROW_BW:
|
||||
startEditFloat(_prefs->bw);
|
||||
break;
|
||||
case ROW_SF:
|
||||
startEditInt(_prefs->sf);
|
||||
break;
|
||||
case ROW_CR:
|
||||
startEditInt(_prefs->cr);
|
||||
break;
|
||||
case ROW_TX_POWER:
|
||||
startEditInt(_prefs->tx_power_dbm);
|
||||
break;
|
||||
case ROW_UTC_OFFSET:
|
||||
startEditInt(_prefs->utc_offset_hours);
|
||||
break;
|
||||
case ROW_ADD_CHANNEL:
|
||||
startEditText("");
|
||||
break;
|
||||
case ROW_CHANNEL:
|
||||
case ROW_PUB_KEY:
|
||||
case ROW_FIRMWARE:
|
||||
// Not directly editable on Enter
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// X: delete channel (when on a channel row, idx > 0)
|
||||
if (c == 'x' || c == 'X') {
|
||||
if (_rows[_cursor].type == ROW_CHANNEL && _rows[_cursor].param > 0) {
|
||||
_editMode = EDIT_CONFIRM;
|
||||
_confirmAction = 1;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Q: back — if radio changed, prompt to apply first
|
||||
if (c == 'q' || c == 'Q') {
|
||||
if (_radioChanged) {
|
||||
_editMode = EDIT_CONFIRM;
|
||||
_confirmAction = 2;
|
||||
return true;
|
||||
}
|
||||
_onboarding = false;
|
||||
return false; // Let the caller handle navigation back
|
||||
}
|
||||
|
||||
return true; // Consume all other keys (don't let caller exit)
|
||||
}
|
||||
|
||||
// Override handleInput for UIScreen compatibility (used by injectKey)
|
||||
bool handleInput(char c) override {
|
||||
return handleKeyInput(c);
|
||||
}
|
||||
};
|
||||
@@ -33,6 +33,7 @@
|
||||
#include "ChannelScreen.h"
|
||||
#include "ContactsScreen.h"
|
||||
#include "TextReaderScreen.h"
|
||||
#include "SettingsScreen.h"
|
||||
|
||||
class SplashScreen : public UIScreen {
|
||||
UITask* _task;
|
||||
@@ -716,6 +717,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);
|
||||
settings_screen = new SettingsScreen(this, &rtc_clock, node_prefs);
|
||||
setCurrScreen(splash);
|
||||
}
|
||||
|
||||
@@ -1155,6 +1157,26 @@ void UITask::gotoTextReader() {
|
||||
_next_refresh = 100;
|
||||
}
|
||||
|
||||
void UITask::gotoSettingsScreen() {
|
||||
((SettingsScreen*)settings_screen)->enter();
|
||||
setCurrScreen(settings_screen);
|
||||
if (_display != NULL && !_display->isOn()) {
|
||||
_display->turnOn();
|
||||
}
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
_next_refresh = 100;
|
||||
}
|
||||
|
||||
void UITask::gotoOnboarding() {
|
||||
((SettingsScreen*)settings_screen)->enterOnboarding();
|
||||
setCurrScreen(settings_screen);
|
||||
if (_display != NULL && !_display->isOn()) {
|
||||
_display->turnOn();
|
||||
}
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
_next_refresh = 100;
|
||||
}
|
||||
|
||||
uint8_t UITask::getChannelScreenViewIdx() const {
|
||||
return ((ChannelScreen *) channel_screen)->getViewChannelIdx();
|
||||
}
|
||||
|
||||
@@ -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* settings_screen; // Settings/onboarding screen
|
||||
UIScreen* curr;
|
||||
|
||||
void userLedHandler();
|
||||
@@ -79,6 +80,8 @@ public:
|
||||
void gotoChannelScreen(); // Navigate to channel message screen
|
||||
void gotoContactsScreen(); // Navigate to contacts list
|
||||
void gotoTextReader(); // *** NEW: Navigate to text reader ***
|
||||
void gotoSettingsScreen(); // Navigate to settings
|
||||
void gotoOnboarding(); // Navigate to settings in onboarding mode
|
||||
void showAlert(const char* text, int duration_millis) override;
|
||||
void forceRefresh() override { _next_refresh = 100; }
|
||||
int getMsgCount() const { return _msgcount; }
|
||||
@@ -87,6 +90,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 isOnSettingsScreen() const { return curr == settings_screen; }
|
||||
uint8_t getChannelScreenViewIdx() const;
|
||||
|
||||
void toggleBuzzer();
|
||||
@@ -108,6 +112,7 @@ public:
|
||||
UIScreen* getTextReaderScreen() const { return text_reader; } // *** NEW ***
|
||||
UIScreen* getContactsScreen() const { return contacts_screen; }
|
||||
UIScreen* getChannelScreen() const { return channel_screen; }
|
||||
UIScreen* getSettingsScreen() const { return settings_screen; }
|
||||
|
||||
// from AbstractUITask
|
||||
void msgRead(int msgcount) override;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user