feat(companion): core parity -- L1 sends via the core, shared telemetry, schema settings on L1

- L1's MessagesScreen sends through UiCore::sendDirectText / sendChannelText
  (its own copy of send + file + ACK tracking gone).
- ui-core/Telemetry.h: the dashboard fields (numbers, names, formats) and
  sensor readings as text, each frontend passing its Style; L2 Home, L1's
  clock page, lock screen, sensors page and GPS altitude use it.
- L2: the side button held in the first 8 s is the CLI rescue, as on L1.
- L1's repeater radio goes through rptctl.
- L1 Settings are built from SettingsSchema: short labels for small
  screens, SCHEMA_* placeholders expand to the schema's sections, one
  generic row (switch ON/OFF, bars for brightness / volume). 20 hand-written
  rows and their tables gone; L1 implements applyDisplayPrefs /
  applySoundPrefs. settings::text / step / indexOf shared with L2.
- Schema: auto-off gains 5 s; the advert sound scope reads "Direct".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-09-28 23:19:47 +02:00
co-authored by Claude Opus 5.5
parent 0a669e1fd7
commit b355922d32
12 changed files with 375 additions and 686 deletions
+9 -1
View File
@@ -89,8 +89,16 @@ for a second radio). They are L2-specific and block the loop for ~90 s.
## G. Core parity with L1 SOLO
- [ ] A feature table: L1 SOLO / ui-core / L2 (ui-lvgl). What's missing goes
- [x] A feature table: L1 SOLO / ui-core / L2 (ui-lvgl). What's missing goes
into ui-core, not into each UI separately.
Done 2026-09-28: L1 sends through UiCore::sendDirectText /
sendChannelText; ui-core/Telemetry.h (dashboard fields, sensor
readings) for both; CLI rescue on L2 (side button held in the first
8 s); L1's repeater radio through rptctl; L1 Settings built from
SettingsSchema (short labels, SCHEMA_* placeholders per section; 20
hand-written rows gone). Kept per device: Noise measure (L2), screen
PIN (L2, a PR pending). Later, as settings are touched: radio switches,
keyboard alphabets and chat filters into the schema.
## H. Before the merge into dev
@@ -104,7 +104,7 @@ static void fmtSecs(char* b, int n, int32_t s, const char* zero) {
else if (s < 3600) snprintf(b, n, "%ld min", (long)(s / 60));
else snprintf(b, n, "%ld h", (long)(s / 3600));
}
static const int32_t AUTO_OFF[] = { 15, 30, 60, 120, 300, 0 };
static const int32_t AUTO_OFF[] = { 5, 15, 30, 60, 120, 300, 0 };
static void optAutoOff(uint8_t v, char* b, int n, const NodePrefs&) { fmtSecs(b, n, AUTO_OFF[v], "Never"); }
static const int32_t GPS_DUTY[] = { 0, 60, 300, 900, 1800, 3600 };
static void optGpsDuty(uint8_t v, char* b, int n, const NodePrefs&) { fmtSecs(b, n, GPS_DUTY[v], "Always on"); }
@@ -142,7 +142,7 @@ static void optHour(uint8_t v, char* b, int n, const NodePrefs& p) { // "22:00
}
static void optSound(uint8_t v, char* b, int n, const NodePrefs&) { snprintf(b, n, "%s", soundctl::soundLabel(v)); }
static void optAdvertScope(uint8_t v, char* b, int n, const NodePrefs&) {
snprintf(b, n, "%s", v == ADVERT_SOUND_SCOPE_ZERO_HOP ? "Direct only" : "All");
snprintf(b, n, "%s", v == ADVERT_SOUND_SCOPE_ZERO_HOP ? "Direct" : "All");
}
// ── Side effects ──────────────────────────────────────────────────────────────
@@ -222,13 +222,40 @@ static const Setting ALL[] = {
IDX("Adverts", "A node announcing itself", SEC_SOUND_FOR, notif_melody_ad, soundctl::SOUND_COUNT, optSound, nullptr),
IDX("Advert sound for", "Direct: no repeaters", SEC_SOUND_FOR, advert_sound_scope, 2, optAdvertScope, nullptr),
};
static const int COUNT = (int)(sizeof(ALL) / sizeof(ALL[0]));
// The label on a small screen (ui-new: about ten characters beside an
// eight-character value); a setting not listed uses its own.
static const struct { uint16_t offset; const char* text; } SHORT_LABELS[] = {
{ NP_OFF(display_brightness), "Bright" }, { NP_OFF(auto_off_secs), "Auto off" },
{ NP_OFF(msg_wake_screen_off), "Msg wake" }, { NP_OFF(auto_lock), "Auto lock" },
{ NP_OFF(batt_display_mode), "Batt disp" }, { NP_OFF(low_batt_mv), "Low batt" },
{ NP_OFF(gps_interval), "GPS pwr" }, { NP_OFF(clock_12h), "12h clock" },
{ NP_OFF(clock_hide_seconds), "Seconds" }, { NP_OFF(units_imperial), "Imperial" },
{ NP_OFF(dm_resend_count), "Resend" }, { NP_OFF(contact_expiry_idx), "Expire" },
{ NP_OFF(fav_sort_off), "Favs top" }, { NP_OFF(buzzer_volume), "Buzzer vol" },
{ NP_OFF(quiet_hours), "Quiet hrs" }, { NP_OFF(quiet_from), " from" },
{ NP_OFF(quiet_to), " until" }, { NP_OFF(notif_melody_dm), "DM sound" },
{ NP_OFF(notif_melody_ch), "Ch sound" }, { NP_OFF(notif_melody_ad), "AD sound" },
{ NP_OFF(advert_sound_scope), "AD scope" },
};
static const char* shortLabel(const Setting& s) {
for (auto& l : SHORT_LABELS) if (l.offset == s.offset) return l.text;
return s.label;
}
// A switch: two values, no labels of its own (on / off, however it's stored).
static bool isSwitch(const Setting& s) { return !s.option && s.count == 2; }
// The index of the setting over NodePrefs field `offset`; -1 if none.
static int indexOf(uint16_t offset) {
for (int i = 0; i < COUNT; i++) if (ALL[i].offset == offset) return i;
return -1;
}
#undef IDX
#undef SW
#undef MAP
#undef COUNT_OF
#undef NP_SIZE
#undef NP_OFF
static const int COUNT = (int)(sizeof(ALL) / sizeof(ALL[0]));
static int32_t readRaw(const NodePrefs& p, const Setting& s) {
const uint8_t* f = (const uint8_t*)&p + s.offset;
@@ -262,5 +289,18 @@ static void set(NodePrefs& p, const Setting& s, uint8_t v) {
if (v >= s.count) return;
writeRaw(p, s, s.values ? s.values[v] : v);
}
// The current value as text ("UTC+2", "3.4 V"); a switch as `on` / `off`.
static const char* text(const NodePrefs& p, const Setting& s, char* buf, int n,
const char* on = "On", const char* off = "Off") {
uint8_t v = get(p, s);
if (s.option) s.option(v, buf, n, p);
else snprintf(buf, n, "%s", v ? on : off);
return buf;
}
// One step along the values, wrapping; then the setting's side effect.
static void step(NodePrefs& p, const Setting& s, int dir, UiCore& core) {
set(p, s, (uint8_t)((get(p, s) + s.count + dir) % s.count));
if (s.changed) s.changed(core);
}
} // namespace settings
@@ -0,0 +1,138 @@
#pragma once
// Telemetry as text: the fields a frontend's clock page shows
// (NodePrefs::dashboard_fields -- Home on L2, the clock page and lock screen
// on L1) and any sensor reading (L1's sensors page). One set of numbers,
// names and formats; each frontend passes its own Style (the degree glyph in
// its font, how much room it has).
#include <stdint.h>
#include <stdio.h>
#include <helpers/sensors/LPPDataHelpers.h>
#include <helpers/sensors/LocationProvider.h>
#include "Battery.h"
namespace telemetry {
// Stored in NodePrefs::dashboard_fields: append only.
enum Field : uint8_t { NONE, BATT_V, TEMP, HUM, PRES, GPS, ALT, LUX, CO2, NODES, MSGS,
BATT_PCT, SATS, ALT_GPS, COUNT };
static const char* const NAME[COUNT] = {
"None", "Battery (V)", "Temperature", "Humidity", "Pressure", "Position", "Altitude (sensor)",
"Light", "CO2", "Contacts", "Unread", "Battery (%)", "Satellites", "Altitude (GPS)",
};
// The same, for a narrow menu column (L1).
static const char* const COMPACT[COUNT] = {
"None", "Batt V", "Temp", "Humidity", "Pressure", "GPS", "Altitude (Baro)", "Lux", "CO2", "Contacts",
"Messages", "Batt %", "Sats", "Altitude (GPS)",
};
// A label beside the value, where room is short.
static const char* const LABEL[COUNT] = {
"", "Batt", "Temp", "Hum", "Pres", "GPS", "Alt", "Lux", "CO2", "Nodes", "Msgs", "Batt", "Sats", "AltG",
};
struct Style {
const char* deg; // the degree sign in the display's font
bool spaced; // "12 m" rather than "12m"
bool nouns; // counts say what they count ("3 nodes"): a value without its label
uint8_t gps_dp; // decimals of a position
};
// What the values are read from; the frontend fills in what it has.
struct Inputs {
uint16_t batt_mv = 0, low_batt_mv = 0;
int nodes = 0;
int unread = 0;
bool unread_more = false; // some unread already gone from the rings: "3+"
bool imperial = false;
LocationProvider* loc = nullptr; // nullptr: no GPS
bool gps_on = true;
const uint8_t* lpp = nullptr; // sensors on the bus, as one querySensors() left them
uint8_t lpp_len = 0;
};
static uint8_t lppType(uint8_t f) {
switch (f) {
case TEMP: return LPP_TEMPERATURE;
case HUM: return LPP_RELATIVE_HUMIDITY;
case PRES: return LPP_BAROMETRIC_PRESSURE;
case ALT: return LPP_ALTITUDE;
case LUX: return LPP_LUMINOSITY;
case CO2: return LPP_CONCENTRATION;
}
return 0;
}
// Read from the sensors, not the radio or GPS.
static bool isSensor(uint8_t f) { return lppType(f) != 0; }
// Altitude always in the small unit (never km / mi).
static void altText(float m, bool imperial, const Style& st, char* v, int n) {
snprintf(v, n, "%.0f%s%s", imperial ? m * 3.28084f : m, st.spaced ? " " : "", imperial ? "ft" : "m");
}
// The reading `r` is at (its header read) as text; false, and the data
// skipped, for a type that isn't shown.
static bool lppText(LPPReader& r, uint8_t type, bool imperial, const Style& st, char* v, int n) {
const char* sp = st.spaced ? " " : "";
float x, y, z;
switch (type) {
case LPP_TEMPERATURE:
r.readTemperature(x);
snprintf(v, n, "%.1f%s%s%s", imperial ? x * 9 / 5 + 32 : x, sp, st.deg, imperial ? "F" : "C");
return true;
case LPP_RELATIVE_HUMIDITY: r.readRelativeHumidity(x); snprintf(v, n, "%.0f%%", x); return true;
case LPP_BAROMETRIC_PRESSURE: r.readPressure(x); snprintf(v, n, "%.0f%shPa", x, sp); return true;
case LPP_ALTITUDE: r.readAltitude(x); altText(x, imperial, st, v, n); return true;
case LPP_LUMINOSITY: r.readLuminosity(x); snprintf(v, n, "%.0f%slx", x, sp); return true;
case LPP_CONCENTRATION: r.readConcentration(x); snprintf(v, n, "%.0f%sppm", x, sp); return true;
case LPP_VOLTAGE: r.readVoltage(x); snprintf(v, n, "%.2f%sV", x, sp); return true;
case LPP_CURRENT: r.readCurrent(x); snprintf(v, n, "%.3f%sA", x, sp); return true;
case LPP_POWER: r.readPower(x); snprintf(v, n, "%.1f%sW", x, sp); return true;
case LPP_PERCENTAGE: r.readPercentage(x); snprintf(v, n, "%.0f%%", x); return true;
case LPP_DISTANCE: r.readDistance(x); snprintf(v, n, "%.2f%sm", x, sp); return true;
case LPP_GPS:
r.readGPS(x, y, z);
if (x != 0 || y != 0) snprintf(v, n, "%.4f %.4f", x, y);
else snprintf(v, n, "--");
return true;
}
r.skipData(type);
return false;
}
// Field `f` as text; "--" when there's nothing to show.
static void text(uint8_t f, const Inputs& in, const Style& st, char* v, int n) {
const char* sp = st.spaced ? " " : "";
snprintf(v, n, "--");
bool fix = in.loc && in.loc->isValid();
switch (f) {
case BATT_V:
if (in.batt_mv) snprintf(v, n, "%u.%02u%sV", in.batt_mv / 1000, (in.batt_mv % 1000) / 10, sp);
return;
case BATT_PCT:
if (in.batt_mv) snprintf(v, n, "%d%%", battery::percent(in.batt_mv, in.low_batt_mv));
return;
case NODES: snprintf(v, n, "%d%s", in.nodes, st.nouns ? " nodes" : ""); return;
case MSGS: snprintf(v, n, "%d%s%s", in.unread, in.unread_more ? "+" : "", st.nouns ? " msgs" : ""); return;
case SATS:
if (in.loc && in.gps_on) snprintf(v, n, "%ld%s", (long)in.loc->satellitesCount(), st.nouns ? " sats" : "");
return;
case GPS:
if (fix) snprintf(v, n, "%.*f %.*f", st.gps_dp, in.loc->getLatitude() / 1e6, st.gps_dp, in.loc->getLongitude() / 1e6);
else if (in.loc) snprintf(v, n, "no fix");
return;
case ALT_GPS:
if (fix) altText(in.loc->getAltitude() / 1000.0f, in.imperial, st, v, n);
else if (in.loc) snprintf(v, n, "no fix");
return;
}
uint8_t want = lppType(f);
if (!want || !in.lpp) return;
LPPReader r(in.lpp, in.lpp_len);
uint8_t ch, type;
while (r.readHeader(ch, type)) {
if (type == want) { lppText(r, type, in.imperial, st, v, n); return; }
r.skipData(type);
}
}
} // namespace telemetry
+23 -64
View File
@@ -110,14 +110,7 @@ static void onAppHold(lv_event_t* e) {
}
// ── Telemetry fields ──
// The values of NodePrefs::dashboard_fields, numbered as L1 has them (the
// prefs are shared): battery, sensors on the bus, position, counts.
enum : uint8_t { F_NONE, F_BATT_V, F_TEMP, F_HUM, F_PRES, F_GPS, F_ALT, F_LUX, F_CO2, F_NODES, F_MSGS,
F_BATT_PCT, F_SATS, F_ALT_GPS, F_COUNT };
static const char* const FIELD_NAME[F_COUNT] = {
"None", "Battery (V)", "Temperature", "Humidity", "Pressure", "Position", "Altitude (sensor)",
"Light", "CO2", "Contacts", "Unread", "Battery (%)", "Satellites", "Altitude (GPS)",
};
// NodePrefs::dashboard_fields, shared with L1: ui-core/Telemetry.h.
static const int FIELDS = 3;
static lv_obj_t* s_field_val[FIELDS];
static lv_obj_t* s_field_pick[FIELDS]; // hidden choices: holding a field opens its picker
@@ -507,10 +500,10 @@ void UITask::buildHomeClock(lv_obj_t* box) {
lv_obj_set_style_pad_top(row, 12, 0);
char opts[200];
int o = 0;
for (int k = 0; k < F_COUNT; k++) o += snprintf(opts + o, sizeof(opts) - o, "%s%s", k ? "\n" : "", FIELD_NAME[k]);
for (int k = 0; k < telemetry::COUNT; k++) o += snprintf(opts + o, sizeof(opts) - o, "%s%s", k ? "\n" : "", telemetry::NAME[k]);
for (int i = 0; i < FIELDS; i++) {
uint8_t f = _prefs ? _prefs->dashboard_fields[i] : F_NONE;
if (f >= F_COUNT) f = F_NONE;
uint8_t f = _prefs ? _prefs->dashboard_fields[i] : telemetry::NONE;
if (f >= telemetry::COUNT) f = telemetry::NONE;
lv_obj_t* c = lv_obj_create(row);
styleSurface(c, f ? theme::SURFACE : theme::BG);
lv_obj_remove_flag(c, LV_OBJ_FLAG_SCROLLABLE);
@@ -530,7 +523,7 @@ void UITask::buildHomeClock(lv_obj_t* box) {
lv_obj_add_event_cb(s_field_pick[i], onFieldPicked, LV_EVENT_VALUE_CHANGED, (void*)(uintptr_t)i);
if (f) {
lv_obj_add_event_cb(c, onFieldHold, LV_EVENT_LONG_PRESSED, (void*)(uintptr_t)i);
label(c, FIELD_NAME[f], THEME_FONT_SMALL, theme::TEXT_MUTED);
label(c, telemetry::NAME[f], THEME_FONT_SMALL, theme::TEXT_MUTED);
s_field_val[i] = label(c, "--", THEME_FONT_TITLE, theme::TEXT);
} else { // a tap is enough on an empty one
lv_obj_add_event_cb(c, onFieldHold, LV_EVENT_CLICKED, (void*)(uintptr_t)i);
@@ -540,7 +533,7 @@ void UITask::buildHomeClock(lv_obj_t* box) {
}
void UITask::homeFieldSet(int slot, int f) {
if (!_prefs || slot < 0 || slot >= home::FIELDS || f < 0 || f >= home::F_COUNT) return;
if (!_prefs || slot < 0 || slot >= home::FIELDS || f < 0 || f >= telemetry::COUNT) return;
_prefs->dashboard_fields[slot] = (uint8_t)f;
prefsSave();
setHomePage(home::CLOCK);
@@ -549,59 +542,25 @@ void UITask::homeFieldSet(int slot, int f) {
// One field's value, as short as fits its card.
void UITask::homeFieldText(uint8_t f, char* v, int n) {
using namespace home;
snprintf(v, n, "--");
bool imperial = _prefs && _prefs->units_imperial;
uint16_t mv = _batt_mv ? _batt_mv : getBattMilliVolts();
LocationProvider* loc = _sensors ? _sensors->getLocationProvider() : nullptr;
switch (f) {
case F_BATT_V: if (mv) snprintf(v, n, "%u.%02u V", mv / 1000, (mv % 1000) / 10); return;
case F_BATT_PCT: if (mv) snprintf(v, n, "%d%%", battery::percent(mv, _prefs ? _prefs->low_batt_mv : 0)); return;
case F_NODES: snprintf(v, n, "%d", the_mesh.getNumContacts()); return;
case F_MSGS: snprintf(v, n, "%d", unreadTotal()); return;
case F_SATS: if (loc && _core->gpsEnabled()) snprintf(v, n, "%ld", (long)loc->satellitesCount()); return;
case F_GPS: {
int32_t lat, lon;
if (_core->course.currentLocation(lat, lon)) snprintf(v, n, "%.3f %.3f", lat / 1e6, lon / 1e6);
else snprintf(v, n, "no fix");
return;
static const telemetry::Style STYLE = { "\xC2\xB0", true, false, 3 }; // UTF-8 degree
telemetry::Inputs in;
in.batt_mv = _batt_mv ? _batt_mv : getBattMilliVolts();
in.low_batt_mv = _prefs ? _prefs->low_batt_mv : 0;
in.nodes = the_mesh.getNumContacts();
in.unread = unreadTotal();
in.imperial = _prefs && _prefs->units_imperial;
in.loc = _sensors ? _sensors->getLocationProvider() : nullptr;
in.gps_on = _core->gpsEnabled();
if (telemetry::isSensor(f)) { // the sensors on the bus: read every few seconds, not per field
if (_sensors && (s_lpp_ms == 0 || millis() - s_lpp_ms > 5000)) {
s_lpp_ms = millis() | 1;
s_lpp.reset();
_sensors->querySensors(0xFF, s_lpp);
}
case F_ALT_GPS:
if (loc && loc->isValid()) {
float m = loc->getAltitude() / 1000.0f;
snprintf(v, n, imperial ? "%.0f ft" : "%.0f m", imperial ? m * 3.28084f : m);
} else snprintf(v, n, "no fix");
return;
}
// Sensors on the bus: read every few seconds, not per field.
if (_sensors && (s_lpp_ms == 0 || millis() - s_lpp_ms > 5000)) {
s_lpp_ms = millis() | 1;
s_lpp.reset();
_sensors->querySensors(0xFF, s_lpp);
}
uint8_t want = f == F_TEMP ? LPP_TEMPERATURE : f == F_HUM ? LPP_RELATIVE_HUMIDITY : f == F_PRES ? LPP_BAROMETRIC_PRESSURE
: f == F_ALT ? LPP_ALTITUDE : f == F_LUX ? LPP_LUMINOSITY : f == F_CO2 ? LPP_CONCENTRATION : 0;
if (!want) return;
LPPReader r(s_lpp.getBuffer(), s_lpp.getSize());
uint8_t ch, type;
while (r.readHeader(ch, type)) {
if (type != want) { r.skipData(type); continue; }
float x;
switch (type) {
case LPP_TEMPERATURE:
r.readTemperature(x);
snprintf(v, n, imperial ? "%.1f \xC2\xB0""F" : "%.1f \xC2\xB0""C", imperial ? x * 9 / 5 + 32 : x);
break;
case LPP_RELATIVE_HUMIDITY: r.readRelativeHumidity(x); snprintf(v, n, "%.0f%%", x); break;
case LPP_BAROMETRIC_PRESSURE: r.readPressure(x); snprintf(v, n, "%.0f hPa", x); break;
case LPP_ALTITUDE:
r.readAltitude(x);
snprintf(v, n, imperial ? "%.0f ft" : "%.0f m", imperial ? x * 3.28084f : x);
break;
case LPP_LUMINOSITY: r.readLuminosity(x); snprintf(v, n, "%.0f lx", x); break;
case LPP_CONCENTRATION: r.readConcentration(x); snprintf(v, n, "%.0f ppm", x); break;
}
return;
in.lpp = s_lpp.getBuffer();
in.lpp_len = s_lpp.getSize();
}
telemetry::text(f, in, STYLE, v, n);
}
// ── Minimap page ──
+10 -12
View File
@@ -73,6 +73,7 @@ template <class T> static T* psramBuf(size_t n) {
#include "../ui-core/RepeaterControl.h"
#include "../ui-core/Diagnostics.h"
#include "../ui-core/Battery.h"
#include "../ui-core/Telemetry.h"
#include "../ui-core/ChannelControl.h"
#include "../ui-core/BotConfig.h"
#include "../ui-core/SoundControl.h"
@@ -585,21 +586,12 @@ static lv_obj_t* rowSub(lv_obj_t* row) {
enum : uint8_t { PG_KEYBOARD = settings::PG_COUNT, PG_ABOUT, PG_DISPLAY, PG_POWER, PG_TIME, PG_ALL };
// A schema setting by its NodePrefs field; -1 if there's none.
static int settingIdx(uint16_t offset) {
for (int i = 0; i < settings::COUNT; i++) if (settings::ALL[i].offset == offset) return i;
return -1;
}
#define SETTING(field) settingIdx(offsetof(NodePrefs, field))
#define SETTING(field) settings::indexOf(offsetof(NodePrefs, field))
// A setting's current value as its choice shows it ("UTC+2", "3.4 V" ...).
static const char* settingText(const NodePrefs& p, int idx, char* buf, int n) {
buf[0] = '\0';
if (idx < 0) return buf;
const settings::Setting& st = settings::ALL[idx];
uint8_t v = settings::get(p, st);
if (st.option) st.option(v, buf, n, p);
else snprintf(buf, n, "%s", v ? "On" : "Off");
return buf;
return idx < 0 ? buf : settings::text(p, settings::ALL[idx], buf, n);
}
static lv_obj_t* s_sec_card[settings::SEC_COUNT]; // schemaRows()' groups, by section
static bool s_opts_from_map = false; // Map options opened from the map: back returns there
@@ -861,6 +853,7 @@ static void contactName(const uint8_t* prefix, char* out, size_t n) {
void UITask::begin(DisplayDriver* display_drv, SensorManager* sensors, NodePrefs* node_prefs) {
s_ui = this;
_ui_started_ms = millis();
_display = display_drv;
_sensors = sensors;
_prefs = node_prefs;
@@ -933,7 +926,8 @@ void UITask::loop() {
// USER (BOOT, side) button: Home's clock page; held and let go, mutes / unmutes, also with the
// screen off, which it doesn't wake. WAKE (top) button: screen off / on.
// Either one silences a ringing alarm first.
// Either one silences a ringing alarm first. Held in the first 8 s (once the
// screen is up -- held at power-on it's the ESP32's download mode): CLI rescue.
bool btn_click = false, btn_hold = false, wake_press = false;
#ifdef PIN_USER_BTN
int ev = user_btn.check();
@@ -970,6 +964,10 @@ void UITask::loop() {
if (shot) { if (!_asleep) takeScreenshot(); }
else if ((btn_click || btn_hold || wake_press) && _core->clock.isRinging()) dismissRing();
else if (wake_press) { if (_asleep) wake(); else sleep(); }
else if (btn_hold && millis() - _ui_started_ms < 8000) { // held in the first 8 s: CLI rescue, as on the L1
the_mesh.enterCLIRescue();
showToast("CLI rescue on the USB serial", 4000);
}
else if (btn_hold) mute_armed = true;
else if (mute) toggleMute();
else if (btn_click) { if (!_asleep && !locked()) goHome(); } // the side button doesn't wake: pockets
@@ -507,6 +507,7 @@ private:
uint32_t _next_nearby_ms = 0;
bool _pinging = false;
uint32_t _ping_started_ms = 0;
uint32_t _ui_started_ms = 0; // begin(): the side button held soon after is the CLI rescue
lv_obj_t* _nearby_list = nullptr;
lv_obj_t* _nearby_status = nullptr;
lv_obj_t* _nearby_sort_lbl = nullptr;
@@ -2,22 +2,7 @@
// 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_V = 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_MSGS = 10;
static const uint8_t DASH_BATT_PCT = 11;
static const uint8_t DASH_SATS = 12;
static const uint8_t DASH_ALT_GPS = 13;
static const uint8_t DASH_COUNT = 14;
// The fields and their names: ui-core/Telemetry.h.
class DashboardConfigScreen : public UIScreen {
UITask* _task;
@@ -25,14 +10,12 @@ class DashboardConfigScreen : public UIScreen {
static const int FIELD_SLOTS = 3;
static const char* OPTION_NAMES[DASH_COUNT];
int _sel;
bool _dirty;
void cycle(int slot, int dir) {
uint8_t& f = _prefs->dashboard_fields[slot];
f = (uint8_t)((f + DASH_COUNT + dir) % DASH_COUNT);
f = (uint8_t)((f + telemetry::COUNT + dir) % telemetry::COUNT);
_dirty = true;
}
@@ -59,7 +42,7 @@ public:
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.print(telemetry::COMPACT[f < telemetry::COUNT ? f : telemetry::NONE]);
display.setColor(DisplayDriver::LIGHT);
}
return 500;
@@ -80,8 +63,3 @@ public:
}
};
const char* DashboardConfigScreen::OPTION_NAMES[DASH_COUNT] = {
"None", "Batt V", "Temp", "Humidity", "Pressure",
"GPS", "Altitude (Baro)", "Lux", "CO2", "Contacts", "Messages", "Batt %", "Sats",
"Altitude (GPS)"
};
@@ -37,12 +37,6 @@ class MessagesScreen : public UIScreen {
int _sel_channel_idx;
bool _sending_to_channel;
// Carries the just-sent DM's ACK tag + deadline + send timestamp from
// sendText() to afterSend().
uint32_t _last_ack_tag = 0;
uint32_t _last_ack_deadline_ms = 0;
uint32_t _last_send_ts = 0;
// MSG_PICK (shared)
int _msg_sel, _msg_scroll;
int _active_msgs[QUICK_MSGS_MAX];
@@ -538,24 +532,12 @@ class MessagesScreen : public UIScreen {
if (ok && _sending_to_channel) {
_hist_sel = 0;
_hist_scroll = 0;
_phase = CHANNEL_HIST; // set before filing so the Core sees viewing=true, no unread bump
char entry[sizeof(ChHistEntry::text)];
snprintf(entry, sizeof(entry), "Me: %s", msg);
int pos = _task->core().addChannelMsg(_sel_channel_idx, entry);
// Arm the "relayed into mesh" marker on this exact entry — MyMesh tracked
// the flood it just originated and reports a heard repeater echo by seq.
if (pos >= 0) _history.armChannelRelay(pos, the_mesh.lastChannelRelaySeq());
// After inserting sent msg at index 0, the unread index range is stale.
// User is active in this channel — treat as fully read.
_history.setChUnread(_sel_channel_idx, 0);
// Filed at index 0 by sendText(): the unread index range is stale, and
// the user is active in this channel -- fully read.
_unread_at_entry = 0;
_viewing_max_seen = 0;
_task->showAlert("Sent", 600);
} else if (ok) {
NodePrefs* np = _task->getNodePrefs();
uint8_t resends = np ? np->dm_resend_count : 0;
_history.storeDMMsg(_sel_contact.id.pub_key, true, msg, _last_ack_tag,
_last_ack_deadline_ms, _last_send_ts, resends);
_dm_hist_sel = 0;
_dm_hist_scroll = 0;
_phase = DM_HIST;
@@ -566,29 +548,16 @@ class MessagesScreen : public UIScreen {
}
}
// Sent through the Core, which files the message as L2's are: a DM with its
// ACK tracking and resends, a channel post as "Me: ..." with the relay
// marker armed and the channel read.
bool sendText(const char* msg) {
_last_ack_tag = 0;
_last_ack_deadline_ms = 0;
_last_send_ts = 0;
if (_sending_to_channel) {
ChannelDetails ch;
if (!the_mesh.getChannel(_sel_channel_idx, ch)) return false;
return the_mesh.sendGroupMessage(rtc_clock.getCurrentTime(), ch.channel,
the_mesh.getNodeName(), msg, strlen(msg));
} else {
uint32_t send_ts = rtc_clock.getCurrentTime();
uint32_t expected_ack = 0, est_timeout = 0;
bool ok = the_mesh.sendMessage(_sel_contact, send_ts, 0,
msg, expected_ack, est_timeout) > 0;
if (ok && expected_ack) {
_last_send_ts = send_ts;
_last_ack_tag = expected_ack;
// Generous margin over the base estimate so a slow multi-hop ACK isn't
// prematurely shown as failed.
_last_ack_deadline_ms = millis() + est_timeout + 4000;
}
return ok;
}
if (!_sending_to_channel) return _task->core().sendDirectText(_sel_contact, msg);
Phase prev = _phase;
_phase = CHANNEL_HIST; // before filing, so the Core sees it viewed: no unread bump
bool ok = _task->core().sendChannelText(_sel_channel_idx, msg);
if (!ok) _phase = prev;
return ok;
}
// Fill _pin_slot_labels for the "Pick slot" submenu: each dial slot with its
@@ -218,7 +218,7 @@ public:
_task->keyboard().clearPlaceholders(); // {loc}/{time} are for messages, not preset names
break;
case RadioPresetPicker::APPLIED:
the_mesh.applyRepeaterRadio(); // live if currently relaying on the profile
rptctl::applyProfile(); // live if currently relaying on the profile
_dirty = true;
break;
case RadioPresetPicker::DELETED:
@@ -235,7 +235,7 @@ public:
return true;
}
if (_editor.active()) {
if (_editor.handleFreqInput(c) && p) { the_mesh.applyRepeaterRadio(); _dirty = true; }
if (_editor.handleFreqInput(c) && p) { rptctl::applyProfile(); _dirty = true; }
return true;
}
@@ -272,9 +272,9 @@ public:
return true;
}
int dir = right ? 1 : (left ? -1 : 0);
if (item == IT_RSF && dir && RadioParamsEditor::stepSF(p->repeater_sf, dir)) { the_mesh.applyRepeaterRadio(); _dirty = true; return true; }
if (item == IT_RBW && dir && RadioParamsEditor::stepBW(p->repeater_bw, dir)) { the_mesh.applyRepeaterRadio(); _dirty = true; return true; }
if (item == IT_RCR && dir && RadioParamsEditor::stepCR(p->repeater_cr, dir)) { the_mesh.applyRepeaterRadio(); _dirty = true; return true; }
if (item == IT_RSF && dir && RadioParamsEditor::stepSF(p->repeater_sf, dir)) { rptctl::applyProfile(); _dirty = true; return true; }
if (item == IT_RBW && dir && RadioParamsEditor::stepBW(p->repeater_bw, dir)) { rptctl::applyProfile(); _dirty = true; return true; }
if (item == IT_RCR && dir && RadioParamsEditor::stepCR(p->repeater_cr, dir)) { rptctl::applyProfile(); _dirty = true; return true; }
if (item == IT_SKIP && (left || right || enter)) {
p->repeat_skip_adverts ^= 1; _dirty = true; return true;
}
+85 -319
View File
@@ -15,18 +15,7 @@ class SettingsScreen : public UIScreen {
enum SettingItem {
// Display section
SECTION_DISPLAY,
#if FEAT_BRIGHTNESS_SETTING
BRIGHTNESS,
#endif
#if AUTO_OFF_MILLIS > 0
AUTO_OFF,
#endif
AUTO_LOCK,
BATT_DISPLAY,
#if FEAT_CLOCK_SECONDS_SETTING
CLOCK_SECONDS,
#endif
CLOCK_FORMAT,
SCHEMA_DISPLAY, // the schema's display and time settings (ui-core/SettingsSchema.h)
#if FEAT_DISPLAY_ROTATION_SETTING
ROTATION,
#endif
@@ -36,16 +25,10 @@ class SettingsScreen : public UIScreen {
#if FEAT_FULL_REFRESH_SETTING
EINK_FULL_REFRESH,
#endif
MSG_WAKE,
// Sound section
SECTION_SOUND,
BUZZER,
BUZZER_VOLUME,
QUIET_HOURS, QUIET_FROM, QUIET_TO,
DM_MELODY,
CH_MELODY,
AD_SOUND,
AD_SOUND_SCOPE,
SCHEMA_SOUND, // volume, quiet hours, what plays for what
// Home pages section
SECTION_HOME_PAGES,
HOME_CLOCK, HOME_FAVOURITES, HOME_RADIO, HOME_BT, HOME_ADVERT,
@@ -70,12 +53,7 @@ class SettingsScreen : public UIScreen {
// System section
SECTION_SYSTEM,
DEVICE_NAME,
TIMEZONE,
LOW_BAT,
#if ENV_INCLUDE_GPS == 1
GPS_DUTY_CYCLE,
#endif
UNITS,
SCHEMA_SYSTEM, // power, units
REBOOT,
// Keyboard section
SECTION_KEYBOARD,
@@ -86,14 +64,16 @@ class SettingsScreen : public UIScreen {
KEYBOARD_CARDKB_COMPACT,
#endif
// Contacts section
SECTION_CONTACTS, DM_FILTER, CH_FILTER, ROOM_FILTER, FAV_SORT, EXPIRE_AFTER, PRUNE_NOW,
SECTION_CONTACTS, DM_FILTER, CH_FILTER, ROOM_FILTER, SCHEMA_CONTACTS, PRUNE_NOW,
// Messages section
SECTION_MESSAGES,
DM_RESEND,
SCHEMA_MESSAGES,
MSG_SLOT_0, MSG_SLOT_1, MSG_SLOT_2, MSG_SLOT_3, MSG_SLOT_4,
MSG_SLOT_5, MSG_SLOT_6, MSG_SLOT_7, MSG_SLOT_8, MSG_SLOT_9,
Count
};
// A schema setting's row: SCHEMA_ITEM + its index in settings::ALL.
static const int SCHEMA_ITEM = 128;
// Cursor + scroll, fold state and the flattened visible list are owned by the
// shared AccordionList helper. We keep only the section→SettingItem mapping it
@@ -110,50 +90,11 @@ class SettingsScreen : public UIScreen {
uint8_t _sec_header[NUM_SECTIONS]; // the SECTION_* enum for each section
int _num_sections = 0;
#if AUTO_OFF_MILLIS > 0
static const uint16_t AUTO_OFF_OPTS[5];
static const char* AUTO_OFF_LABELS[5];
static const int AUTO_OFF_COUNT = 5;
#endif
static const uint16_t LOW_BAT_OPTS[7];
static const char* LOW_BAT_LABELS[7];
static const int LOW_BAT_COUNT = 7;
#if ENV_INCLUDE_GPS == 1
// GPS duty-cycle sleep window: how long GPS naps between fix acquisitions.
// "OFF" (0) keeps it continuously on, today's behaviour. Backed by
// NodePrefs::gps_interval, seconds.
static const uint32_t GPS_DUTY_OPTS[6];
static const char* GPS_DUTY_LABELS[6];
static const int GPS_DUTY_COUNT = 6;
int gpsDutyIndex() {
NodePrefs* p = _task->getNodePrefs();
if (!p) return 0;
for (int i = 0; i < GPS_DUTY_COUNT; i++)
if (GPS_DUTY_OPTS[i] == p->gps_interval) return i;
return 0;
}
#endif
static const char* BATT_DISPLAY_LABELS[3];
static const int BATT_DISPLAY_COUNT = 3;
static const char* SOUND_LABELS[4];
static const int SOUND_COUNT = 4;
static const char* AD_SCOPE_LABELS[2];
static const int AD_SCOPE_COUNT = 2;
// ("Expire" reads its labels straight from NodePrefs::contactExpiryLabel(),
// which is also where MyMesh takes the matching day count from.)
#if FEAT_FULL_REFRESH_SETTING
static const char* EINK_FULL_REFRESH_LABELS[5];
static const int EINK_FULL_REFRESH_COUNT = 5;
#endif
int lowBatIndex() {
NodePrefs* p = _task->getNodePrefs();
if (!p) return 0;
for (int i = 0; i < LOW_BAT_COUNT; i++)
if (LOW_BAT_OPTS[i] == p->low_batt_mv) return i;
return 0;
}
// The companion's own radio fields, as the shared preset picker's target
// (Tools › Repeater points the same picker at the dedicated repeater profile).
RadioPresetPicker::Target radioTarget(NodePrefs* p) const {
@@ -185,15 +126,6 @@ class SettingsScreen : public UIScreen {
}
}
#if AUTO_OFF_MILLIS > 0
int autoOffIndex() {
NodePrefs* p = _task->getNodePrefs();
if (!p) return 1;
for (int i = 0; i < AUTO_OFF_COUNT; i++)
if (AUTO_OFF_OPTS[i] == p->auto_off_secs) return i;
return 1;
}
#endif
bool isSection(int item) const {
@@ -218,6 +150,8 @@ class SettingsScreen : public UIScreen {
// Walk the SettingItem enum once, bucketing items under their section header.
// #if-guarded items need no special handling — they simply aren't in the enum.
// A SCHEMA_* placeholder becomes the schema's settings of its sections, in
// the schema's order -- so a setting added there shows here too.
void buildSections() {
int cur = -1;
for (int i = 0; i < (int)Count; i++) {
@@ -225,6 +159,10 @@ class SettingsScreen : public UIScreen {
if (++cur >= NUM_SECTIONS) break;
_sec_header[cur] = (uint8_t)i;
_sec_count[cur] = 0;
} else if (cur >= 0 && isSchemaGroup(i)) {
for (int k = 0; k < settings::COUNT && _sec_count[cur] < MAX_PER_SEC; k++)
if (schemaIn(i, settings::ALL[k].section) && schemaShown(settings::ALL[k]))
_sec_items[cur][_sec_count[cur]++] = (uint8_t)(SCHEMA_ITEM + k);
} else if (cur >= 0 && _sec_count[cur] < MAX_PER_SEC) {
_sec_items[cur][_sec_count[cur]++] = (uint8_t)i;
}
@@ -232,6 +170,69 @@ class SettingsScreen : public UIScreen {
_num_sections = cur + 1;
}
static bool isSchemaGroup(int item) {
return item == SCHEMA_DISPLAY || item == SCHEMA_SOUND || item == SCHEMA_SYSTEM ||
item == SCHEMA_CONTACTS || item == SCHEMA_MESSAGES;
}
// Which schema sections a placeholder stands for.
static bool schemaIn(int group, uint8_t sec) {
using namespace settings;
switch (group) {
case SCHEMA_DISPLAY: return sec == SEC_DISPLAY || sec == SEC_TIME;
case SCHEMA_SOUND: return sec == SEC_SOUND || sec == SEC_QUIET || sec == SEC_SOUND_FOR;
case SCHEMA_SYSTEM: return sec == SEC_POWER || sec == SEC_UNITS;
case SCHEMA_CONTACTS: return sec == SEC_CONTACTS;
case SCHEMA_MESSAGES: return sec == SEC_MESSAGES;
}
return false;
}
// Settings this board has no use for.
static bool schemaShown(const settings::Setting& st) {
uint16_t o = st.offset;
(void)o;
#if !FEAT_BRIGHTNESS_SETTING
if (o == offsetof(NodePrefs, display_brightness)) return false;
#endif
#if AUTO_OFF_MILLIS == 0
if (o == offsetof(NodePrefs, auto_off_secs)) return false;
#endif
#if !FEAT_CLOCK_SECONDS_SETTING
if (o == offsetof(NodePrefs, clock_hide_seconds)) return false;
#endif
#if ENV_INCLUDE_GPS != 1
if (o == offsetof(NodePrefs, gps_interval)) return false;
#endif
#ifndef PIN_BUZZER
if (o == offsetof(NodePrefs, buzzer_volume) || o == offsetof(NodePrefs, notif_melody_dm) ||
o == offsetof(NodePrefs, notif_melody_ch) || o == offsetof(NodePrefs, notif_melody_ad) ||
o == offsetof(NodePrefs, advert_sound_scope)) return false;
#endif
return true;
}
// A schema setting: its short label, the value in the value column (a
// switch as ON / OFF, brightness and volume as bars). A value too long for
// the column starts further left, clear of the label, else scrolls.
int renderSchema(DisplayDriver& display, const settings::Setting& st, NodePrefs* p, int y, bool sel) {
const char* label = settings::shortLabel(st);
display.print(label);
if (!p) return 0;
if (st.offset == offsetof(NodePrefs, display_brightness) || st.offset == offsetof(NodePrefs, buzzer_volume)) {
renderBar(display, valCol(display), y, settings::get(*p, st) + 1, st.count);
return 0;
}
char v[24];
settings::text(*p, st, v, sizeof(v), "ON", "OFF");
int right = display.width() - _reserve;
int x = valCol(display), w = display.getTextWidth(v);
if (x + w > right) {
int min_x = 2 + display.getTextWidth(label) + display.getCharWidth();
x = right - w > min_x ? right - w : min_x;
}
int r = display.drawTextEllipsized(x, y, right - x, v, sel);
return sel && r > 0 ? r : 0;
}
// (Re)load the section sizes into the accordion (folds all, resets the cursor).
void resetList() {
uint8_t sizes[NUM_SECTIONS];
@@ -457,13 +458,9 @@ class SettingsScreen : public UIScreen {
display.setCursor(2, y);
#if FEAT_BRIGHTNESS_SETTING
if (item == BRIGHTNESS) {
display.print("Bright");
renderBar(display, valCol(display), y, (p ? p->display_brightness : 2) + 1, 5);
} else
#endif
if (item == BUZZER) {
if (item >= SCHEMA_ITEM) {
mq_delay = renderSchema(display, settings::ALL[item - SCHEMA_ITEM], p, y, sel);
} else if (item == BUZZER) {
display.print("Buzzer");
display.setCursor(valCol(display), y);
#ifdef PIN_BUZZER
@@ -473,49 +470,6 @@ class SettingsScreen : public UIScreen {
#else
display.print("N/A");
#endif
} else if (item == BUZZER_VOLUME) {
display.print("Buzzer vol");
#ifdef PIN_BUZZER
renderBar(display, valCol(display), y, _task->getBuzzerVolume() + 1, 5);
#else
display.setCursor(valCol(display), y);
display.print("N/A");
#endif
} else if (item == QUIET_HOURS) {
display.print("Quiet hrs");
display.setCursor(valCol(display), y);
display.print((p && p->quiet_hours) ? "ON" : "OFF");
} else if (item == QUIET_FROM || item == QUIET_TO) {
display.print(item == QUIET_FROM ? " from" : " until");
display.setCursor(valCol(display), y);
{ char hb[8]; uint8_t h = p ? (item == QUIET_FROM ? p->quiet_from : p->quiet_to) : 0;
if (p && p->clock_12h) snprintf(hb, sizeof(hb), "%u%s", (unsigned)(h % 12 ? h % 12 : 12), h < 12 ? "AM" : "PM");
else snprintf(hb, sizeof(hb), "%02u:00", (unsigned)h);
display.print(hb); }
} else if (item == DM_MELODY) {
display.print("DM sound");
display.setCursor(valCol(display), y);
{ uint8_t v = p ? p->notif_melody_dm : 0;
display.print(SOUND_LABELS[v < SOUND_COUNT ? v : 0]); }
} else if (item == CH_MELODY) {
display.print("Ch sound");
display.setCursor(valCol(display), y);
{ uint8_t v = p ? p->notif_melody_ch : 0;
display.print(SOUND_LABELS[v < SOUND_COUNT ? v : 0]); }
} else if (item == AD_SOUND) {
display.print("AD sound");
display.setCursor(valCol(display), y);
{ uint8_t v = p ? p->notif_melody_ad : 0;
display.print(SOUND_LABELS[v < SOUND_COUNT ? v : 0]); }
} else if (item == AD_SOUND_SCOPE) {
display.print("AD scope");
display.setCursor(valCol(display), y);
{ uint8_t v = p ? p->advert_sound_scope : ADVERT_SOUND_SCOPE_ALL;
display.print(AD_SCOPE_LABELS[v < AD_SCOPE_COUNT ? v : 0]); }
} else if (item == MSG_WAKE) {
display.print("Msg wake");
display.setCursor(valCol(display), y);
display.print((p && p->msg_wake_screen_off) ? "OFF" : "ON");
} else if (isHomePage(item)) {
if (p) ensurePageOrderInit(p);
int pos = homePagePosition(item, p);
@@ -591,38 +545,6 @@ class SettingsScreen : public UIScreen {
int r = display.drawTextEllipsized(vx, y, display.width() - vx - _reserve,
sl.name(sl.default_idx), sel);
if (sel && r > 0) mq_delay = r;
#if AUTO_OFF_MILLIS > 0
} else if (item == AUTO_OFF) {
display.print("Auto off");
display.setCursor(valCol(display), y);
display.print(AUTO_OFF_LABELS[autoOffIndex()]);
#endif
} else if (item == AUTO_LOCK) {
display.print("Auto lock");
display.setCursor(valCol(display), y);
display.print((p && p->auto_lock) ? "ON" : "OFF");
} else if (item == TIMEZONE) {
display.print("Time zone");
char buf[8];
int8_t tz = p ? p->tz_offset_hours : 0;
if (tz >= 0) snprintf(buf, sizeof(buf),"UTC+%d", (int)tz);
else snprintf(buf, sizeof(buf),"UTC%d", (int)tz);
display.setCursor(valCol(display), y);
display.print(buf);
} else if (item == LOW_BAT) {
display.print("Low batt");
display.setCursor(valCol(display), y);
display.print(LOW_BAT_LABELS[lowBatIndex()]);
#if ENV_INCLUDE_GPS == 1
} else if (item == GPS_DUTY_CYCLE) {
display.print("GPS pwr");
display.setCursor(valCol(display), y);
display.print(GPS_DUTY_LABELS[gpsDutyIndex()]);
#endif
} else if (item == UNITS) {
display.print("Units");
display.setCursor(valCol(display), y);
display.print((p && p->units_imperial) ? "Imperial" : "Metric");
} else if (item == DEVICE_NAME) {
display.print("Name");
int vx = valCol(display);
@@ -648,21 +570,6 @@ class SettingsScreen : public UIScreen {
display.setCursor(valCol(display), y);
display.print((p && p->keyboard_cardkb_compact) ? "Compact" : "Full");
#endif
} else if (item == BATT_DISPLAY) {
display.print("Batt disp");
display.setCursor(valCol(display), y);
uint8_t mode = p ? p->batt_display_mode : 0;
display.print(BATT_DISPLAY_LABELS[mode < BATT_DISPLAY_COUNT ? mode : 0]);
#if FEAT_CLOCK_SECONDS_SETTING
} else if (item == CLOCK_SECONDS) {
display.print("Seconds");
display.setCursor(valCol(display), y);
display.print((p && p->clock_hide_seconds) ? "OFF" : "ON");
#endif
} else if (item == CLOCK_FORMAT) {
display.print("Format");
display.setCursor(valCol(display), y);
display.print((p && p->clock_12h) ? "12h" : "24h");
#if FEAT_DISPLAY_ROTATION_SETTING
} else if (item == ROTATION) {
display.print("Rotation");
@@ -695,22 +602,8 @@ class SettingsScreen : public UIScreen {
display.print("Rooms");
display.setCursor(valCol(display), y);
display.print((p && p->room_fav_only) ? "Fav" : "All");
} else if (item == FAV_SORT) {
display.print("Favs top");
display.setCursor(valCol(display), y);
display.print((p && p->fav_sort_off) ? "OFF" : "ON");
} else if (item == EXPIRE_AFTER) {
display.print("Expire");
display.setCursor(valCol(display), y);
display.print(NodePrefs::contactExpiryLabel(p ? p->contact_expiry_idx : 0));
} else if (item == PRUNE_NOW) {
display.print("Prune now"); // action row: Enter counts + confirms + removes
} else if (item == DM_RESEND) {
display.print("Resend");
display.setCursor(valCol(display), y);
uint8_t n = p ? p->dm_resend_count : 0;
if (n == 0) display.print("OFF");
else { char buf[6]; snprintf(buf, sizeof(buf), "%ux", (unsigned)n); display.print(buf); }
} else if (isMsgSlot(item)) {
int slot = msgSlotIndex(item);
char label[5];
@@ -1018,56 +911,18 @@ public:
bool left = keyIsPrev(c);
bool enter = (c == KEY_ENTER);
#if FEAT_BRIGHTNESS_SETTING
if (_selected == BRIGHTNESS) {
uint8_t lvl = _task->getBrightnessLevel();
if (right && lvl < 4) { _task->setBrightnessLevel(lvl + 1); _dirty = true; return true; }
if (left && lvl > 0) { _task->setBrightnessLevel(lvl - 1); _dirty = true; return true; }
return right || left;
if (_selected >= SCHEMA_ITEM) {
if (!p || !(left || right || enter)) return false;
settings::step(*p, settings::ALL[_selected - SCHEMA_ITEM], left ? -1 : 1, _task->core());
_dirty = true;
return true;
}
#endif
if (_selected == BUZZER && (left || right || enter)) {
_task->cycleBuzzerMode();
_dirty = true;
return true;
}
if (_selected == BUZZER_VOLUME) {
#ifdef PIN_BUZZER
uint8_t lvl = _task->getBuzzerVolume();
if (right && lvl < 4) { _task->setBuzzerVolumeLevel(lvl + 1); _dirty = true; return true; }
if (left && lvl > 0) { _task->setBuzzerVolumeLevel(lvl - 1); _dirty = true; return true; }
#endif
return right || left;
}
if (_selected == QUIET_HOURS && p && (left || right || enter)) {
p->quiet_hours ^= 1;
_dirty = true; return true;
}
if ((_selected == QUIET_FROM || _selected == QUIET_TO) && p && (left || right || enter)) {
uint8_t& h = _selected == QUIET_FROM ? p->quiet_from : p->quiet_to;
h = (h + (left ? 23 : 1)) % 24;
_dirty = true; return true;
}
if (_selected == DM_MELODY && p && (left || right || enter)) {
p->notif_melody_dm = (p->notif_melody_dm + (left ? SOUND_COUNT - 1 : 1)) % SOUND_COUNT;
_dirty = true; return true;
}
if (_selected == CH_MELODY && p && (left || right || enter)) {
p->notif_melody_ch = (p->notif_melody_ch + (left ? SOUND_COUNT - 1 : 1)) % SOUND_COUNT;
_dirty = true; return true;
}
if (_selected == AD_SOUND && p && (left || right || enter)) {
p->notif_melody_ad = (p->notif_melody_ad + (left ? SOUND_COUNT - 1 : 1)) % SOUND_COUNT;
_dirty = true; return true;
}
if (_selected == AD_SOUND_SCOPE && p && (left || right || enter)) {
p->advert_sound_scope ^= 1;
_dirty = true; return true;
}
if (_selected == MSG_WAKE && p && (left || right || enter)) {
p->msg_wake_screen_off ^= 1;
_dirty = true; return true;
}
if (isHomePage(_selected) && p) {
if (left || right) {
movePageInOrder(_selected, left ? -1 : 1, p);
@@ -1119,45 +974,6 @@ public:
_dirty = true;
return true;
}
#if AUTO_OFF_MILLIS > 0
if (_selected == AUTO_OFF && p) {
int idx = autoOffIndex();
if (right || enter) idx = (idx + 1) % AUTO_OFF_COUNT;
else if (left) idx = (idx + AUTO_OFF_COUNT - 1) % AUTO_OFF_COUNT;
if (left || right || enter) { p->auto_off_secs = AUTO_OFF_OPTS[idx]; _dirty = true; return true; }
}
#endif
if (_selected == AUTO_LOCK && p && (left || right || enter)) {
p->auto_lock ^= 1;
_dirty = true;
return true;
}
if (_selected == TIMEZONE && p) {
if (right && p->tz_offset_hours < 14) { p->tz_offset_hours++; _dirty = true; return true; }
if (left && p->tz_offset_hours > -12) { p->tz_offset_hours--; _dirty = true; return true; }
}
if (_selected == LOW_BAT && p) {
int idx = lowBatIndex();
if (right || enter) idx = (idx + 1) % LOW_BAT_COUNT;
else if (left) idx = (idx + LOW_BAT_COUNT - 1) % LOW_BAT_COUNT;
if (left || right || enter) { p->low_batt_mv = LOW_BAT_OPTS[idx]; _dirty = true; return true; }
}
#if ENV_INCLUDE_GPS == 1
if (_selected == GPS_DUTY_CYCLE && p && (left || right || enter)) {
int idx = gpsDutyIndex();
if (right || enter) idx = (idx + 1) % GPS_DUTY_COUNT;
else if (left) idx = (idx + GPS_DUTY_COUNT - 1) % GPS_DUTY_COUNT;
p->gps_interval = GPS_DUTY_OPTS[idx];
_task->applyGpsInterval();
_dirty = true;
return true;
}
#endif
if (_selected == UNITS && p && (left || right || enter)) {
p->units_imperial ^= 1;
_dirty = true;
return true;
}
if (_selected == DEVICE_NAME && p && enter) {
_edit_name = true;
_kb->begin(the_mesh.getNodeName(), (int)sizeof(p->node_name) - 1);
@@ -1203,30 +1019,6 @@ public:
return true;
}
#endif
if (_selected == DM_RESEND && p) {
int n = p->dm_resend_count;
if (right || enter) n = (n + 1) % 6; // 0..5, wraps
else if (left) n = (n + 5) % 6;
if (left || right || enter) { p->dm_resend_count = (uint8_t)n; _dirty = true; return true; }
}
if (_selected == BATT_DISPLAY && p) {
int idx = p->batt_display_mode < BATT_DISPLAY_COUNT ? p->batt_display_mode : 0;
if (right || enter) idx = (idx + 1) % BATT_DISPLAY_COUNT;
else if (left) idx = (idx + BATT_DISPLAY_COUNT - 1) % BATT_DISPLAY_COUNT;
if (left || right || enter) { p->batt_display_mode = idx; _dirty = true; return true; }
}
#if FEAT_CLOCK_SECONDS_SETTING
if (_selected == CLOCK_SECONDS && p && (left || right || enter)) {
p->clock_hide_seconds ^= 1;
_dirty = true;
return true;
}
#endif
if (_selected == CLOCK_FORMAT && p && (left || right || enter)) {
p->clock_12h ^= 1;
_dirty = true;
return true;
}
#if FEAT_DISPLAY_ROTATION_SETTING
if (_selected == ROTATION && p && (left || right || enter)) {
p->display_rotation = (p->display_rotation + (left ? 3 : 1)) & 3;
@@ -1268,19 +1060,6 @@ public:
_dirty = true;
return true;
}
if (_selected == FAV_SORT && p && (left || right || enter)) {
p->fav_sort_off = p->fav_sort_off ? 0 : 1;
_dirty = true;
return true;
}
if (_selected == EXPIRE_AFTER && p && (left || right || enter)) {
const int n_opt = NodePrefs::CONTACT_EXPIRY_COUNT;
int idx = (p->contact_expiry_idx < n_opt) ? p->contact_expiry_idx : 0;
idx = (idx + (left ? n_opt - 1 : 1)) % n_opt;
p->contact_expiry_idx = (uint8_t)idx;
_dirty = true;
return true;
}
if (_selected == PRUNE_NOW && enter) {
int n = the_mesh.countStaleContacts();
if (n == 0) {
@@ -1307,19 +1086,6 @@ public:
}
};
#if AUTO_OFF_MILLIS > 0
const uint16_t SettingsScreen::AUTO_OFF_OPTS[5] = { 5, 15, 30, 60, 0 };
const char* SettingsScreen::AUTO_OFF_LABELS[5] = { "5s", "15s", "30s", "60s", "OFF" };
#endif
const uint16_t SettingsScreen::LOW_BAT_OPTS[7] = { 0, 3000, 3100, 3200, 3300, 3400, 3500 };
const char* SettingsScreen::LOW_BAT_LABELS[7] = { "OFF", "3.0V", "3.1V", "3.2V", "3.3V", "3.4V", "3.5V" };
#if ENV_INCLUDE_GPS == 1
const uint32_t SettingsScreen::GPS_DUTY_OPTS[6] = { 0, 60, 300, 900, 1800, 3600 };
const char* SettingsScreen::GPS_DUTY_LABELS[6] = { "OFF", "1 min", "5 min", "15 min", "30 min", "1 h" };
#endif
const char* SettingsScreen::BATT_DISPLAY_LABELS[3] = { "Icon", "%", "V" };
const char* SettingsScreen::SOUND_LABELS[4] = { "Built-in", "M1", "M2", "None" };
const char* SettingsScreen::AD_SCOPE_LABELS[2] = { "All", "Zero-hop" };
#if FEAT_FULL_REFRESH_SETTING
const char* SettingsScreen::EINK_FULL_REFRESH_LABELS[5] = { "OFF", "5", "10", "20", "30" };
#endif
+44 -209
View File
@@ -1,6 +1,7 @@
#include "UITask.h"
#include "../ui-core/SoundNotifier.h"
#include "../ui-core/SoundControl.h"
#include "../ui-core/Telemetry.h"
#include <helpers/TxtDataHelpers.h>
#include "../MyMesh.h"
#include "../MsgExpand.h"
@@ -161,8 +162,9 @@ static const int QUICK_MSGS_MAX = 10;
#include "FullscreenMsgView.h"
#include "../ui-core/MessageText.h"
#include "SensorPlaceholders.h"
#include "SettingsScreen.h"
#include "../ui-core/UiCore.h" // shared UI Core: history rings + unread models (MessagesScreen views them)
#include "../ui-core/SettingsSchema.h" // the settings both frontends share (SettingsScreen.h)
#include "SettingsScreen.h"
#include "MessagesScreen.h"
// ── Custom screens (separate files to ease upstream merges) ───────────────────
@@ -273,15 +275,14 @@ static int drawClockTime(DisplayDriver& d, int top_y, const struct tm* ti,
// ── HomeScreen ────────────────────────────────────────────────────────────────
// Forward declaration to be able to call formatDashVal from HomeScreen::render()
static void formatDashVal(uint8_t field, char* val, int val_len, uint16_t batt_mv,
uint16_t low_batt_mv, int unread, bool unread_overflow, bool imperial, CayenneLPP* lpp = nullptr);
static void formatDashVal(uint8_t field, char* val, int val_len, uint16_t batt_mv, uint16_t low_batt_mv,
int unread, bool unread_overflow, bool imperial, CayenneLPP* lpp, bool nouns);
// Altitude (baro or GPS) respects Settings > System > Units, same as every
// other distance in the UI -- unlike geo::fmtDist, never switches to km/mi
// regardless of magnitude, since altitude is always discussed in the small unit.
// Telemetry (ui-core/Telemetry.h) in this display's font and width.
static const telemetry::Style L1_TELEMETRY = { "\xf8", false, false, 3 };
// Altitude (baro or GPS) in Settings > System > Units, always the small unit.
static void fmtAlt(char* buf, int n, float meters, bool imperial) {
if (imperial) snprintf(buf, n, "%.0fft", meters * 3.28084f);
else snprintf(buf, n, "%.0fm", meters);
telemetry::altText(meters, imperial, L1_TELEMETRY, buf, n);
}
class HomeScreen : public UIScreen {
@@ -843,96 +844,13 @@ public:
const int FIELD_Y[3] = { dash0, dash0 + step, dash0 + step * 2 };
for (int fi = 0; fi < 3; fi++) {
uint8_t field = _node_prefs->dashboard_fields[fi];
if (field == DASH_NONE) continue;
if (field == telemetry::NONE) continue;
char label[10], val[20];
label[0] = '\0';
val[0] = '\0';
if (field == DASH_BATT_V) {
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_BATT_PCT) {
strcpy(label, "Batt");
uint16_t mv = _task->getBattMilliVolts();
if (mv > 0) snprintf(val, sizeof(val), "%d%%",
battMvToPercent(mv, _node_prefs->low_batt_mv));
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_SATS) {
strcpy(label, "Sats");
#if ENV_INCLUDE_GPS == 1
LocationProvider* loc = sensors.getLocationProvider();
if (loc) snprintf(val, sizeof(val), "%ld", loc->satellitesCount());
else strcpy(val, "--");
#else
strcpy(val, "--");
#endif
} else if (field == DASH_ALT_GPS) {
strcpy(label, "AltG");
#if ENV_INCLUDE_GPS == 1
LocationProvider* loc = sensors.getLocationProvider();
if (loc && loc->isValid())
fmtAlt(val, sizeof(val), loc->getAltitude() / 1000.0f, _node_prefs && _node_prefs->units_imperial);
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 if (field == DASH_MSGS) {
strcpy(label, "Msgs");
int unread = _task->getDMUnreadTotal() + _task->getChannelUnreadCount() + _task->getRoomUnreadCount();
// "+" when the true total is understated -- at least one unread
// message has already been evicted off a ring before ever being
// seen (see UITask::getAnyUnreadOverflow()).
snprintf(val, sizeof(val), _task->getAnyUnreadOverflow() ? "%d+" : "%d", unread);
} 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); fmtAlt(val, sizeof(val), v, _node_prefs && _node_prefs->units_imperial); 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, "--");
}
const char* label = telemetry::LABEL[field < telemetry::COUNT ? field : telemetry::NONE];
char val[20];
formatDashVal(field, val, sizeof(val), _task->getBattMilliVolts(), _node_prefs->low_batt_mv,
_task->getDMUnreadTotal() + _task->getChannelUnreadCount() + _task->getRoomUnreadCount(),
_task->getAnyUnreadOverflow(), _node_prefs->units_imperial, &sensors_lpp, false);
if (val[0] && label[0]) {
display.setColor(DisplayDriver::LIGHT);
@@ -972,18 +890,18 @@ public:
CayenneLPP* lpp_ptr = nullptr;
uint8_t f0 = _node_prefs->dashboard_fields[0], f1 = _node_prefs->dashboard_fields[1];
auto isLPP = [](uint8_t f) {
return f==DASH_TEMP||f==DASH_HUM||f==DASH_PRES||f==DASH_ALT||f==DASH_LUX||f==DASH_CO2;
return f==telemetry::TEMP||f==telemetry::HUM||f==telemetry::PRES||f==telemetry::ALT||f==telemetry::LUX||f==telemetry::CO2;
};
if (isLPP(f0) || isLPP(f1)) {
sensors_lpp.reset(); sensors.querySensors(0xFF, sensors_lpp); lpp_ptr = &sensors_lpp;
}
bool show_msgs = f0 == DASH_MSGS || f1 == DASH_MSGS;
bool show_msgs = f0 == telemetry::MSGS || f1 == telemetry::MSGS;
int unread = show_msgs
? _task->getDMUnreadTotal() + _task->getChannelUnreadCount() + _task->getRoomUnreadCount() : 0;
bool unread_overflow = show_msgs && _task->getAnyUnreadOverflow();
uint16_t batt_mv = _task->getBattMilliVolts();
formatDashVal(f0, v0, sizeof(v0), batt_mv, _node_prefs->low_batt_mv, unread, unread_overflow, _node_prefs->units_imperial, lpp_ptr);
formatDashVal(f1, v1, sizeof(v1), batt_mv, _node_prefs->low_batt_mv, unread, unread_overflow, _node_prefs->units_imperial, lpp_ptr);
formatDashVal(f0, v0, sizeof(v0), batt_mv, _node_prefs->low_batt_mv, unread, unread_overflow, _node_prefs->units_imperial, lpp_ptr, true);
formatDashVal(f1, v1, sizeof(v1), batt_mv, _node_prefs->low_batt_mv, unread, unread_overflow, _node_prefs->units_imperial, lpp_ptr, true);
if (v0[0] || v1[0]) {
int sv_y = date_y + step;
display.setColor(DisplayDriver::LIGHT);
@@ -1141,25 +1059,7 @@ public:
char buf[22] = "--";
while (r.readHeader(ch, type)) {
if (type == target) {
float v, v2, v3;
switch (type) {
case LPP_GPS:
r.readGPS(v, v2, v3);
if (v != 0 || v2 != 0) snprintf(buf, sizeof(buf), "%.4f %.4f", v, v2);
break;
case LPP_VOLTAGE: r.readVoltage(v); snprintf(buf, sizeof(buf), "%.2fV", v); break;
case LPP_CURRENT: r.readCurrent(v); snprintf(buf, sizeof(buf), "%.3fA", v); break;
case LPP_POWER: r.readPower(v); snprintf(buf, sizeof(buf), "%.1fW", v); break;
case LPP_TEMPERATURE:r.readTemperature(v); snprintf(buf, sizeof(buf), "%.1f\xf8""C", v); break;
case LPP_RELATIVE_HUMIDITY: r.readRelativeHumidity(v); snprintf(buf, sizeof(buf), "%.0f%%", v); break;
case LPP_BAROMETRIC_PRESSURE: r.readPressure(v); snprintf(buf, sizeof(buf), "%.1fhPa", v); break;
case LPP_ALTITUDE: r.readAltitude(v); snprintf(buf, sizeof(buf), "%.0fm", v); break;
case LPP_LUMINOSITY: r.readLuminosity(v); snprintf(buf, sizeof(buf), "%.0flux", v); break;
case LPP_PERCENTAGE: r.readPercentage(v); snprintf(buf, sizeof(buf), "%.0f%%", v); break;
case LPP_DISTANCE: r.readDistance(v); snprintf(buf, sizeof(buf), "%.2fm", v); break;
case LPP_CONCENTRATION: r.readConcentration(v); snprintf(buf, sizeof(buf), "%.0fppm", v); break;
default: r.skipData(type); continue;
}
telemetry::lppText(r, type, _node_prefs && _node_prefs->units_imperial, L1_TELEMETRY, buf, sizeof(buf));
break;
}
r.skipData(type);
@@ -2101,85 +2001,31 @@ bool UITask::isButtonPressed() const {
#endif
}
static void formatDashVal(uint8_t field, char* val, int val_len, uint16_t batt_mv,
uint16_t low_batt_mv, int unread, bool unread_overflow, bool imperial, CayenneLPP* lpp) {
// A dashboard field in this display's font (ui-core/Telemetry.h); `nouns`
// when it stands without its label (the lock screen). "" for none.
static void formatDashVal(uint8_t field, char* val, int val_len, uint16_t batt_mv, uint16_t low_batt_mv,
int unread, bool unread_overflow, bool imperial, CayenneLPP* lpp, bool nouns) {
val[0] = '\0';
switch (field) {
case DASH_NONE: return;
case DASH_BATT_V:
if (batt_mv > 0) snprintf(val, val_len, "%u.%02uV", batt_mv/1000, (batt_mv%1000)/10);
else strcpy(val, "--");
return;
case DASH_BATT_PCT:
if (batt_mv > 0) snprintf(val, val_len, "%d%%", battMvToPercent(batt_mv, low_batt_mv));
else strcpy(val, "--");
return;
case DASH_NODES:
snprintf(val, val_len, "%d nodes", the_mesh.getNumContacts());
return;
case DASH_MSGS:
snprintf(val, val_len, unread_overflow ? "%d+ msgs" : "%d msgs", unread);
return;
if (field == telemetry::NONE || field >= telemetry::COUNT) return;
telemetry::Style st = L1_TELEMETRY;
st.nouns = nouns;
if (nouns) st.gps_dp = 2; // no label: room for the position is shorter
telemetry::Inputs in;
in.batt_mv = batt_mv;
in.low_batt_mv = low_batt_mv;
in.nodes = the_mesh.getNumContacts();
in.unread = unread;
in.unread_more = unread_overflow;
in.imperial = imperial;
#if ENV_INCLUDE_GPS == 1
case DASH_GPS: {
LocationProvider* loc = sensors.getLocationProvider();
if (loc && loc->isValid())
snprintf(val, val_len, "%.2f %.2f",
loc->getLatitude()/1000000.0f, loc->getLongitude()/1000000.0f);
else strcpy(val, "no fix");
return;
}
case DASH_SATS: {
LocationProvider* loc = sensors.getLocationProvider();
if (loc) snprintf(val, val_len, "%ld sats", loc->satellitesCount());
else strcpy(val, "--");
return;
}
case DASH_ALT_GPS: {
LocationProvider* loc = sensors.getLocationProvider();
if (loc && loc->isValid())
fmtAlt(val, val_len, loc->getAltitude() / 1000.0f, imperial);
else strcpy(val, "no fix");
return;
}
#else
case DASH_SATS:
case DASH_ALT_GPS:
strcpy(val, "--");
return;
in.loc = sensors.getLocationProvider();
#endif
default: break;
}
// LPP sensor fields
uint8_t lpp_type = 0;
switch (field) {
case DASH_TEMP: lpp_type = LPP_TEMPERATURE; break;
case DASH_HUM: lpp_type = LPP_RELATIVE_HUMIDITY; break;
case DASH_PRES: lpp_type = LPP_BAROMETRIC_PRESSURE; break;
case DASH_ALT: lpp_type = LPP_ALTITUDE; break;
case DASH_LUX: lpp_type = LPP_LUMINOSITY; break;
case DASH_CO2: lpp_type = LPP_CONCENTRATION; break;
}
if (lpp_type) {
if (telemetry::isSensor(field)) {
if (!lpp) { static CayenneLPP s_lpp(200); s_lpp.reset(); sensors.querySensors(0xFF, s_lpp); lpp = &s_lpp; }
LPPReader r(lpp->getBuffer(), 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, val_len, "%.1f\xf8""C", v); return;
case LPP_RELATIVE_HUMIDITY: r.readRelativeHumidity(v); snprintf(val, val_len, "%.0f%%", v); return;
case LPP_BAROMETRIC_PRESSURE: r.readPressure(v); snprintf(val, val_len, "%.0fhPa", v); return;
case LPP_ALTITUDE: r.readAltitude(v); fmtAlt(val, val_len, v, imperial); return;
case LPP_LUMINOSITY: r.readLuminosity(v); snprintf(val, val_len, "%.0flux", v); return;
case LPP_CONCENTRATION: r.readConcentration(v); snprintf(val, val_len, "%.0fppm", v); return;
}
}
r.skipData(type);
}
strcpy(val, "--");
in.lpp = lpp->getBuffer();
in.lpp_len = lpp->getSize();
}
telemetry::text(field, in, st, val, val_len);
}
void UITask::enqueueKey(char c) {
@@ -3208,15 +3054,6 @@ void UITask::applyApc() {
radioctl::applyApc();
}
#if ENV_INCLUDE_GPS == 1
void UITask::applyGpsInterval() {
if (_node_prefs == NULL || _sensors == NULL) return;
char buf[12];
sprintf(buf, "%u", _node_prefs->gps_interval);
_sensors->setSettingValue("gps_interval", buf);
}
#endif
void UITask::applyRadioParams() {
if (_node_prefs == NULL) return;
radioctl::applyParams(); // companion params, or the repeater profile if relaying with one set
@@ -3245,12 +3082,10 @@ void UITask::applyFullRefreshInterval() {
}
}
void UITask::setBrightnessLevel(uint8_t level) {
if (_node_prefs == NULL) return;
if (level > 4) level = 4;
_node_prefs->display_brightness = level;
applyBrightness();
_next_refresh = 0;
void UITask::applySoundPrefs() {
#ifdef PIN_BUZZER
if (_node_prefs) setBuzzerVolumeLevel(_node_prefs->buzzer_volume); // with a sample at the new level
#endif
}
void UITask::setBuzzerVolumeLevel(uint8_t level) {
+3 -6
View File
@@ -437,17 +437,14 @@ public:
void setGpioMode(int idx, uint8_t mode); // 0=Off 1=In 2=Out-low 3=Out-high 4=Analog; applies + persists
void applyAllGpioModes(); // boot-time restore from NodePrefs, called from begin()
void applyBrightness();
void setBrightnessLevel(uint8_t level);
uint8_t getBrightnessLevel() const { return _node_prefs ? _node_prefs->display_brightness : 2; }
// Settings changed through the schema (ui-core/SettingsSchema.h).
void applyDisplayPrefs() override { applyBrightness(); _next_refresh = 0; }
void applySoundPrefs() override;
void setBuzzerVolumeLevel(uint8_t level);
uint8_t getBuzzerVolume() const { return _node_prefs ? _node_prefs->buzzer_volume : 4; }
void applyTxPower();
void applyPowerSave(); // hardware duty-cycle RX on/off from prefs
void applyApc(); // Adaptive Power Control on/off from prefs
void applyRadioParams(); // freq/bw/sf/cr from prefs (radio preset change)
#if ENV_INCLUDE_GPS == 1
void applyGpsInterval(); // GPS duty-cycle sleep window from prefs
#endif
// Save-on-exit helper for the screen `_dirty` pattern: persists NodePrefs once
// only if `dirty`, then clears the flag. Standardises the screens' exit paths
// (some used to leave the flag set, relying on onShow() to reset it) and keeps