mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-09 03:06:39 +00:00
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:
@@ -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
|
||||
@@ -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 ──
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user