From 4ec5cfdd4c9e8703638787d5c6c94b0dc62fcf3a Mon Sep 17 00:00:00 2001 From: Marcus Kida Date: Wed, 22 Jul 2026 11:06:24 +0200 Subject: [PATCH] Battery header: add "Bars" display mode Extends the battery header setting from a %/V toggle into a 3-state option (Percentage / Voltage / Bars), encoded across two option bits so existing codeplugs keep reading as Percentage. Bars renders a battery-shaped icon (outlined body + terminal nub) with up to 4 segments, each representing 25% of charge, and blinks the fill when the battery is low. Co-Authored-By: Claude Opus 4.8 --- .../application/include/functions/settings.h | 16 ++++++- .../application/source/functions/settings.c | 21 +++++++++ .../user_interface/menuDisplayOptions.c | 36 ++++++++++----- .../source/user_interface/uiUtilities.c | 45 +++++++++++++++++-- 4 files changed, 102 insertions(+), 16 deletions(-) diff --git a/MDUV380_firmware/application/include/functions/settings.h b/MDUV380_firmware/application/include/functions/settings.h index 599d8f5..710999f 100644 --- a/MDUV380_firmware/application/include/functions/settings.h +++ b/MDUV380_firmware/application/include/functions/settings.h @@ -129,7 +129,7 @@ typedef enum { BIT_INVERSE_VIDEO = (SETTINGS_BITS_BANK_0 | (1 << 0)), BIT_PTT_LATCH = (SETTINGS_BITS_BANK_0 | (1 << 1)), - BIT_UNUSED_2 = (SETTINGS_BITS_BANK_0 | (1 << 2)), + BIT_BATTERY_BARS_IN_HEADER = (SETTINGS_BITS_BANK_0 | (1 << 2)), // companion to BIT_BATTERY_VOLTAGE_IN_HEADER: encodes the "Bars" battery display mode BIT_BATTERY_VOLTAGE_IN_HEADER = (SETTINGS_BITS_BANK_0 | (1 << 3)), BIT_SETTINGS_UPDATED = (SETTINGS_BITS_BANK_0 | (1 << 4)), BIT_TX_RX_FREQ_LOCK = (SETTINGS_BITS_BANK_0 | (1 << 5)), @@ -411,6 +411,20 @@ void settingsSetOptionBit(bitfieldOptions_t bit, bool set); bool settingsIsOptionBitSetFromSettings(settingsStruct_t *sets, bitfieldOptions_t bit); bool settingsIsOptionBitSet(bitfieldOptions_t bit); +// How the battery level is shown in the header. Encoded across two option bits +// (BIT_BATTERY_VOLTAGE_IN_HEADER / BIT_BATTERY_BARS_IN_HEADER) for backwards +// compatibility: an unset pair reads as PERCENTAGE, matching legacy behaviour. +typedef enum +{ + BATTERY_UNIT_PERCENTAGE = 0, + BATTERY_UNIT_VOLTAGE, + BATTERY_UNIT_BARS, + NUM_BATTERY_UNIT_DISPLAY +} batteryUnitDisplay_t; + +batteryUnitDisplay_t settingsGetBatteryUnitDisplay(void); +void settingsSetBatteryUnitDisplay(batteryUnitDisplay_t mode); + void settingsSetDirty(void); void settingsSetVFODirty(void); void settingsSaveIfNeeded(bool immediately); diff --git a/MDUV380_firmware/application/source/functions/settings.c b/MDUV380_firmware/application/source/functions/settings.c index 475db67..fedd5ae 100644 --- a/MDUV380_firmware/application/source/functions/settings.c +++ b/MDUV380_firmware/application/source/functions/settings.c @@ -619,6 +619,27 @@ void settingsSetScreenDimLevel(uint8_t level) settingsSetOptionBit(BIT_SCREEN_DIM_2, ((level & 4) != 0)); } +// Battery header display mode is stored across two option bits so existing +// codeplugs (both bits clear) keep reading as BATTERY_UNIT_PERCENTAGE. +batteryUnitDisplay_t settingsGetBatteryUnitDisplay(void) +{ + if (settingsIsOptionBitSet(BIT_BATTERY_BARS_IN_HEADER)) + { + return BATTERY_UNIT_BARS; + } + if (settingsIsOptionBitSet(BIT_BATTERY_VOLTAGE_IN_HEADER)) + { + return BATTERY_UNIT_VOLTAGE; + } + return BATTERY_UNIT_PERCENTAGE; +} + +void settingsSetBatteryUnitDisplay(batteryUnitDisplay_t mode) +{ + settingsSetOptionBit(BIT_BATTERY_VOLTAGE_IN_HEADER, (mode == BATTERY_UNIT_VOLTAGE)); + settingsSetOptionBit(BIT_BATTERY_BARS_IN_HEADER, (mode == BATTERY_UNIT_BARS)); +} + void settingsSetDirty(void) { settingsDirty = true; diff --git a/MDUV380_firmware/application/source/user_interface/menuDisplayOptions.c b/MDUV380_firmware/application/source/user_interface/menuDisplayOptions.c index 902e2fe..72365cd 100644 --- a/MDUV380_firmware/application/source/user_interface/menuDisplayOptions.c +++ b/MDUV380_firmware/application/source/user_interface/menuDisplayOptions.c @@ -314,15 +314,20 @@ static void updateScreen(bool isFirstRun) #if ! defined(PLATFORM_MD9600) case DISPLAY_BATTERY_UNIT_IN_HEADER: leftSide = currentLanguage->battery; - if (settingsIsOptionBitSet(BIT_BATTERY_VOLTAGE_IN_HEADER)) + switch (settingsGetBatteryUnitDisplay()) { - rightSideUnitsPrompt = PROMPT_VOLTS; - rightSideUnitsStr = "V"; - } - else - { - rightSideUnitsPrompt = PROMPT_PERCENT; - rightSideUnitsStr = "%"; + case BATTERY_UNIT_VOLTAGE: + rightSideUnitsPrompt = PROMPT_VOLTS; + rightSideUnitsStr = "V"; + break; + case BATTERY_UNIT_BARS: + // Plain literal -> rightSideVar (voice-safe), like the Screen Dim entry above. + snprintf(rightSideVar, SCREEN_LINE_BUFFER_SIZE, "%s", "Bars"); + break; + default: // BATTERY_UNIT_PERCENTAGE + rightSideUnitsPrompt = PROMPT_PERCENT; + rightSideUnitsStr = "%"; + break; } break; #endif @@ -705,9 +710,12 @@ static void handleEvent(uiEvent_t *ev) #if ! defined(PLATFORM_MD9600) case DISPLAY_BATTERY_UNIT_IN_HEADER: - if (settingsIsOptionBitSet(BIT_BATTERY_VOLTAGE_IN_HEADER) == false) { - settingsSetOptionBit(BIT_BATTERY_VOLTAGE_IN_HEADER, true); + batteryUnitDisplay_t batteryUnit = settingsGetBatteryUnitDisplay(); + if (batteryUnit < (NUM_BATTERY_UNIT_DISPLAY - 1)) + { + settingsSetBatteryUnitDisplay(batteryUnit + 1); + } } break; #endif @@ -928,9 +936,12 @@ static void handleEvent(uiEvent_t *ev) #if ! defined(PLATFORM_MD9600) case DISPLAY_BATTERY_UNIT_IN_HEADER: - if (settingsIsOptionBitSet(BIT_BATTERY_VOLTAGE_IN_HEADER)) { - settingsSetOptionBit(BIT_BATTERY_VOLTAGE_IN_HEADER, false); + batteryUnitDisplay_t batteryUnit = settingsGetBatteryUnitDisplay(); + if (batteryUnit > BATTERY_UNIT_PERCENTAGE) + { + settingsSetBatteryUnitDisplay(batteryUnit - 1); + } } break; #endif @@ -1182,6 +1193,7 @@ static void exitCallback(void *data) #endif #if ! defined(PLATFORM_MD9600) BIT_BATTERY_VOLTAGE_IN_HEADER, + BIT_BATTERY_BARS_IN_HEADER, #endif #if defined(HAS_SOFT_VOLUME) BIT_VISUAL_VOLUME, diff --git a/MDUV380_firmware/application/source/user_interface/uiUtilities.c b/MDUV380_firmware/application/source/user_interface/uiUtilities.c index ef9f4f0..cd161b0 100644 --- a/MDUV380_firmware/application/source/user_interface/uiUtilities.c +++ b/MDUV380_firmware/application/source/user_interface/uiUtilities.c @@ -1897,7 +1897,7 @@ void uiUtilityRenderHeader(bool isVFODualWatchScanning, bool isVFOSweepScanning, UNUSED_PARAMETER(forceBatteryDisplay); #endif - if (settingsIsOptionBitSet(BIT_BATTERY_VOLTAGE_IN_HEADER)) + if (settingsGetBatteryUnitDisplay() == BATTERY_UNIT_VOLTAGE) { getBatteryVoltage(&volts, &mvolts); @@ -1911,6 +1911,7 @@ void uiUtilityRenderHeader(bool isVFODualWatchScanning, bool isVFOSweepScanning, } else { + // Both the percentage and bars display modes need the battery percentage. batteryPercentage = getBatteryPercentage(); if (batteryPercentage < 100) @@ -2207,7 +2208,9 @@ void uiUtilityRenderHeader(bool isVFODualWatchScanning, bool isVFOSweepScanning, else #endif { - if (settingsIsOptionBitSet(BIT_BATTERY_VOLTAGE_IN_HEADER)) + batteryUnitDisplay_t batteryUnit = settingsGetBatteryUnitDisplay(); + + if (batteryUnit == BATTERY_UNIT_VOLTAGE) { int16_t xV = (DISPLAY_SIZE_X - ((4 * 6) + 3)); @@ -2219,6 +2222,41 @@ void uiUtilityRenderHeader(bool isVFODualWatchScanning, bool isVFOSweepScanning, snprintf(buffer, SCREEN_LINE_BUFFER_SIZE, "%1dV", mvolts); displayPrintCore(xV + (6 * 2) + 3, DISPLAY_Y_POS_HEADER, buffer, (apoEnabled ? FONT_SIZE_1_BOLD : FONT_SIZE_1), TEXT_ALIGN_LEFT, ((batteryIsLow ? scanBlinkPhase : false))); } + else if (batteryUnit == BATTERY_UNIT_BARS) + { + // Battery-shaped icon, right-aligned in the header: an outlined body with a + // terminal nub, holding up to 4 segments each representing 25% of charge. + const int16_t bodyW = 24; + const int16_t bodyH = 7; + const int16_t nubW = 2; + const int16_t nubH = 3; + const int16_t barW = 4; + const int16_t barGap = 1; + int16_t bx = (DISPLAY_SIZE_X - bodyW - nubW); + int16_t by = DISPLAY_Y_POS_HEADER; + + // 0..4 filled segments (rounded to the nearest quarter). + int16_t filledBars = ((batteryPercentage + 12) / 25); + if (filledBars > 4) + { + filledBars = 4; + } + else if (filledBars < 0) + { + filledBars = 0; + } + + // Body outline and terminal nub stay solid; only the fill blinks when low. + // NOTE: displayDrawRect uses isInverted==true for foreground (visible), whereas + // displayFillRect is the opposite (isInverted==false draws the foreground). + displayDrawRect(bx, by, bodyW, bodyH, true); + displayFillRect(bx + bodyW, by + ((bodyH - nubH) / 2), nubW, nubH, false); + + for (int16_t b = 0; b < filledBars; b++) + { + displayFillRect(bx + 2 + (b * (barW + barGap)), by + 2, barW, (bodyH - 4), ((batteryIsLow ? scanBlinkPhase : false))); + } + } else { snprintf(buffer, SCREEN_LINE_BUFFER_SIZE, "%d%%", batteryPercentage); @@ -3149,12 +3187,13 @@ void announceItemWithInit(bool init, voicePromptItem_t item, audioPromptThreshol break; case PROMPT_SEQUENCE_BATTERY: - if (settingsIsOptionBitSet(BIT_BATTERY_VOLTAGE_IN_HEADER)) + if (settingsGetBatteryUnitDisplay() == BATTERY_UNIT_VOLTAGE) { announceBatteryVoltage(); } else { + // Bars mode maps to a charge percentage, so announce it as a percentage. announceBatteryPercentage(); } break;