feat(ui): status-bar mini-icons + settings fixes

- Replace single-letter M/B/A/G top-bar indicators with scalable mini-icons:
  ICON_MUTE (speaker+cross), ICON_BLUETOOTH (rune), ICON_ADVERT (broadcast
  mast+waves), ICON_TRAIL (map pin). Centred in the cw+2 indicator box;
  disconnected BT shows the plain glyph instead of lowercase b.
- fix(settings): right-side values used display.valCol() without the scrollbar
  reserve, so after expanding a section they rendered under the indicator.
  Route all value cursors through valCol(display) = display.valCol() - _reserve.
- fix(settings): font toggle used ^=1, which on a stale value of 2 (older Hybrid
  build) flips 2<->3 — both nonzero, locking applyFont() on Lemon with no way
  back. Normalise: use_lemon_font = use_lemon_font ? 0 : 1.
- docs(settings): document Messages > Resend setting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-06-15 16:45:28 +02:00
parent 6e4177f3d6
commit 353dee5878
4 changed files with 77 additions and 38 deletions

View File

@@ -93,4 +93,8 @@ Lists all available home screen pages. For each entry:
### Messages ### Messages
| Setting | Options | Notes |
| ------- | -------------- | ---------------------------------------------------------------------------------------------- |
| Resend | off / 1×5× | Auto-resend an on-device direct message this many times when no delivery ACK is received (default 2×) |
Up to 10 quick reply templates (Q1Q10). Press **Enter** on a slot to open the keyboard editor. Supports the same placeholders as the main keyboard (`{time}`, `{loc}`, and sensor placeholders when connected). Up to 10 quick reply templates (Q1Q10). Press **Enter** on a slot to open the keyboard editor. Supports the same placeholders as the main keyboard (`{time}`, `{loc}`, and sensor placeholders when connected).

View File

@@ -111,6 +111,10 @@ class SettingsScreen : public UIScreen {
return 0; return 0;
} }
// Value column start, pulled left by the scrollbar gutter so right-side
// values never render under the indicator when the list scrolls.
int valCol(DisplayDriver& display) const { return display.valCol() - _reserve; }
void renderBar(DisplayDriver& display, int x, int y, int value, int max_val) { void renderBar(DisplayDriver& display, int x, int y, int value, int max_val) {
const int gap = 2; const int gap = 2;
const int avail = display.width() - x - _reserve; const int avail = display.width() - x - _reserve;
@@ -419,12 +423,12 @@ class SettingsScreen : public UIScreen {
#if FEAT_BRIGHTNESS_SETTING #if FEAT_BRIGHTNESS_SETTING
if (item == BRIGHTNESS) { if (item == BRIGHTNESS) {
display.print("Bright"); display.print("Bright");
renderBar(display, display.valCol(), y, (p ? p->display_brightness : 2) + 1, 5); renderBar(display, valCol(display), y, (p ? p->display_brightness : 2) + 1, 5);
} else } else
#endif #endif
if (item == BUZZER) { if (item == BUZZER) {
display.print("Buzzer"); display.print("Buzzer");
display.setCursor(display.valCol(), y); display.setCursor(valCol(display), y);
#ifdef PIN_BUZZER #ifdef PIN_BUZZER
{ static const char* labels[] = { "ON", "OFF", "Auto" }; { static const char* labels[] = { "ON", "OFF", "Auto" };
int m = _task->getBuzzerMode(); int m = _task->getBuzzerMode();
@@ -435,29 +439,29 @@ class SettingsScreen : public UIScreen {
} else if (item == BUZZER_VOLUME) { } else if (item == BUZZER_VOLUME) {
display.print("BzrVol"); display.print("BzrVol");
#ifdef PIN_BUZZER #ifdef PIN_BUZZER
renderBar(display, display.valCol(), y, _task->getBuzzerVolume() + 1, 5); renderBar(display, valCol(display), y, _task->getBuzzerVolume() + 1, 5);
#else #else
display.setCursor(display.valCol(), y); display.setCursor(valCol(display), y);
display.print("N/A"); display.print("N/A");
#endif #endif
} else if (item == DM_MELODY) { } else if (item == DM_MELODY) {
display.print("DM sound"); display.print("DM sound");
display.setCursor(display.valCol(), y); display.setCursor(valCol(display), y);
{ uint8_t v = p ? p->notif_melody_dm : 0; { uint8_t v = p ? p->notif_melody_dm : 0;
display.print(SOUND_LABELS[v < SOUND_COUNT ? v : 0]); } display.print(SOUND_LABELS[v < SOUND_COUNT ? v : 0]); }
} else if (item == CH_MELODY) { } else if (item == CH_MELODY) {
display.print("Ch sound"); display.print("Ch sound");
display.setCursor(display.valCol(), y); display.setCursor(valCol(display), y);
{ uint8_t v = p ? p->notif_melody_ch : 0; { uint8_t v = p ? p->notif_melody_ch : 0;
display.print(SOUND_LABELS[v < SOUND_COUNT ? v : 0]); } display.print(SOUND_LABELS[v < SOUND_COUNT ? v : 0]); }
} else if (item == AD_SOUND) { } else if (item == AD_SOUND) {
display.print("AD sound"); display.print("AD sound");
display.setCursor(display.valCol(), y); display.setCursor(valCol(display), y);
{ uint8_t v = p ? p->notif_melody_ad : 0; { uint8_t v = p ? p->notif_melody_ad : 0;
display.print(SOUND_LABELS[v < SOUND_COUNT ? v : 0]); } display.print(SOUND_LABELS[v < SOUND_COUNT ? v : 0]); }
} else if (item == AD_SOUND_SCOPE) { } else if (item == AD_SOUND_SCOPE) {
display.print("AD scope"); display.print("AD scope");
display.setCursor(display.valCol(), y); display.setCursor(valCol(display), y);
{ uint8_t v = p ? p->advert_sound_scope : ADVERT_SOUND_SCOPE_ALL; { uint8_t v = p ? p->advert_sound_scope : ADVERT_SOUND_SCOPE_ALL;
display.print(AD_SCOPE_LABELS[v < AD_SCOPE_COUNT ? v : 0]); } display.print(AD_SCOPE_LABELS[v < AD_SCOPE_COUNT ? v : 0]); }
} else if (isHomePage(item)) { } else if (isHomePage(item)) {
@@ -477,25 +481,25 @@ class SettingsScreen : public UIScreen {
display.print("TX Pwr"); display.print("TX Pwr");
char buf[8]; char buf[8];
snprintf(buf, sizeof(buf),"%ddBm", p ? p->tx_power_dbm : 0); snprintf(buf, sizeof(buf),"%ddBm", p ? p->tx_power_dbm : 0);
display.setCursor(display.valCol(), y); display.setCursor(valCol(display), y);
display.print(buf); display.print(buf);
} else if (item == POWER_SAVE) { } else if (item == POWER_SAVE) {
display.print("Pwr save"); display.print("Pwr save");
display.setCursor(display.valCol(), y); display.setCursor(valCol(display), y);
display.print((p && p->rx_powersave) ? "ON" : "OFF"); display.print((p && p->rx_powersave) ? "ON" : "OFF");
} else if (item == TX_APC) { } else if (item == TX_APC) {
display.print("Auto pwr"); display.print("Auto pwr");
display.setCursor(display.valCol(), y); display.setCursor(valCol(display), y);
display.print((p && p->tx_apc) ? "ON" : "OFF"); display.print((p && p->tx_apc) ? "ON" : "OFF");
#if AUTO_OFF_MILLIS > 0 #if AUTO_OFF_MILLIS > 0
} else if (item == AUTO_OFF) { } else if (item == AUTO_OFF) {
display.print("AutoOff"); display.print("AutoOff");
display.setCursor(display.valCol(), y); display.setCursor(valCol(display), y);
display.print(AUTO_OFF_LABELS[autoOffIndex()]); display.print(AUTO_OFF_LABELS[autoOffIndex()]);
#endif #endif
} else if (item == AUTO_LOCK) { } else if (item == AUTO_LOCK) {
display.print("AutoLock"); display.print("AutoLock");
display.setCursor(display.valCol(), y); display.setCursor(valCol(display), y);
display.print((p && p->auto_lock) ? "ON" : "OFF"); display.print((p && p->auto_lock) ? "ON" : "OFF");
} else if (item == TIMEZONE) { } else if (item == TIMEZONE) {
display.print("TimeZone"); display.print("TimeZone");
@@ -503,39 +507,39 @@ class SettingsScreen : public UIScreen {
int8_t tz = p ? p->tz_offset_hours : 0; int8_t tz = p ? p->tz_offset_hours : 0;
if (tz >= 0) snprintf(buf, sizeof(buf),"UTC+%d", (int)tz); if (tz >= 0) snprintf(buf, sizeof(buf),"UTC+%d", (int)tz);
else snprintf(buf, sizeof(buf),"UTC%d", (int)tz); else snprintf(buf, sizeof(buf),"UTC%d", (int)tz);
display.setCursor(display.valCol(), y); display.setCursor(valCol(display), y);
display.print(buf); display.print(buf);
} else if (item == LOW_BAT) { } else if (item == LOW_BAT) {
display.print("LowBat"); display.print("LowBat");
display.setCursor(display.valCol(), y); display.setCursor(valCol(display), y);
display.print(LOW_BAT_LABELS[lowBatIndex()]); display.print(LOW_BAT_LABELS[lowBatIndex()]);
} else if (item == UNITS) { } else if (item == UNITS) {
display.print("Units"); display.print("Units");
display.setCursor(display.valCol(), y); display.setCursor(valCol(display), y);
display.print((p && p->units_imperial) ? "Imperial" : "Metric"); display.print((p && p->units_imperial) ? "Imperial" : "Metric");
} else if (item == BATT_DISPLAY) { } else if (item == BATT_DISPLAY) {
display.print("BattDisp"); display.print("BattDisp");
display.setCursor(display.valCol(), y); display.setCursor(valCol(display), y);
uint8_t mode = p ? p->batt_display_mode : 0; uint8_t mode = p ? p->batt_display_mode : 0;
display.print(BATT_DISPLAY_LABELS[mode < BATT_DISPLAY_COUNT ? mode : 0]); display.print(BATT_DISPLAY_LABELS[mode < BATT_DISPLAY_COUNT ? mode : 0]);
#if FEAT_CLOCK_SECONDS_SETTING #if FEAT_CLOCK_SECONDS_SETTING
} else if (item == CLOCK_SECONDS) { } else if (item == CLOCK_SECONDS) {
display.print("Seconds"); display.print("Seconds");
display.setCursor(display.valCol(), y); display.setCursor(valCol(display), y);
display.print((p && p->clock_hide_seconds) ? "OFF" : "ON"); display.print((p && p->clock_hide_seconds) ? "OFF" : "ON");
#endif #endif
} else if (item == CLOCK_FORMAT) { } else if (item == CLOCK_FORMAT) {
display.print("Format"); display.print("Format");
display.setCursor(display.valCol(), y); display.setCursor(valCol(display), y);
display.print((p && p->clock_12h) ? "12h" : "24h"); display.print((p && p->clock_12h) ? "12h" : "24h");
} else if (item == FONT) { } else if (item == FONT) {
display.print("Font"); display.print("Font");
display.setCursor(display.valCol(), y); display.setCursor(valCol(display), y);
display.print((p && p->use_lemon_font) ? "Lemon" : "Default"); display.print((p && p->use_lemon_font) ? "Lemon" : "Default");
#if FEAT_DISPLAY_ROTATION_SETTING #if FEAT_DISPLAY_ROTATION_SETTING
} else if (item == ROTATION) { } else if (item == ROTATION) {
display.print("Rotation"); display.print("Rotation");
display.setCursor(display.valCol(), y); display.setCursor(valCol(display), y);
{ static const char* ROT_LABELS[] = { "0 deg", "90 deg", "180 deg", "270 deg" }; { static const char* ROT_LABELS[] = { "0 deg", "90 deg", "180 deg", "270 deg" };
uint8_t r = p ? (p->display_rotation & 3) : 0; uint8_t r = p ? (p->display_rotation & 3) : 0;
display.print(ROT_LABELS[r]); } display.print(ROT_LABELS[r]); }
@@ -543,7 +547,7 @@ class SettingsScreen : public UIScreen {
#if FEAT_JOYSTICK_ROTATION_SETTING #if FEAT_JOYSTICK_ROTATION_SETTING
} else if (item == JOY_ROTATION) { } else if (item == JOY_ROTATION) {
display.print("Joystick"); display.print("Joystick");
display.setCursor(display.valCol(), y); display.setCursor(valCol(display), y);
{ static const char* ROT_LABELS[] = { "0 deg", "90 deg", "180 deg", "270 deg" }; { static const char* ROT_LABELS[] = { "0 deg", "90 deg", "180 deg", "270 deg" };
uint8_t r = p ? (p->joystick_rotation & 3) : 0; uint8_t r = p ? (p->joystick_rotation & 3) : 0;
display.print(ROT_LABELS[r]); } display.print(ROT_LABELS[r]); }
@@ -551,26 +555,26 @@ class SettingsScreen : public UIScreen {
#if FEAT_FULL_REFRESH_SETTING #if FEAT_FULL_REFRESH_SETTING
} else if (item == EINK_FULL_REFRESH) { } else if (item == EINK_FULL_REFRESH) {
display.print("Full rfsh"); display.print("Full rfsh");
display.setCursor(display.valCol(), y); display.setCursor(valCol(display), y);
{ uint8_t idx = p ? p->eink_full_refresh_every : 0; { uint8_t idx = p ? p->eink_full_refresh_every : 0;
if (idx >= EINK_FULL_REFRESH_COUNT) idx = 0; if (idx >= EINK_FULL_REFRESH_COUNT) idx = 0;
display.print(EINK_FULL_REFRESH_LABELS[idx]); } display.print(EINK_FULL_REFRESH_LABELS[idx]); }
#endif #endif
} else if (item == DM_FILTER) { } else if (item == DM_FILTER) {
display.print("DM"); display.print("DM");
display.setCursor(display.valCol(), y); display.setCursor(valCol(display), y);
display.print((p && p->dm_show_all) ? "all" : "fav"); display.print((p && p->dm_show_all) ? "all" : "fav");
} else if (item == CH_FILTER) { } else if (item == CH_FILTER) {
display.print("Channels"); display.print("Channels");
display.setCursor(display.valCol(), y); display.setCursor(valCol(display), y);
display.print((p && p->ch_fav_only) ? "fav" : "all"); display.print((p && p->ch_fav_only) ? "fav" : "all");
} else if (item == ROOM_FILTER) { } else if (item == ROOM_FILTER) {
display.print("Rooms"); display.print("Rooms");
display.setCursor(display.valCol(), y); display.setCursor(valCol(display), y);
display.print((p && p->room_fav_only) ? "fav" : "all"); display.print((p && p->room_fav_only) ? "fav" : "all");
} else if (item == DM_RESEND) { } else if (item == DM_RESEND) {
display.print("Resend"); display.print("Resend");
display.setCursor(display.valCol(), y); display.setCursor(valCol(display), y);
uint8_t n = p ? p->dm_resend_count : 0; uint8_t n = p ? p->dm_resend_count : 0;
if (n == 0) display.print("OFF"); if (n == 0) display.print("OFF");
else { char buf[6]; snprintf(buf, sizeof(buf), "%ux", (unsigned)n); display.print(buf); } else { char buf[6]; snprintf(buf, sizeof(buf), "%ux", (unsigned)n); display.print(buf); }
@@ -806,7 +810,9 @@ public:
return true; return true;
} }
if (_selected == FONT && p && (left || right || enter)) { if (_selected == FONT && p && (left || right || enter)) {
p->use_lemon_font ^= 1; // Normalise (not ^=1): a stale value >1 from an older build with extra
// font modes would otherwise toggle 2<->3 and stay stuck on Lemon.
p->use_lemon_font = p->use_lemon_font ? 0 : 1;
_task->applyFont(); _task->applyFont();
_dirty = true; _dirty = true;
return true; return true;

View File

@@ -445,8 +445,7 @@ class HomeScreen : public UIScreen {
int mx = battLeftX - ind - ind_gap; int mx = battLeftX - ind - ind_gap;
display.fillRect(mx, 0, ind, ind_h); display.fillRect(mx, 0, ind, ind_h);
display.setColor(DisplayDriver::DARK); display.setColor(DisplayDriver::DARK);
display.setCursor(mx + 1, 0); miniIconDraw(display, mx + (ind - ICON_MUTE.w * miniIconScale(display)) / 2, 0, ICON_MUTE);
display.print("M");
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
battLeftX = mx; battLeftX = mx;
} }
@@ -456,16 +455,15 @@ class HomeScreen : public UIScreen {
int leftmostX = battLeftX; int leftmostX = battLeftX;
if (_task->isSerialEnabled()) { if (_task->isSerialEnabled()) {
int btX = battLeftX - ind - ind_gap; int btX = battLeftX - ind - ind_gap;
int btIconX = btX + (ind - ICON_BLUETOOTH.w * miniIconScale(display)) / 2;
if (_task->isBLEConnected()) { // BT icon reflects BLE link, not USB if (_task->isBLEConnected()) { // BT icon reflects BLE link, not USB
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.fillRect(btX, 0, ind, ind_h); display.fillRect(btX, 0, ind, ind_h);
display.setColor(DisplayDriver::DARK); display.setColor(DisplayDriver::DARK);
display.setCursor(btX + 1, 0); miniIconDraw(display, btIconX, 0, ICON_BLUETOOTH);
display.print("B");
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
} else { } else {
display.setCursor(btX + 1, 0); miniIconDraw(display, btIconX, 0, ICON_BLUETOOTH); // plain glyph: available, not linked
display.print("b");
} }
leftmostX = btX - ind_gap; leftmostX = btX - ind_gap;
@@ -477,8 +475,7 @@ class HomeScreen : public UIScreen {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.fillRect(aX, 0, ind, ind_h); display.fillRect(aX, 0, ind, ind_h);
display.setColor(DisplayDriver::DARK); display.setColor(DisplayDriver::DARK);
display.setCursor(aX + 1, 0); miniIconDraw(display, aX + (ind - ICON_ADVERT.w * miniIconScale(display)) / 2, 0, ICON_ADVERT);
display.print("A");
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
} }
leftmostX = aX - 1; leftmostX = aX - 1;
@@ -492,8 +489,7 @@ class HomeScreen : public UIScreen {
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
display.fillRect(gX, 0, ind, ind_h); display.fillRect(gX, 0, ind, ind_h);
display.setColor(DisplayDriver::DARK); display.setColor(DisplayDriver::DARK);
display.setCursor(gX + 1, 0); miniIconDraw(display, gX + (ind - ICON_TRAIL.w * miniIconScale(display)) / 2, 0, ICON_TRAIL);
display.print("G");
display.setColor(DisplayDriver::LIGHT); display.setColor(DisplayDriver::LIGHT);
} }
leftmostX = gX - 1; leftmostX = gX - 1;

View File

@@ -118,6 +118,39 @@ MINI_ICON(ICON_CROSS, 4, // ✗
packRow(".##."), packRow(".##."),
packRow("#..#")); packRow("#..#"));
// Top-bar status glyphs (replace the single-letter M / B / A indicators).
MINI_ICON(ICON_MUTE, 6, // speaker + cross (sound off)
packRow("..#..."),
packRow(".##..."),
packRow("####.#"),
packRow("###.#."),
packRow("####.#"),
packRow(".##..."),
packRow("..#..."));
MINI_ICON(ICON_BLUETOOTH, 5, // ᛒ bluetooth rune
packRow("..#.."),
packRow("..##."),
packRow("#.#.#"),
packRow(".###."),
packRow("#.#.#"),
packRow("..##."),
packRow("..#.."));
MINI_ICON(ICON_ADVERT, 6, // broadcast mast + radiating waves (auto-advert)
packRow("#....#"),
packRow(".#..#."),
packRow("..##.."),
packRow("..##.."),
packRow(".####."),
packRow(".####."));
MINI_ICON(ICON_TRAIL, 6, // map pin / location marker (GPS trail logging)
packRow(".####."),
packRow("######"),
packRow("##..##"),
packRow("######"),
packRow(".####."),
packRow("..##.."));
// Keyboard special-key glyphs. // Keyboard special-key glyphs.
MINI_ICON(ICON_SHIFT, 7, // ⇧ caps MINI_ICON(ICON_SHIFT, 7, // ⇧ caps
packRow("...#..."), packRow("...#..."),