mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-27 15:28:11 +00:00
feat: NearbyScreen, configurable clock dashboard, memory optimizations
- Add NearbyScreen: GPS contact list with haversine distance sort, type filter (ALL/Chat/Rpt/Room/Snsr), detail view with ping (RTT via ACK poll), node scan via advert(), context menu - Add DashboardConfigScreen: configure up to 3 data fields on the clock page (Battery, Temp, Humidity, Pressure, GPS, Altitude, Lux, CO2, Contacts); entered via long-press ENTER on clock page - Rework HP_CLOCK layout: full-screen clock (size 2 at y=0), date at y=19, separator at y=28, three data field rows at y=31/41/51; hide node name/battery indicator on clock page - Persist dashboard_fields[3] in NodePrefs and DataStore - Add isAckPending() to MyMesh for NearbyScreen ping detection - Wire NearbyScreen into ToolsScreen and UITask - Reduce NearbyScreen entry buffer from MAX_CONTACTS(100) to 32, saving ~3.3 KB RAM - Switch haversine/bearing math from double to float (sinf/cosf/atan2f), eliminating double libm and saving ~5-10 KB flash Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -266,6 +266,9 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
|
||||
file.read((uint8_t *)&_prefs.buzzer_auto, sizeof(_prefs.buzzer_auto));
|
||||
if (file.available()) {
|
||||
file.read((uint8_t *)_prefs.dm_notif, sizeof(_prefs.dm_notif));
|
||||
if (file.available()) {
|
||||
file.read((uint8_t *)_prefs.dashboard_fields, sizeof(_prefs.dashboard_fields));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -338,6 +341,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
|
||||
file.write((uint8_t *)&_prefs.clock_hide_seconds, sizeof(_prefs.clock_hide_seconds));
|
||||
file.write((uint8_t *)&_prefs.buzzer_auto, sizeof(_prefs.buzzer_auto));
|
||||
file.write((uint8_t *)_prefs.dm_notif, sizeof(_prefs.dm_notif));
|
||||
file.write((uint8_t *)_prefs.dashboard_fields, sizeof(_prefs.dashboard_fields));
|
||||
|
||||
file.close();
|
||||
}
|
||||
|
||||
@@ -168,6 +168,12 @@ public:
|
||||
void savePrefs() { _store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon); }
|
||||
void saveRTCTime() { _store->saveRTCTime(); }
|
||||
|
||||
bool isAckPending(uint32_t expected_ack) const {
|
||||
for (int i = 0; i < EXPECTED_ACK_TABLE_SIZE; i++)
|
||||
if (expected_ack_table[i].ack == expected_ack) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
#if ENV_INCLUDE_GPS == 1
|
||||
void applyGpsPrefs() {
|
||||
sensors.setSettingValue("gps", _prefs.gps_enabled ? "1" : "0");
|
||||
|
||||
@@ -60,4 +60,5 @@ struct NodePrefs { // persisted to file
|
||||
struct DmNotifEntry { uint8_t prefix[4]; uint8_t state; }; // state: 0=default,1=muted,2=force-on
|
||||
static const int DM_NOTIF_TABLE_MAX = 16;
|
||||
DmNotifEntry dm_notif[DM_NOTIF_TABLE_MAX]; // 16*5 = 80 bytes
|
||||
uint8_t dashboard_fields[3]; // 0=None,1=Batt,2=Temp,3=Hum,4=Pres,5=GPS,6=Alt,7=Lux,8=CO2,9=Nodes
|
||||
};
|
||||
86
examples/companion_radio/ui-new/DashboardConfigScreen.h
Normal file
86
examples/companion_radio/ui-new/DashboardConfigScreen.h
Normal file
@@ -0,0 +1,86 @@
|
||||
#pragma once
|
||||
// Configures which data fields appear on the clock home page.
|
||||
// Included by UITask.cpp after BotScreen.h.
|
||||
|
||||
// Field type constants — used here and in UITask.cpp HP_CLOCK render.
|
||||
static const uint8_t DASH_NONE = 0;
|
||||
static const uint8_t DASH_BATT = 1;
|
||||
static const uint8_t DASH_TEMP = 2;
|
||||
static const uint8_t DASH_HUM = 3;
|
||||
static const uint8_t DASH_PRES = 4;
|
||||
static const uint8_t DASH_GPS = 5;
|
||||
static const uint8_t DASH_ALT = 6;
|
||||
static const uint8_t DASH_LUX = 7;
|
||||
static const uint8_t DASH_CO2 = 8;
|
||||
static const uint8_t DASH_NODES = 9;
|
||||
static const uint8_t DASH_COUNT = 10;
|
||||
|
||||
class DashboardConfigScreen : public UIScreen {
|
||||
UITask* _task;
|
||||
NodePrefs* _prefs;
|
||||
|
||||
static const int FIELD_SLOTS = 3;
|
||||
static const int ITEM_H = 13;
|
||||
static const int START_Y = 13;
|
||||
static const int VAL_X = 62;
|
||||
|
||||
static const char* OPTION_NAMES[DASH_COUNT];
|
||||
|
||||
int _sel;
|
||||
|
||||
void cycle(int slot, int dir) {
|
||||
uint8_t& f = _prefs->dashboard_fields[slot];
|
||||
f = (uint8_t)((f + DASH_COUNT + dir) % DASH_COUNT);
|
||||
}
|
||||
|
||||
public:
|
||||
DashboardConfigScreen(UITask* task, NodePrefs* prefs) : _task(task), _prefs(prefs) {}
|
||||
|
||||
void enter() { _sel = 0; }
|
||||
|
||||
int render(DisplayDriver& display) override {
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawTextCentered(display.width() / 2, 0, "CLOCK FIELDS");
|
||||
display.fillRect(0, 10, display.width(), 1);
|
||||
|
||||
static const char* labels[] = { "Field 1", "Field 2", "Field 3" };
|
||||
for (int i = 0; i < FIELD_SLOTS; i++) {
|
||||
int y = START_Y + i * ITEM_H;
|
||||
bool sel = (i == _sel);
|
||||
if (sel) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.fillRect(0, y - 1, display.width(), ITEM_H);
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
} else {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
}
|
||||
display.setCursor(2, y);
|
||||
display.print(labels[i]);
|
||||
display.setCursor(VAL_X, y);
|
||||
uint8_t f = _prefs->dashboard_fields[i];
|
||||
display.print(OPTION_NAMES[f < DASH_COUNT ? f : DASH_NONE]);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
}
|
||||
return 500;
|
||||
}
|
||||
|
||||
bool handleInput(char c) override {
|
||||
if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) {
|
||||
the_mesh.savePrefs();
|
||||
_task->gotoHomeScreen();
|
||||
return true;
|
||||
}
|
||||
if (c == KEY_UP && _sel > 0) { _sel--; return true; }
|
||||
if (c == KEY_DOWN && _sel < FIELD_SLOTS - 1){ _sel++; return true; }
|
||||
if (c == KEY_LEFT || c == KEY_PREV) { cycle(_sel, -1); return true; }
|
||||
if (c == KEY_RIGHT || c == KEY_NEXT) { cycle(_sel, 1); return true; }
|
||||
if (c == KEY_ENTER) { cycle(_sel, 1); return true; }
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const char* DashboardConfigScreen::OPTION_NAMES[DASH_COUNT] = {
|
||||
"None", "Battery", "Temp", "Humidity", "Pressure",
|
||||
"GPS", "Altitude", "Lux", "CO2", "Contacts"
|
||||
};
|
||||
353
examples/companion_radio/ui-new/NearbyScreen.h
Normal file
353
examples/companion_radio/ui-new/NearbyScreen.h
Normal file
@@ -0,0 +1,353 @@
|
||||
#pragma once
|
||||
#include <math.h>
|
||||
|
||||
#ifndef M_PI
|
||||
#define M_PI 3.14159265358979323846
|
||||
#endif
|
||||
|
||||
class NearbyScreen : public UIScreen {
|
||||
UITask* _task;
|
||||
|
||||
static const int VISIBLE = 4;
|
||||
static const int ITEM_H = 12;
|
||||
static const int START_Y = 12;
|
||||
static const int DIST_COL = 86;
|
||||
|
||||
static const int FILTER_COUNT = 5;
|
||||
static const char* FILTER_LABELS[FILTER_COUNT];
|
||||
static const uint8_t FILTER_TYPES[FILTER_COUNT];
|
||||
|
||||
struct Entry {
|
||||
char name[32];
|
||||
int32_t lat_e6, lon_e6;
|
||||
float dist_km;
|
||||
uint8_t type;
|
||||
int contact_idx;
|
||||
};
|
||||
|
||||
static const int MAX_NEARBY = 32;
|
||||
Entry _entries[MAX_NEARBY];
|
||||
int _count;
|
||||
int _sel;
|
||||
int _scroll;
|
||||
bool _detail;
|
||||
int32_t _own_lat, _own_lon;
|
||||
bool _own_gps;
|
||||
uint8_t _filter;
|
||||
|
||||
// scan state
|
||||
bool _scanning;
|
||||
unsigned long _scan_started_ms;
|
||||
static const unsigned long SCAN_DURATION_MS = 4000UL;
|
||||
|
||||
// context menu (list view)
|
||||
PopupMenu _ctx_menu;
|
||||
|
||||
// ping state (detail view)
|
||||
enum PingState { PING_IDLE, PING_WAIT, PING_OK, PING_TIMEOUT };
|
||||
PingState _ping_state;
|
||||
uint32_t _ping_expected_ack;
|
||||
unsigned long _ping_start_ms;
|
||||
unsigned long _ping_rtt_ms;
|
||||
static const unsigned long PING_TIMEOUT_MS = 30000UL;
|
||||
|
||||
static float haversineKm(int32_t lat1, int32_t lon1, int32_t lat2, int32_t lon2) {
|
||||
static const float DEG2RAD = (float)M_PI / 180.0f;
|
||||
float la1 = lat1 * (1e-6f * DEG2RAD);
|
||||
float la2 = lat2 * (1e-6f * DEG2RAD);
|
||||
float dla = (lat2 - lat1) * (1e-6f * DEG2RAD);
|
||||
float dlo = (lon2 - lon1) * (1e-6f * DEG2RAD);
|
||||
float a = sinf(dla/2)*sinf(dla/2) + cosf(la1)*cosf(la2)*sinf(dlo/2)*sinf(dlo/2);
|
||||
return 6371.0f * 2.0f * asinf(sqrtf(a));
|
||||
}
|
||||
|
||||
static int bearingDeg(int32_t lat1, int32_t lon1, int32_t lat2, int32_t lon2) {
|
||||
static const float DEG2RAD = (float)M_PI / 180.0f;
|
||||
float la1 = lat1 * (1e-6f * DEG2RAD);
|
||||
float la2 = lat2 * (1e-6f * DEG2RAD);
|
||||
float dlo = (lon2 - lon1) * (1e-6f * DEG2RAD);
|
||||
float b = atan2f(sinf(dlo)*cosf(la2),
|
||||
cosf(la1)*sinf(la2) - sinf(la1)*cosf(la2)*cosf(dlo)) * (180.0f / (float)M_PI);
|
||||
if (b < 0.0f) b += 360.0f;
|
||||
return (int)(b + 0.5f) % 360;
|
||||
}
|
||||
|
||||
static void fmtDist(char* buf, int n, float km) {
|
||||
if (km < 1.0f) snprintf(buf, n, "%dm", (int)(km * 1000 + 0.5f));
|
||||
else if (km < 100.0f) snprintf(buf, n, "%.1fkm", km);
|
||||
else snprintf(buf, n, "%dkm", (int)(km + 0.5f));
|
||||
}
|
||||
|
||||
static const char* typeName(uint8_t t) {
|
||||
switch (t) {
|
||||
case ADV_TYPE_CHAT: return "Chat";
|
||||
case ADV_TYPE_REPEATER: return "Repeater";
|
||||
case ADV_TYPE_ROOM: return "Room";
|
||||
case ADV_TYPE_SENSOR: return "Sensor";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
void startScan() {
|
||||
the_mesh.advert();
|
||||
_scanning = true;
|
||||
_scan_started_ms = millis();
|
||||
}
|
||||
|
||||
void refresh() {
|
||||
_count = 0;
|
||||
_own_gps = false;
|
||||
_own_lat = _own_lon = 0;
|
||||
|
||||
#if ENV_INCLUDE_GPS == 1
|
||||
LocationProvider* loc = sensors.getLocationProvider();
|
||||
if (loc && loc->isValid()) {
|
||||
_own_lat = loc->getLatitude();
|
||||
_own_lon = loc->getLongitude();
|
||||
_own_gps = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
int nc = the_mesh.getNumContacts();
|
||||
for (int i = 0; i < nc && _count < MAX_NEARBY; i++) {
|
||||
ContactInfo ci;
|
||||
if (!the_mesh.getContactByIdx(i, ci)) continue;
|
||||
if (ci.gps_lat == 0 && ci.gps_lon == 0) continue;
|
||||
if (_filter > 0 && ci.type != FILTER_TYPES[_filter]) continue;
|
||||
|
||||
Entry& e = _entries[_count++];
|
||||
strncpy(e.name, ci.name, sizeof(e.name) - 1);
|
||||
e.name[sizeof(e.name) - 1] = '\0';
|
||||
e.lat_e6 = ci.gps_lat;
|
||||
e.lon_e6 = ci.gps_lon;
|
||||
e.dist_km = _own_gps ? haversineKm(_own_lat, _own_lon, ci.gps_lat, ci.gps_lon) : -1.0f;
|
||||
e.type = ci.type;
|
||||
e.contact_idx = i;
|
||||
}
|
||||
|
||||
if (_own_gps) {
|
||||
for (int i = 0; i < _count - 1; i++) {
|
||||
int best = i;
|
||||
for (int j = i + 1; j < _count; j++)
|
||||
if (_entries[j].dist_km < _entries[best].dist_km) best = j;
|
||||
if (best != i) { Entry tmp = _entries[i]; _entries[i] = _entries[best]; _entries[best] = tmp; }
|
||||
}
|
||||
}
|
||||
|
||||
if (_count == 0) {
|
||||
_sel = _scroll = 0;
|
||||
} else if (_sel >= _count) {
|
||||
_sel = _count - 1;
|
||||
if (_scroll > _sel) _scroll = _sel;
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
NearbyScreen(UITask* task)
|
||||
: _task(task), _filter(0), _scanning(false), _ping_state(PING_IDLE) {}
|
||||
|
||||
void enter() {
|
||||
_sel = _scroll = 0;
|
||||
_detail = false;
|
||||
_filter = 0;
|
||||
_scanning = false;
|
||||
_ctx_menu.active = false;
|
||||
_ping_state = PING_IDLE;
|
||||
refresh();
|
||||
}
|
||||
|
||||
int render(DisplayDriver& display) override {
|
||||
display.setTextSize(1);
|
||||
|
||||
// poll scan timer — only refresh list when not in detail view
|
||||
if (_scanning && !_detail && millis() - _scan_started_ms >= SCAN_DURATION_MS) {
|
||||
_scanning = false;
|
||||
refresh();
|
||||
}
|
||||
|
||||
// poll ping ack
|
||||
if (_ping_state == PING_WAIT) {
|
||||
if (!the_mesh.isAckPending(_ping_expected_ack)) {
|
||||
_ping_rtt_ms = millis() - _ping_start_ms;
|
||||
_ping_state = PING_OK;
|
||||
} else if (millis() - _ping_start_ms >= PING_TIMEOUT_MS) {
|
||||
_ping_state = PING_TIMEOUT;
|
||||
}
|
||||
}
|
||||
|
||||
// ── detail view ──────────────────────────────────────────────────────────
|
||||
if (_detail && _sel < _count) {
|
||||
const Entry& e = _entries[_sel];
|
||||
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.fillRect(0, 0, display.width(), 10);
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
char filtered[32];
|
||||
display.translateUTF8ToBlocks(filtered, e.name, sizeof(filtered));
|
||||
display.drawTextEllipsized(2, 1, display.width() - 4, filtered);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "Lat: %.5f", e.lat_e6 / 1e6);
|
||||
display.setCursor(2, 13); display.print(buf);
|
||||
|
||||
snprintf(buf, sizeof(buf), "Lon: %.5f", e.lon_e6 / 1e6);
|
||||
display.setCursor(2, 22); display.print(buf);
|
||||
|
||||
if (e.dist_km >= 0.0f) {
|
||||
char dist[12];
|
||||
fmtDist(dist, sizeof(dist), e.dist_km);
|
||||
snprintf(buf, sizeof(buf), "Dist:%s %d\xb0", dist, bearingDeg(_own_lat, _own_lon, e.lat_e6, e.lon_e6));
|
||||
display.setCursor(2, 31); display.print(buf);
|
||||
} else {
|
||||
display.setCursor(2, 31); display.print("Dist: no own GPS");
|
||||
}
|
||||
|
||||
display.setCursor(2, 40);
|
||||
switch (_ping_state) {
|
||||
case PING_IDLE: display.print("[OK]=Ping"); break;
|
||||
case PING_WAIT: display.print("Pinging..."); break;
|
||||
case PING_OK: snprintf(buf,sizeof(buf),"RTT:%lums",_ping_rtt_ms);
|
||||
display.print(buf); break;
|
||||
case PING_TIMEOUT: display.print("Ping timeout"); break;
|
||||
}
|
||||
|
||||
snprintf(buf, sizeof(buf), "Type:%s", typeName(e.type));
|
||||
display.setCursor(2, 49); display.print(buf);
|
||||
|
||||
return (_ping_state == PING_WAIT) ? 100 : 2000;
|
||||
}
|
||||
|
||||
// ── list view ────────────────────────────────────────────────────────────
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
if (_scanning) {
|
||||
display.drawTextCentered(display.width() / 2, 0, "SCANNING...");
|
||||
} else {
|
||||
char title[22];
|
||||
snprintf(title, sizeof(title), "NEARBY[%s]", FILTER_LABELS[_filter]);
|
||||
display.drawTextCentered(display.width() / 2, 0, title);
|
||||
}
|
||||
display.fillRect(0, 10, display.width(), 1);
|
||||
|
||||
if (_count == 0) {
|
||||
display.drawTextCentered(display.width() / 2, 28, _scanning ? "Waiting..." : "No GPS contacts");
|
||||
display.drawTextCentered(display.width() / 2, 40, "[M]=Scan");
|
||||
} else {
|
||||
for (int i = 0; i < VISIBLE && (_scroll + i) < _count; i++) {
|
||||
int idx = _scroll + i;
|
||||
bool sel = (idx == _sel);
|
||||
int y = START_Y + i * ITEM_H;
|
||||
const Entry& e = _entries[idx];
|
||||
|
||||
if (sel) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.fillRect(0, y - 1, display.width(), ITEM_H - 1);
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
} else {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
}
|
||||
|
||||
char filt[32];
|
||||
display.translateUTF8ToBlocks(filt, e.name, sizeof(filt));
|
||||
display.drawTextEllipsized(2, y, DIST_COL - 4, filt);
|
||||
|
||||
display.setColor(sel ? DisplayDriver::DARK : DisplayDriver::LIGHT);
|
||||
char dist[10];
|
||||
if (e.dist_km >= 0.0f) fmtDist(dist, sizeof(dist), e.dist_km);
|
||||
else strncpy(dist, "?GPS", sizeof(dist));
|
||||
display.setCursor(DIST_COL, y);
|
||||
display.print(dist);
|
||||
}
|
||||
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
if (_scroll > 0)
|
||||
{ display.setCursor(display.width() - 6, START_Y); display.print("^"); }
|
||||
if (_scroll + VISIBLE < _count)
|
||||
{ display.setCursor(display.width() - 6, START_Y + (VISIBLE - 1) * ITEM_H); display.print("v"); }
|
||||
}
|
||||
|
||||
// context menu overlay — drawn on top regardless of list state
|
||||
if (_ctx_menu.active) {
|
||||
_ctx_menu.render(display);
|
||||
return 50;
|
||||
}
|
||||
|
||||
return _scanning ? 100 : (_count == 0 ? 3000 : 2000);
|
||||
}
|
||||
|
||||
bool handleInput(char c) override {
|
||||
// ── detail view input ────────────────────────────────────────────────────
|
||||
if (_detail) {
|
||||
if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) {
|
||||
_detail = false;
|
||||
_ping_state = PING_IDLE;
|
||||
return true;
|
||||
}
|
||||
if (c == KEY_ENTER && _ping_state == PING_IDLE) {
|
||||
ContactInfo ci;
|
||||
if (the_mesh.getContactByIdx(_entries[_sel].contact_idx, ci)) {
|
||||
uint32_t expected_ack, est_timeout;
|
||||
int res = the_mesh.sendMessage(ci, rtc_clock.getCurrentTime(), 0, "", expected_ack, est_timeout);
|
||||
if (res != MSG_SEND_FAILED && expected_ack != 0) {
|
||||
_ping_expected_ack = expected_ack;
|
||||
_ping_start_ms = millis();
|
||||
_ping_state = PING_WAIT;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── list view — context menu ─────────────────────────────────────────────
|
||||
if (_ctx_menu.active) {
|
||||
auto res = _ctx_menu.handleInput(c);
|
||||
if (res == PopupMenu::SELECTED) {
|
||||
if (_ctx_menu.selectedIndex() == 0) {
|
||||
startScan();
|
||||
} else {
|
||||
_task->gotoToolsScreen();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── list view — normal input ─────────────────────────────────────────────
|
||||
if (c == KEY_CANCEL) { _task->gotoToolsScreen(); return true; }
|
||||
if (c == KEY_CONTEXT_MENU) {
|
||||
_ctx_menu.begin("Options", 2);
|
||||
_ctx_menu.addItem("Scan for nodes");
|
||||
_ctx_menu.addItem("Back");
|
||||
return true;
|
||||
}
|
||||
if (c == KEY_UP && _sel > 0) {
|
||||
_sel--;
|
||||
if (_sel < _scroll) _scroll = _sel;
|
||||
return true;
|
||||
}
|
||||
if (c == KEY_DOWN && _sel < _count - 1) {
|
||||
_sel++;
|
||||
if (_sel >= _scroll + VISIBLE) _scroll = _sel - VISIBLE + 1;
|
||||
return true;
|
||||
}
|
||||
if (c == KEY_ENTER && _count > 0) {
|
||||
_detail = true;
|
||||
_ping_state = PING_IDLE;
|
||||
return true;
|
||||
}
|
||||
if (c == KEY_LEFT) {
|
||||
_filter = (_filter + FILTER_COUNT - 1) % FILTER_COUNT;
|
||||
refresh();
|
||||
return true;
|
||||
}
|
||||
if (c == KEY_RIGHT) {
|
||||
_filter = (_filter + 1) % FILTER_COUNT;
|
||||
refresh();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const char* NearbyScreen::FILTER_LABELS[5] = { "ALL", "Chat", "Rpt", "Room", "Snsr" };
|
||||
const uint8_t NearbyScreen::FILTER_TYPES[5] = { 0, ADV_TYPE_CHAT, ADV_TYPE_REPEATER, ADV_TYPE_ROOM, ADV_TYPE_SENSOR };
|
||||
@@ -6,7 +6,7 @@ class ToolsScreen : public UIScreen {
|
||||
UITask* _task;
|
||||
int _sel;
|
||||
|
||||
static const int ITEM_COUNT = 2;
|
||||
static const int ITEM_COUNT = 3;
|
||||
static const char* ITEMS[ITEM_COUNT];
|
||||
|
||||
public:
|
||||
@@ -44,8 +44,9 @@ public:
|
||||
if (c == KEY_ENTER) {
|
||||
if (_sel == 0) { _task->gotoRingtoneEditor(); return true; }
|
||||
if (_sel == 1) { _task->gotoBotScreen(); return true; }
|
||||
if (_sel == 2) { _task->gotoNearbyScreen(); return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
const char* ToolsScreen::ITEMS[2] = { "Ringtone Editor", "Auto-Reply Bot" };
|
||||
const char* ToolsScreen::ITEMS[3] = { "Ringtone Editor", "Auto-Reply Bot", "Nearby Nodes" };
|
||||
|
||||
@@ -1592,6 +1592,8 @@ public:
|
||||
// ── Custom screens (separate files to ease upstream merges) ───────────────────
|
||||
#include "RingtoneEditorScreen.h"
|
||||
#include "BotScreen.h"
|
||||
#include "NearbyScreen.h"
|
||||
#include "DashboardConfigScreen.h"
|
||||
#include "ToolsScreen.h"
|
||||
|
||||
// ── HomeScreen ────────────────────────────────────────────────────────────────
|
||||
@@ -1787,16 +1789,16 @@ public:
|
||||
|
||||
int render(DisplayDriver& display) override {
|
||||
char tmp[80];
|
||||
// node name
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
char filtered_name[sizeof(_node_prefs->node_name)];
|
||||
display.translateUTF8ToBlocks(filtered_name, _node_prefs->node_name, sizeof(filtered_name));
|
||||
display.setCursor(0, 0);
|
||||
display.print(filtered_name);
|
||||
|
||||
// battery voltage
|
||||
renderBatteryIndicator(display, _task->getBattMilliVolts());
|
||||
// node name + battery — hidden on CLOCK page (full screen used for dashboard)
|
||||
if (_page != CLOCK) {
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
char filtered_name[sizeof(_node_prefs->node_name)];
|
||||
display.translateUTF8ToBlocks(filtered_name, _node_prefs->node_name, sizeof(filtered_name));
|
||||
display.setCursor(0, 0);
|
||||
display.print(filtered_name);
|
||||
renderBatteryIndicator(display, _task->getBattMilliVolts());
|
||||
}
|
||||
|
||||
// ensure current page is visible (e.g. after settings change)
|
||||
if (!isPageVisible(_page)) _page = navPage(_page, +1);
|
||||
@@ -1842,17 +1844,90 @@ public:
|
||||
sprintf(buf, "%02d:%02d:%02d", ti->tm_hour, ti->tm_min, ti->tm_sec);
|
||||
else
|
||||
sprintf(buf, "%02d:%02d", ti->tm_hour, ti->tm_min);
|
||||
display.drawTextCentered(display.width() / 2, 18, buf);
|
||||
display.drawTextCentered(display.width() / 2, 0, buf);
|
||||
|
||||
display.setTextSize(1);
|
||||
const char* wd[] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
|
||||
const char* mo[] = {"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"};
|
||||
static const char* wd[] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
|
||||
static const char* mo[] = {"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"};
|
||||
sprintf(buf, "%s %d %s %d", wd[ti->tm_wday], ti->tm_mday, mo[ti->tm_mon], 1900 + ti->tm_year);
|
||||
display.drawTextCentered(display.width() / 2, 40, buf);
|
||||
display.drawTextCentered(display.width() / 2, 19, buf);
|
||||
|
||||
if (tz >= 0) sprintf(buf, "UTC+%d", (int)tz);
|
||||
else sprintf(buf, "UTC%d", (int)tz);
|
||||
display.drawTextCentered(display.width() / 2, 54, buf);
|
||||
display.fillRect(0, 28, display.width(), 1);
|
||||
|
||||
// dashboard data fields
|
||||
if (_node_prefs) {
|
||||
refresh_sensors();
|
||||
static const int FIELD_Y[] = { 31, 41, 51 };
|
||||
for (int fi = 0; fi < 3; fi++) {
|
||||
uint8_t field = _node_prefs->dashboard_fields[fi];
|
||||
if (field == DASH_NONE) continue;
|
||||
|
||||
char label[10], val[20];
|
||||
label[0] = '\0';
|
||||
val[0] = '\0';
|
||||
|
||||
if (field == DASH_BATT) {
|
||||
strcpy(label, "Batt");
|
||||
uint16_t mv = _task->getBattMilliVolts();
|
||||
if (mv > 0) snprintf(val, sizeof(val), "%u.%02uV", mv/1000, (mv%1000)/10);
|
||||
else strcpy(val, "--");
|
||||
} else if (field == DASH_GPS) {
|
||||
strcpy(label, "GPS");
|
||||
#if ENV_INCLUDE_GPS == 1
|
||||
LocationProvider* loc = sensors.getLocationProvider();
|
||||
if (loc && loc->isValid())
|
||||
snprintf(val, sizeof(val), "%.3f %.3f",
|
||||
loc->getLatitude()/1000000.0f, loc->getLongitude()/1000000.0f);
|
||||
else
|
||||
strcpy(val, "no fix");
|
||||
#else
|
||||
strcpy(val, "--");
|
||||
#endif
|
||||
} else if (field == DASH_NODES) {
|
||||
strcpy(label, "Nodes");
|
||||
snprintf(val, sizeof(val), "%d", the_mesh.getNumContacts());
|
||||
} else {
|
||||
uint8_t lpp_type = 0;
|
||||
switch (field) {
|
||||
case DASH_TEMP: strcpy(label, "Temp"); lpp_type = LPP_TEMPERATURE; break;
|
||||
case DASH_HUM: strcpy(label, "Hum"); lpp_type = LPP_RELATIVE_HUMIDITY; break;
|
||||
case DASH_PRES: strcpy(label, "Pres"); lpp_type = LPP_BAROMETRIC_PRESSURE; break;
|
||||
case DASH_ALT: strcpy(label, "Alt"); lpp_type = LPP_ALTITUDE; break;
|
||||
case DASH_LUX: strcpy(label, "Lux"); lpp_type = LPP_LUMINOSITY; break;
|
||||
case DASH_CO2: strcpy(label, "CO2"); lpp_type = LPP_CONCENTRATION; break;
|
||||
}
|
||||
if (lpp_type) {
|
||||
LPPReader r(sensors_lpp.getBuffer(), sensors_lpp.getSize());
|
||||
uint8_t ch, type;
|
||||
while (r.readHeader(ch, type)) {
|
||||
if (type == lpp_type) {
|
||||
float v;
|
||||
switch (lpp_type) {
|
||||
case LPP_TEMPERATURE: r.readTemperature(v); snprintf(val, sizeof(val), "%.1f\xf8""C", v); break;
|
||||
case LPP_RELATIVE_HUMIDITY: r.readRelativeHumidity(v); snprintf(val, sizeof(val), "%.0f%%", v); break;
|
||||
case LPP_BAROMETRIC_PRESSURE: r.readPressure(v); snprintf(val, sizeof(val), "%.0fhPa", v); break;
|
||||
case LPP_ALTITUDE: r.readAltitude(v); snprintf(val, sizeof(val), "%.0fm", v); break;
|
||||
case LPP_LUMINOSITY: r.readLuminosity(v); snprintf(val, sizeof(val), "%.0flux", v); break;
|
||||
case LPP_CONCENTRATION: r.readConcentration(v); snprintf(val, sizeof(val), "%.0fppm", v); break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
r.skipData(type);
|
||||
}
|
||||
}
|
||||
if (!val[0]) strcpy(val, "--");
|
||||
}
|
||||
|
||||
if (val[0] && label[0]) {
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setCursor(0, FIELD_Y[fi]);
|
||||
display.print(label);
|
||||
int vw = display.getTextWidth(val);
|
||||
display.setCursor(display.width() - vw - 1, FIELD_Y[fi]);
|
||||
display.print(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (_page == HomePage::RECENT) {
|
||||
the_mesh.getRecentlyHeard(recent, UI_RECENT_LIST_SIZE);
|
||||
@@ -2122,6 +2197,10 @@ public:
|
||||
_shutdown_init = true; // need to wait for button to be released
|
||||
return true;
|
||||
}
|
||||
if (c == KEY_CONTEXT_MENU && _page == HomePage::CLOCK) {
|
||||
_task->gotoDashboardConfig();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -2171,6 +2250,8 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
||||
tools_screen = new ToolsScreen(this);
|
||||
ringtone_edit = new RingtoneEditorScreen(this, node_prefs);
|
||||
bot_screen = new BotScreen(this, node_prefs);
|
||||
nearby_screen = new NearbyScreen(this);
|
||||
dashboard_config = new DashboardConfigScreen(this, node_prefs);
|
||||
setCurrScreen(splash);
|
||||
|
||||
applyBrightness();
|
||||
@@ -2195,6 +2276,16 @@ void UITask::gotoBotScreen() {
|
||||
setCurrScreen(bot_screen);
|
||||
}
|
||||
|
||||
void UITask::gotoNearbyScreen() {
|
||||
((NearbyScreen*)nearby_screen)->enter();
|
||||
setCurrScreen(nearby_screen);
|
||||
}
|
||||
|
||||
void UITask::gotoDashboardConfig() {
|
||||
((DashboardConfigScreen*)dashboard_config)->enter();
|
||||
setCurrScreen(dashboard_config);
|
||||
}
|
||||
|
||||
void UITask::playMelody(const char* melody) {
|
||||
#ifdef PIN_BUZZER
|
||||
buzzer.playForced(melody);
|
||||
|
||||
@@ -63,6 +63,8 @@ class UITask : public AbstractUITask {
|
||||
UIScreen* tools_screen;
|
||||
UIScreen* ringtone_edit;
|
||||
UIScreen* bot_screen;
|
||||
UIScreen* nearby_screen;
|
||||
UIScreen* dashboard_config;
|
||||
UIScreen* curr;
|
||||
|
||||
void userLedHandler();
|
||||
@@ -98,6 +100,8 @@ public:
|
||||
void gotoToolsScreen();
|
||||
void gotoRingtoneEditor();
|
||||
void gotoBotScreen();
|
||||
void gotoNearbyScreen();
|
||||
void gotoDashboardConfig();
|
||||
void playMelody(const char* melody);
|
||||
void stopMelody();
|
||||
bool isMelodyPlaying();
|
||||
|
||||
Reference in New Issue
Block a user