Mute: toggle audio mute with SK2 short-tap, with LED + header indicator

Add a one-tap audio mute on the VFO and Channel screens:

- SK2 short-tap toggles mute via the built-in audioAmpMute()/audioAmpIsMuted().
  The tap is only consumed by the existing "set TX TG to incoming" behaviour
  while a DMR QSO is on screen, so mute works everywhere else.
- Header: a bold "M" is shown just left of the battery while muted (the
  firmware's built-in strike-through over the mode text also still shows).
- LED: flashes red for 0.5s every 2s while muted (main loop), overriding the
  normal RX/TX LED.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marcus Kida 2026-07-06 15:41:29 +02:00
parent fe9b8d4e10
commit f67bb2fade
5 changed files with 57 additions and 0 deletions

View file

@ -243,6 +243,7 @@ void acceptPrivateCall(uint32_t id, int timeslot);
bool rebuildVoicePromptOnExtraLongSK1(uiEvent_t *ev); bool rebuildVoicePromptOnExtraLongSK1(uiEvent_t *ev);
bool repeatVoicePromptOnSK1(uiEvent_t *ev); bool repeatVoicePromptOnSK1(uiEvent_t *ev);
bool handleMonitorMode(uiEvent_t *ev); bool handleMonitorMode(uiEvent_t *ev);
void toggleAudioMute(void);
void uiUtilityDisplayInformation(const char *str, displayInformation_t line, int8_t yOverride); void uiUtilityDisplayInformation(const char *str, displayInformation_t line, int8_t yOverride);
void uiUtilityRenderQSODataAndUpdateScreen(void); void uiUtilityRenderQSODataAndUpdateScreen(void);

View file

@ -36,6 +36,7 @@
#include "interfaces/adc.h" #include "interfaces/adc.h"
#include "interfaces/batteryRAM.h" #include "interfaces/batteryRAM.h"
#include "hardware/SPI_Flash.h" #include "hardware/SPI_Flash.h"
#include "io/Leds.h"
#include "interfaces/adc.h" #include "interfaces/adc.h"
#include "hardware/radioHardwareInterface.h" #include "hardware/radioHardwareInterface.h"
@ -1432,6 +1433,27 @@ void applicationMainTask(void)
updateVolumeGain(currentMenu); updateVolumeGain(currentMenu);
} }
// Mute indicator: while muted, flash the RED LED for 0.5s every 2s.
// This overrides the normal RX/TX LED while muted.
{
static bool muteFlashActive = false;
if (audioAmpIsMuted())
{
bool flashOn = ((ticksGetMillis() % 2000) < 500);
LedWrite(LED_GREEN, 0);
LedWrite(LED_RED, (flashOn ? 1 : 0));
muteFlashActive = true;
}
else if (muteFlashActive)
{
// Just unmuted: release our forced LED state.
LedWrite(LED_RED, 0);
LedWrite(LED_GREEN, 0);
muteFlashActive = false;
}
}
while(ticksGetMillis() < startTime + 1) // ensure this Task runs at 1ms intervals. Regardless of clock speed. while(ticksGetMillis() < startTime + 1) // ensure this Task runs at 1ms intervals. Regardless of clock speed.
{ {
vTaskDelay(0U); vTaskDelay(0U);

View file

@ -1224,6 +1224,15 @@ static void handleEvent(uiEvent_t *ev)
return; return;
} }
// SK2 short tap toggles audio mute. The DMR TG-set handler above consumes the
// tap (and returns) only while a QSO is on screen, so this won't fire then.
if (BUTTONCHECK_SHORTUP(ev, BUTTON_SK2) && (BUTTONCHECK_DOWN(ev, BUTTON_SK1) == 0) && (monitorModeData.isEnabled == false))
{
toggleAudioMute();
uiChannelModeUpdateScreen(0);
return;
}
// Display channel settings (RX/TX/etc) while SK1 is pressed // Display channel settings (RX/TX/etc) while SK1 is pressed
if ((uiDataGlobal.displayChannelSettings == false) && (monitorModeData.isEnabled == false) && BUTTONCHECK_DOWN(ev, BUTTON_SK1)) if ((uiDataGlobal.displayChannelSettings == false) && (monitorModeData.isEnabled == false) && BUTTONCHECK_DOWN(ev, BUTTON_SK1))
{ {

View file

@ -1852,6 +1852,14 @@ void uiUtilityRenderQSOData(void)
displayThemeResetToDefault(); displayThemeResetToDefault();
} }
// Toggle the audio mute (speaker) state. The header shows a mute icon and the LED
// flashes amber (see the main loop) while muted.
void toggleAudioMute(void)
{
audioAmpMute(!audioAmpIsMuted());
headerRowIsDirty = true; // refresh the header so the mute icon appears/clears
}
void uiUtilityRenderHeader(bool isVFODualWatchScanning, bool isVFOSweepScanning, bool forceBatteryDisplay) void uiUtilityRenderHeader(bool isVFODualWatchScanning, bool isVFOSweepScanning, bool forceBatteryDisplay)
{ {
const int MODE_TEXT_X_OFFSET = 1; const int MODE_TEXT_X_OFFSET = 1;
@ -2176,6 +2184,14 @@ void uiUtilityRenderHeader(bool isVFODualWatchScanning, bool isVFOSweepScanning,
bool displayTime = (settingsIsOptionBitSet(BIT_DISPLAY_TIME_IN_HEADER) && (forceBatteryDisplay == false) && (batteryIsLow == false)); bool displayTime = (settingsIsOptionBitSet(BIT_DISPLAY_TIME_IN_HEADER) && (forceBatteryDisplay == false) && (batteryIsLow == false));
#endif #endif
#endif #endif
// Mute indicator: bold "M" just left of the battery indicator.
if (audioAmpIsMuted())
{
displayThemeApply(THEME_ITEM_FG_HEADER_TEXT, THEME_ITEM_BG_HEADER_TEXT);
displayPrintCore((DISPLAY_SIZE_X - 40), DISPLAY_Y_POS_HEADER, "M", FONT_SIZE_1_BOLD, TEXT_ALIGN_LEFT, false);
displayThemeResetToDefault();
}
// Display battery percentage/voltage // Display battery percentage/voltage
bool apoEnabled = (nonVolatileSettings.apo > 0); bool apoEnabled = (nonVolatileSettings.apo > 0);

View file

@ -1161,6 +1161,15 @@ static void handleEvent(uiEvent_t *ev)
return; return;
} }
// SK2 short tap toggles audio mute. The DMR TG-set handler above consumes the
// tap (and returns) only while a QSO is on screen, so this won't fire then.
if (BUTTONCHECK_SHORTUP(ev, BUTTON_SK2) && (BUTTONCHECK_DOWN(ev, BUTTON_SK1) == 0) && (monitorModeData.isEnabled == false))
{
toggleAudioMute();
uiVFOModeUpdateScreen(0);
return;
}
if ((uiVFOModeSweepScanning(true) == false) && (monitorModeData.isEnabled == false) && (uiDataGlobal.reverseRepeaterVFO == false) && (BUTTONCHECK_DOWN(ev, BUTTON_SK1) && BUTTONCHECK_DOWN(ev, BUTTON_SK2))) if ((uiVFOModeSweepScanning(true) == false) && (monitorModeData.isEnabled == false) && (uiDataGlobal.reverseRepeaterVFO == false) && (BUTTONCHECK_DOWN(ev, BUTTON_SK1) && BUTTONCHECK_DOWN(ev, BUTTON_SK2)))
{ {
int prevQSODisp = -1; int prevQSODisp = -1;