From b355922d32ef96d05c75f7e3e6a70e9f592bdf17 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Mon, 28 Sep 2026 23:19:47 +0200 Subject: [PATCH] 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 --- docs/development/plan-next.md | 10 +- .../companion_radio/ui-core/SettingsSchema.h | 46 +- examples/companion_radio/ui-core/Telemetry.h | 138 ++++++ examples/companion_radio/ui-lvgl/HomeScreen.h | 87 +--- examples/companion_radio/ui-lvgl/UITask.cpp | 22 +- examples/companion_radio/ui-lvgl/UITask.h | 1 + .../ui-new/DashboardConfigScreen.h | 28 +- .../companion_radio/ui-new/MessagesScreen.h | 53 +-- .../companion_radio/ui-new/RepeaterScreen.h | 10 +- .../companion_radio/ui-new/SettingsScreen.h | 404 ++++-------------- examples/companion_radio/ui-new/UITask.cpp | 253 ++--------- examples/companion_radio/ui-new/UITask.h | 9 +- 12 files changed, 375 insertions(+), 686 deletions(-) create mode 100644 examples/companion_radio/ui-core/Telemetry.h diff --git a/docs/development/plan-next.md b/docs/development/plan-next.md index ca30ab1e..cea4cc19 100644 --- a/docs/development/plan-next.md +++ b/docs/development/plan-next.md @@ -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 diff --git a/examples/companion_radio/ui-core/SettingsSchema.h b/examples/companion_radio/ui-core/SettingsSchema.h index 06310ddd..1c83c02d 100644 --- a/examples/companion_radio/ui-core/SettingsSchema.h +++ b/examples/companion_radio/ui-core/SettingsSchema.h @@ -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 diff --git a/examples/companion_radio/ui-core/Telemetry.h b/examples/companion_radio/ui-core/Telemetry.h new file mode 100644 index 00000000..1a75473f --- /dev/null +++ b/examples/companion_radio/ui-core/Telemetry.h @@ -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 +#include +#include +#include +#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 diff --git a/examples/companion_radio/ui-lvgl/HomeScreen.h b/examples/companion_radio/ui-lvgl/HomeScreen.h index d9563009..6eb00afe 100644 --- a/examples/companion_radio/ui-lvgl/HomeScreen.h +++ b/examples/companion_radio/ui-lvgl/HomeScreen.h @@ -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 ── diff --git a/examples/companion_radio/ui-lvgl/UITask.cpp b/examples/companion_radio/ui-lvgl/UITask.cpp index 5c34eb4f..eab5bbd8 100644 --- a/examples/companion_radio/ui-lvgl/UITask.cpp +++ b/examples/companion_radio/ui-lvgl/UITask.cpp @@ -73,6 +73,7 @@ template 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 diff --git a/examples/companion_radio/ui-lvgl/UITask.h b/examples/companion_radio/ui-lvgl/UITask.h index 826f76ff..97cb109b 100644 --- a/examples/companion_radio/ui-lvgl/UITask.h +++ b/examples/companion_radio/ui-lvgl/UITask.h @@ -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; diff --git a/examples/companion_radio/ui-new/DashboardConfigScreen.h b/examples/companion_radio/ui-new/DashboardConfigScreen.h index 55c4b96a..d46b202c 100644 --- a/examples/companion_radio/ui-new/DashboardConfigScreen.h +++ b/examples/companion_radio/ui-new/DashboardConfigScreen.h @@ -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)" -}; diff --git a/examples/companion_radio/ui-new/MessagesScreen.h b/examples/companion_radio/ui-new/MessagesScreen.h index 1e02f0af..66228c69 100644 --- a/examples/companion_radio/ui-new/MessagesScreen.h +++ b/examples/companion_radio/ui-new/MessagesScreen.h @@ -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 diff --git a/examples/companion_radio/ui-new/RepeaterScreen.h b/examples/companion_radio/ui-new/RepeaterScreen.h index 1375ef87..4b5e13e8 100644 --- a/examples/companion_radio/ui-new/RepeaterScreen.h +++ b/examples/companion_radio/ui-new/RepeaterScreen.h @@ -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; } diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index 63b06e74..3185143a 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -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 diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 1b7613d2..925cd15c 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -1,6 +1,7 @@ #include "UITask.h" #include "../ui-core/SoundNotifier.h" #include "../ui-core/SoundControl.h" +#include "../ui-core/Telemetry.h" #include #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) { diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 8d2cdba8..e1cdeab6 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -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