From f715f776a4b270f6d4bb0eaf561a8b56344e3cf8 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Fri, 6 Mar 2026 05:14:50 +0100 Subject: [PATCH 01/50] Write dBmCorrTable only if flash zone is erased --- App/settings.c | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/App/settings.c b/App/settings.c index aa5888e3..38c1620a 100644 --- a/App/settings.c +++ b/App/settings.c @@ -100,9 +100,21 @@ void SETTINGS_InitEEPROM(void) // 5. Reset dBmCorrTable int8_t buf[7]; - for (uint8_t i = 0; i < 7; i++) - buf[i] = dBmCorrTable[i]; // values from misc.c - PY25Q16_WriteBuffer(0x00A0B9, buf, 7, false); + PY25Q16_ReadBuffer(0x00A0B9, (uint8_t *)buf, 7); + + bool allFF = true; + for (uint8_t i = 0; i < 7; i++) { + if ((uint8_t)buf[i] != 0xFF) { + allFF = false; + break; + } + } + + if (allFF) { + for (uint8_t i = 0; i < 7; i++) + buf[i] = dBmCorrTable[i]; + PY25Q16_WriteBuffer(0x00A0B9, buf, 7, false); + } } } From 77785d14c26359d6d3197621c69e11e2a7eaafca Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Fri, 6 Mar 2026 05:17:22 +0100 Subject: [PATCH 02/50] Code refactoring --- App/settings.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/App/settings.c b/App/settings.c index 38c1620a..5523c347 100644 --- a/App/settings.c +++ b/App/settings.c @@ -102,15 +102,15 @@ void SETTINGS_InitEEPROM(void) int8_t buf[7]; PY25Q16_ReadBuffer(0x00A0B9, (uint8_t *)buf, 7); - bool allFF = true; + needsWrite = true; for (uint8_t i = 0; i < 7; i++) { if ((uint8_t)buf[i] != 0xFF) { - allFF = false; + needsWrite = false; break; } } - if (allFF) { + if (needsWrite) { for (uint8_t i = 0; i < 7; i++) buf[i] = dBmCorrTable[i]; PY25Q16_WriteBuffer(0x00A0B9, buf, 7, false); From ffddf9a9b3a3dc12fcc3be4cc0faf533339519fe Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Sun, 8 Mar 2026 00:17:33 +0100 Subject: [PATCH 03/50] Increase PWM frequency to suppress audio interference (issue #195) --- App/driver/backlight.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/App/driver/backlight.c b/App/driver/backlight.c index ce3f20c2..2614f550 100644 --- a/App/driver/backlight.c +++ b/App/driver/backlight.c @@ -31,7 +31,7 @@ #include "misc.h" #endif -#define PWM_FREQ 320 +#define PWM_FREQ 4000 #define DUTY_CYCLE_LEVELS 64 #define DUTY_CYCLE_ON_VALUE GPIO_PIN_MASK(GPIO_PIN_BACKLIGHT) From 74aa7b354e51f7bf1391087f8a3f615ec5e75081 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Sun, 8 Mar 2026 22:09:53 +0100 Subject: [PATCH 04/50] SetVol(68/69) keeps the changes despite Exit key is pressed (issue #200) --- App/ui/menu.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/App/ui/menu.c b/App/ui/menu.c index 2d230457..d4380f68 100644 --- a/App/ui/menu.c +++ b/App/ui/menu.c @@ -1220,7 +1220,7 @@ void UI_DisplayMenu(void) gaugeMax = 63; //#endif } - gEeprom.VOLUME_GAIN = gSubMenuSelection; + // gEeprom.VOLUME_GAIN = gSubMenuSelection; BK4819_WriteRegister(BK4819_REG_48, (11u << 12) | // ??? .. 0 ~ 15, doesn't seem to make any difference ( 0u << 10) | // AF Rx Gain-1 From 5293a8526d55b40825caec6531306c958ba3cf76 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Sun, 8 Mar 2026 22:14:28 +0100 Subject: [PATCH 05/50] Update donors --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 78a1972b..f2d86274 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Anyway, have fun. # Donations -Special thanks to Jean-Cyrille F6IWW (3 times), Fabrice 14RC123, David F4BPP, Olivier 14RC206, Frédéric F4ESO, Stéphane F5LGW (2 times), Jorge Ornelas (4 times), Laurent F4AXK, Christophe Morel, Clayton W0LED, Pierre Antoine F6FWB, Jean-Claude 14FRS3306, Thierry F4GVO, Eric F1NOU, PricelessToolkit, Ady M6NYJ, Tom McGovern (4 times), Joseph Roth, Pierre-Yves Colin, Frank DJ7FG, Marcel Testaz, Brian Frobisher, Yannick F4JFO, Paolo Bussola, Dirk DL8DF, Levente Szőke (2 times), Bernard-Michel Herrera, Jérôme Saintespes, Paul Davies, RS (3 times), Johan F4WAT, Robert Wörle, Rafael Sundorf, Paul Harker, Peter Fintl, Pascal F4ICR (2 times), Mike DL2MF (3 times), Eric KI1C (2 times), Phil G0ELM, Jérôme Lambert, Eliot Vedel, Alfonso EA7KDF, Jean-François F1EVM, Robert DC1RDB (2 times), Ian KE2CHJ, Daryl VK3AWA, Roberto Brunelli, Robert Boardman, Stephen Oliver, Nicolas F4INE, William Bruno, Daniel OK2VLK, Tayler Chew, Peter DL7RFP, Philippe Kopp, Rune LA6YMA, Jeremy Luna, Steef Wagenaar (2 times), Zhuo BG7SGA, Jamie M0JLB, Antoine LIBERT, Vince K0DKR, Julia DF7JA, Ken 2E0UMK and Victor TI2SYS for their [donations](https://www.paypal.com/paypalme/F4HWN). That’s so kind of them. Thanks so much 🙏🏻 +Special thanks to Jean-Cyrille F6IWW (3 times), Fabrice 14RC123, David F4BPP, Olivier 14RC206, Frédéric F4ESO, Stéphane F5LGW (2 times), Jorge Ornelas (4 times), Laurent F4AXK, Christophe Morel, Clayton W0LED, Pierre Antoine F6FWB, Jean-Claude 14FRS3306, Thierry F4GVO, Eric F1NOU, PricelessToolkit, Ady M6NYJ, Tom McGovern (4 times), Joseph Roth, Pierre-Yves Colin, Frank DJ7FG, Marcel Testaz, Brian Frobisher, Yannick F4JFO, Paolo Bussola, Dirk DL8DF, Levente Szőke (2 times), Bernard-Michel Herrera, Jérôme Saintespes, Paul Davies, RS (3 times), Johan F4WAT, Robert Wörle, Rafael Sundorf, Paul Harker, Peter Fintl, Pascal F4ICR (2 times), Mike DL2MF (3 times), Eric KI1C (2 times), Phil G0ELM, Jérôme Lambert, Eliot Vedel, Alfonso EA7KDF, Jean-François F1EVM, Robert DC1RDB (2 times), Ian KE2CHJ, Daryl VK3AWA, Roberto Brunelli, Robert Boardman, Stephen Oliver, Nicolas F4INE, William Bruno, Daniel OK2VLK, Tayler Chew, Peter DL7RFP, Philippe Kopp, Rune LA6YMA, Jeremy Luna, Steef Wagenaar (2 times), Zhuo BG7SGA, Jamie M0JLB, Antoine LIBERT, Vince K0DKR, Julia DF7JA, Ken 2E0UMK, Victor TI2SYS and Tobi DG9LAY for their [donations](https://www.paypal.com/paypalme/F4HWN). That’s so kind of them. Thanks so much 🙏🏻 ## Table of Contents From d826cbd138b3887ff749de740dfebe3a1200ec27 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Mon, 9 Mar 2026 01:02:19 +0100 Subject: [PATCH 06/50] Reduce the lag between consecutive RX on both frequencies under DW mode (issue #198) --- App/app/app.c | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/App/app/app.c b/App/app/app.c index ba58eb13..5062cc77 100644 --- a/App/app/app.c +++ b/App/app/app.c @@ -535,7 +535,17 @@ void APP_StartListening(FUNCTION_Type_t function) gEeprom.DUAL_WATCH != DUAL_WATCH_OFF) { // not scanning, dual watch is enabled - gDualWatchCountdown_10ms = dual_watch_count_after_2_10ms; + //gDualWatchCountdown_10ms = dual_watch_count_after_2_10ms; + + const bool isMainTxDualRx = + (gEeprom.DUAL_WATCH != DUAL_WATCH_OFF) && + (gEeprom.CROSS_BAND_RX_TX != CROSS_BAND_OFF); + + // Use a short hold only for MAIN TX DUAL RX, keep legacy hold otherwise + gDualWatchCountdown_10ms = isMainTxDualRx + ? dual_watch_count_after_2_10ms / 4 // Short timer = 420 / 4 ... + : dual_watch_count_after_2_10ms; + gScheduleDualWatch = false; // when crossband is active only the main VFO should be used for TX From 7471ac90c5a39f0dc6f3369e0d0156010b1d0cdf Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Mon, 9 Mar 2026 01:15:38 +0100 Subject: [PATCH 07/50] Update file --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f33427b5..64adb71c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ firmware .cache compile_commands.json .vscode +.claude /docs k5_eeprom.raw From 37f18972e76c19dceb9a926fe1655fabfce6772b Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Mon, 9 Mar 2026 05:24:12 +0100 Subject: [PATCH 08/50] BYP and RAW for BK4829 --- App/driver/bk4819.h | 4 +++ App/driver/bk4829.c | 83 +++++++++++++++++++++++++++------------------ App/radio.c | 34 ++++++++++++++----- 3 files changed, 80 insertions(+), 41 deletions(-) diff --git a/App/driver/bk4819.h b/App/driver/bk4819.h index 731fbca8..dfc73346 100644 --- a/App/driver/bk4819.h +++ b/App/driver/bk4819.h @@ -116,6 +116,10 @@ void BK4819_TurnsOffTones_TurnsOnRX(void); #endif void BK4819_ResetFSK(void); void BK4819_Idle(void); +#ifdef ENABLE_BYP_RAW_DEMODULATORS +void BK4819_EnterBypass(void); +void BK4819_EnterRaw(void); +#endif void BK4819_ExitBypass(void); void BK4819_PrepareTransmit(void); void BK4819_TxOn_Beep(void); diff --git a/App/driver/bk4829.c b/App/driver/bk4829.c index f0a3d72b..c5858801 100644 --- a/App/driver/bk4829.c +++ b/App/driver/bk4829.c @@ -1154,45 +1154,62 @@ void BK4819_Idle(void) BK4819_WriteRegister(BK4819_REG_30, 0x0000); } +#ifdef ENABLE_BYP_RAW_DEMODULATORS +void BK4819_EnterBypass(void) +{ + // Keep AF output on normal path (REG_47 mode 0x1 is documented on BK4829). + BK4819_SetAF(BK4819_AF_FM); + + // Bypass all AF filters (Rx + Tx) as recommended for digital bypass mode. + // REG_2B: + // bit10: Disable AF Rx HPF300 + // bit 9: Disable AF Rx LPF3K + // bit 8: Disable AF Rx de-emphasis + // bit 2: Disable AF Tx HPF300 + // bit 1: Disable AF Tx LPF1 + // bit 0: Disable AF Tx pre-emphasis + uint16_t reg2b = BK4819_ReadRegister(BK4819_REG_2B); + reg2b |= (1u << 10) | (1u << 9) | (1u << 8) | (1u << 2) | (1u << 1) | (1u << 0); + BK4819_WriteRegister(BK4819_REG_2B, reg2b); + + // Keep AGC/AFC behavior aligned with FM-like bypass experiments. + BK4819_SetRegValue(afcDisableRegSpec, false); +} + +void BK4819_EnterRaw(void) +{ + // Keep AF output on a documented mode. + BK4819_SetAF(BK4819_AF_FM); + + // RAW profile: bypass RX AF filters only, keep TX filter path unchanged. + // REG_2B: + // bit10: Disable AF Rx HPF300 + // bit 9: Disable AF Rx LPF3K + // bit 8: Disable AF Rx de-emphasis + uint16_t reg2b = BK4819_ReadRegister(BK4819_REG_2B); + reg2b |= (1u << 10) | (1u << 9) | (1u << 8); + reg2b &= ~((1u << 2) | (1u << 1) | (1u << 0)); + BK4819_WriteRegister(BK4819_REG_2B, reg2b); + + // RAW profile keeps AFC disabled to preserve discriminator-like behavior. + BK4819_SetRegValue(afcDisableRegSpec, true); +} +#endif + void BK4819_ExitBypass(void) { BK4819_SetAF(BK4819_AF_MUTE); - // REG_7E - // - // <15> 0 AGC fix mode - // 1 = fix - // 0 = auto - // - // <14:12> 3 AGC fix index - // 3 ( 3) = max - // 2 ( 2) - // 1 ( 1) - // 0 ( 0) - // 7 (-1) - // 6 (-2) - // 5 (-3) - // 4 (-4) = min - // - // <11:6> 0 ??? - // - // <5:3> 5 DC filter band width for Tx (MIC In) - // 0 ~ 7 - // 0 = bypass DC filter - // - // <2:0> 6 DC filter band width for Rx (I.F In) - // 0 ~ 7 - // 0 = bypass DC filter - // +#ifdef ENABLE_BYP_RAW_DEMODULATORS + // Restore normal AF filters. + uint16_t reg2b = BK4819_ReadRegister(BK4819_REG_2B); + reg2b &= ~((1u << 10) | (1u << 9) | (1u << 8) | (1u << 2) | (1u << 1) | (1u << 0)); + BK4819_WriteRegister(BK4819_REG_2B, reg2b); +#endif + // Keep existing REG_7E logic from your current implementation. uint16_t regVal = BK4819_ReadRegister(BK4819_REG_7E); - - // 0x302E / 0 011 000000 101 110 - BK4819_WriteRegister(BK4819_REG_7E, (regVal & ~(0b111 << 3)) - - | (5u << 3) // 5 DC Filter band width for Tx (MIC In) - - ); + BK4819_WriteRegister(BK4819_REG_7E, (regVal & ~(0b111 << 3)) | (5u << 3)); } void BK4819_PrepareTransmit(void) diff --git a/App/radio.c b/App/radio.c index 92153af8..376af6d1 100644 --- a/App/radio.c +++ b/App/radio.c @@ -1056,6 +1056,32 @@ void RADIO_SetTxParameters(void) void RADIO_SetModulation(ModulationMode_t modulation) { + #ifdef ENABLE_BYP_RAW_DEMODULATORS + // BYP on BK4829 uses full audio bypass profile. + if (modulation == MODULATION_BYP) { + BK4819_EnterBypass(); + BK4819_SetRegValue(afDacGainRegSpec, 0xF); + BK4819_WriteRegister(BK4819_REG_3D, 0x2AAB); + BK4819_SetRegValue(afcDisableRegSpec, false); + RADIO_SetupAGC(false, false); + return; + } + + // RAW on BK4829 uses RX-only filter bypass profile. + if (modulation == MODULATION_RAW) { + BK4819_EnterRaw(); + BK4819_SetRegValue(afDacGainRegSpec, 0xF); + BK4819_WriteRegister(BK4819_REG_3D, 0x2AAB); + RADIO_SetupAGC(false, false); + return; + } + #endif + + #ifdef ENABLE_BYP_RAW_DEMODULATORS + // Ensure we always leave bypass / raw mode before applying normal modulation settings. + BK4819_ExitBypass(); + #endif + BK4819_AF_Type_t mod; switch(modulation) { default: @@ -1069,14 +1095,6 @@ void RADIO_SetModulation(ModulationMode_t modulation) mod = BK4819_AF_BASEBAND2; break; -#ifdef ENABLE_BYP_RAW_DEMODULATORS - case MODULATION_BYP: - mod = BK4819_AF_UNKNOWN3; - break; - case MODULATION_RAW: - mod = BK4819_AF_BASEBAND1; - break; -#endif } BK4819_SetAF(mod); From d0fdca4dc1f56de444f453137dffd73d04af18b1 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Tue, 10 Mar 2026 02:54:25 +0100 Subject: [PATCH 09/50] BYP and RAW for BK4829 --- App/radio.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/App/radio.c b/App/radio.c index 376af6d1..bdb94c77 100644 --- a/App/radio.c +++ b/App/radio.c @@ -1062,7 +1062,6 @@ void RADIO_SetModulation(ModulationMode_t modulation) BK4819_EnterBypass(); BK4819_SetRegValue(afDacGainRegSpec, 0xF); BK4819_WriteRegister(BK4819_REG_3D, 0x2AAB); - BK4819_SetRegValue(afcDisableRegSpec, false); RADIO_SetupAGC(false, false); return; } @@ -1071,7 +1070,7 @@ void RADIO_SetModulation(ModulationMode_t modulation) if (modulation == MODULATION_RAW) { BK4819_EnterRaw(); BK4819_SetRegValue(afDacGainRegSpec, 0xF); - BK4819_WriteRegister(BK4819_REG_3D, 0x2AAB); + BK4819_WriteRegister(BK4819_REG_3D, 0x0000); RADIO_SetupAGC(false, false); return; } @@ -1247,6 +1246,12 @@ void RADIO_PrepareTX(void) // over voltage .. this is being a pain State = VFO_STATE_VOLTAGE_HIGH; } +#ifdef ENABLE_BYP_RAW_DEMODULATORS + else if (gCurrentVfo->Modulation == MODULATION_BYP || gCurrentVfo->Modulation == MODULATION_RAW) { + // BYP/RAW are receive-only modes. + State = VFO_STATE_TX_DISABLE; + } +#endif #ifndef ENABLE_TX_WHEN_AM else if (gCurrentVfo->Modulation != MODULATION_FM) { // not allowed to TX if in AM mode From eaa05f107ee17ee556e05ff844a2f20e0d678b7d Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Tue, 10 Mar 2026 04:00:43 +0100 Subject: [PATCH 10/50] Improve one and two-letter scan list names (issue #206) --- App/ui/main.c | 2 +- App/ui/menu.c | 4 ++-- App/ui/status.c | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/App/ui/main.c b/App/ui/main.c index 008ee431..8f4f22cc 100644 --- a/App/ui/main.c +++ b/App/ui/main.c @@ -976,7 +976,7 @@ void UI_DisplayMain(void) const char *name = gListName[countList - 1]; // If name is empty/invalid, display number - if (name[0] == '\0' || name[0] == '\xff' || name[0] == ' ') { + if (name[0] == '\0' || name[0] == '\xff') { sprintf(String, "%02d", countList); xStart = 117; // 2-digit number aligned right xDisplay = 119; diff --git a/App/ui/menu.c b/App/ui/menu.c index d4380f68..c9d22b01 100644 --- a/App/ui/menu.c +++ b/App/ui/menu.c @@ -976,7 +976,7 @@ void UI_DisplayMenu(void) const char *name = gListName[gSubMenuSelection - 1]; // If first character is empty/invalid, display "N/A" - if (name[0] == '\0' || name[0] == '\xff' || name[0] == ' ') + if (name[0] == '\0' || name[0] == '\xff') sprintf(String, "%02u", gSubMenuSelection); else sprintf(String, "%02u (%.3s)", gSubMenuSelection, name); @@ -989,7 +989,7 @@ void UI_DisplayMenu(void) else { const char *name = gListName[gSubMenuSelection - 1]; // If first character is empty/invalid, display "N/A" - if (name[0] == '\0' || name[0] == '\xff' || name[0] == ' ') + if (name[0] == '\0' || name[0] == '\xff') sprintf(String, "%02u", gSubMenuSelection); else sprintf(String, "%02u (%.3s)", gSubMenuSelection, name); diff --git a/App/ui/status.c b/App/ui/status.c index 1d9b9829..1f2de5a8 100644 --- a/App/ui/status.c +++ b/App/ui/status.c @@ -114,7 +114,7 @@ void UI_DisplayStatus() const char *name = gListName[gEeprom.SCAN_LIST_DEFAULT - 1]; // Check if name is valid - bool nameValid = (name[0] != '\0' && name[0] != '\xff' && name[0] != ' '); + bool nameValid = (name[0] != '\0' && name[0] != '\xff'); // Format the string if (nameValid) { From d83c6451c8f00124a9bb98e33704511d8de5ab2d Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Tue, 10 Mar 2026 04:20:38 +0100 Subject: [PATCH 11/50] Fix EX label --- App/ui/main.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/App/ui/main.c b/App/ui/main.c index 8f4f22cc..f17456a6 100644 --- a/App/ui/main.c +++ b/App/ui/main.c @@ -1007,9 +1007,11 @@ void UI_DisplayMain(void) GUI_DisplaySmallest(displayStr, xDisplay, line == 0 ? 1 : 33, false, true); - for (uint8_t x = xStart; x < 128; x++) { + gFrameBuffer[line][xStart] ^= 0x3E; + for (uint8_t x = xStart + 1; x < 127; x++) { gFrameBuffer[line][x] ^= 0x7F; } + gFrameBuffer[line][127] ^= 0x3E; } #ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS From 7b7186e29397396ee55b0b9ce3db820ba6501bbf Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Tue, 10 Mar 2026 04:37:10 +0100 Subject: [PATCH 12/50] Improve one and two-letter scan list names (issue #206) --- App/ui/helper.c | 10 ++++++++++ App/ui/helper.h | 1 + App/ui/main.c | 2 +- App/ui/menu.c | 4 ++-- App/ui/status.c | 5 +---- 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/App/ui/helper.c b/App/ui/helper.c index 252a7c15..764de28f 100644 --- a/App/ui/helper.c +++ b/App/ui/helper.c @@ -321,6 +321,16 @@ static void sort(int16_t *a, int16_t *b) x += 4; } } + + bool IsEmptyName(const char *name, uint8_t len) { + if (name[0] == '\0' || name[0] == '\xff') + return true; + for (uint8_t i = 0; i < len; i++) { + if (name[i] != ' ' && name[i] != '\xff' && name[i] != '\0') + return false; + } + return true; + } #endif void UI_DrawLineBuffer(uint8_t (*buffer)[128], int16_t x1, int16_t y1, int16_t x2, int16_t y2, bool black) diff --git a/App/ui/helper.h b/App/ui/helper.h index 302d375c..267a158e 100644 --- a/App/ui/helper.h +++ b/App/ui/helper.h @@ -38,6 +38,7 @@ void UI_DrawPixelBuffer(uint8_t (*buffer)[128], uint8_t x, uint8_t y, bool black void PutPixel(uint8_t x, uint8_t y, bool fill); void PutPixelStatus(uint8_t x, uint8_t y, bool fill); void GUI_DisplaySmallest(const char *pString, uint8_t x, uint8_t y, bool statusbar, bool fill); + bool IsEmptyName(const char *name, uint8_t len); #endif void UI_DrawLineBuffer(uint8_t (*buffer)[128], int16_t x1, int16_t y1, int16_t x2, int16_t y2, bool black); void UI_DrawRectangleBuffer(uint8_t (*buffer)[128], int16_t x1, int16_t y1, int16_t x2, int16_t y2, bool black); diff --git a/App/ui/main.c b/App/ui/main.c index f17456a6..5db22460 100644 --- a/App/ui/main.c +++ b/App/ui/main.c @@ -976,7 +976,7 @@ void UI_DisplayMain(void) const char *name = gListName[countList - 1]; // If name is empty/invalid, display number - if (name[0] == '\0' || name[0] == '\xff') { + if (IsEmptyName(name, sizeof(gListName[0]))) { sprintf(String, "%02d", countList); xStart = 117; // 2-digit number aligned right xDisplay = 119; diff --git a/App/ui/menu.c b/App/ui/menu.c index c9d22b01..a9b486ed 100644 --- a/App/ui/menu.c +++ b/App/ui/menu.c @@ -976,7 +976,7 @@ void UI_DisplayMenu(void) const char *name = gListName[gSubMenuSelection - 1]; // If first character is empty/invalid, display "N/A" - if (name[0] == '\0' || name[0] == '\xff') + if (IsEmptyName(name, sizeof(gListName[0]))) sprintf(String, "%02u", gSubMenuSelection); else sprintf(String, "%02u (%.3s)", gSubMenuSelection, name); @@ -989,7 +989,7 @@ void UI_DisplayMenu(void) else { const char *name = gListName[gSubMenuSelection - 1]; // If first character is empty/invalid, display "N/A" - if (name[0] == '\0' || name[0] == '\xff') + if (IsEmptyName(name, sizeof(gListName[0]))) sprintf(String, "%02u", gSubMenuSelection); else sprintf(String, "%02u (%.3s)", gSubMenuSelection, name); diff --git a/App/ui/status.c b/App/ui/status.c index 1f2de5a8..cb37526e 100644 --- a/App/ui/status.c +++ b/App/ui/status.c @@ -114,10 +114,7 @@ void UI_DisplayStatus() const char *name = gListName[gEeprom.SCAN_LIST_DEFAULT - 1]; // Check if name is valid - bool nameValid = (name[0] != '\0' && name[0] != '\xff'); - - // Format the string - if (nameValid) { + if (!IsEmptyName(name, sizeof(gListName[0]))) { sprintf(str, "%.3s%s", name, gEeprom.SCAN_LIST_ENABLED ? "+" : ""); end = gEeprom.SCAN_LIST_ENABLED ? 18 : 14; } From 5d88fda158515967c420d77c95308dfc99509c56 Mon Sep 17 00:00:00 2001 From: mrkusypl Date: Wed, 11 Mar 2026 22:52:10 +0100 Subject: [PATCH 13/50] Added backlight fading, improved backlight control in spectrum --- App/app/app.c | 6 +-- App/app/breakout.c | 3 ++ App/app/spectrum.c | 15 +++++-- App/driver/backlight.c | 97 ++++++++++++++++++++++++++++-------------- App/driver/backlight.h | 2 + 5 files changed, 85 insertions(+), 38 deletions(-) diff --git a/App/app/app.c b/App/app/app.c index 5062cc77..f26eb57b 100644 --- a/App/app/app.c +++ b/App/app/app.c @@ -1358,9 +1358,9 @@ void APP_TimeSlice10ms(void) { gNextTimeslice = false; - if (gScheduleVfoSave) { - SETTINGS_SaveVfoIndicesFlush(); - } + SETTINGS_SaveVfoIndicesFlush(); + + BACKLIGHT_Update(); gFlashLightBlinkCounter++; diff --git a/App/app/breakout.c b/App/app/breakout.c index 72cae521..fdf1fcb5 100644 --- a/App/app/breakout.c +++ b/App/app/breakout.c @@ -461,6 +461,9 @@ void APP_RunBreakout(void) { // Init led BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, false); + // Finish the brightness fade if it is in progress + BACKLIGHT_UpdateTickless(); + // Init game UI_DisplayClear(); reset(); diff --git a/App/app/spectrum.c b/App/app/spectrum.c index b8e3b996..2fa38995 100644 --- a/App/app/spectrum.c +++ b/App/app/spectrum.c @@ -787,11 +787,13 @@ static void ToggleBacklight() settings.backlightState = !settings.backlightState; if (settings.backlightState) { - BACKLIGHT_TurnOn(); + // BACKLIGHT_TurnOn(); + BACKLIGHT_SetBrightness(gEeprom.BACKLIGHT_MAX); } else { - BACKLIGHT_TurnOff(); + // BACKLIGHT_TurnOff(); + BACKLIGHT_SetBrightness(gEeprom.BACKLIGHT_MIN); } } @@ -1665,16 +1667,17 @@ static void UpdateListening() static void Tick() { -#ifdef ENABLE_AM_FIX if (gNextTimeslice) { gNextTimeslice = false; +#ifdef ENABLE_AM_FIX if (settings.modulationType == MODULATION_AM && !lockAGC) { AM_fix_10ms(vfo); // allow AM_Fix to apply its AGC action } - } #endif + BACKLIGHT_Update(); + } #ifdef ENABLE_SCAN_RANGES if (gNextTimeslice_500ms) @@ -1742,6 +1745,8 @@ static void Tick() void APP_RunSpectrum() { + settings.backlightState = gEeprom.BACKLIGHT_TIME == 0 ? false : true; + // TX here coz it always? set to active VFO vfo = gEeprom.TX_VFO; #ifdef ENABLE_FEAT_F4HWN_SPECTRUM @@ -1804,4 +1809,6 @@ void APP_RunSpectrum() { Tick(); } + + BACKLIGHT_TurnOn(); } \ No newline at end of file diff --git a/App/driver/backlight.c b/App/driver/backlight.c index 2614f550..dc925803 100644 --- a/App/driver/backlight.c +++ b/App/driver/backlight.c @@ -44,8 +44,14 @@ static uint32_t dutyCycle[DUTY_CYCLE_LEVELS]; // this is decremented once every 500ms uint16_t gBacklightCountdown_500ms = 0; +bool gUpdateBacklight = false; bool backlightOn; +static uint8_t currentIndex = 0; +static int16_t currentBrightness = 0; +static int16_t targetBrightness = 0; +static int16_t fadeStep = 0; + #ifdef ENABLE_FEAT_F4HWN const uint8_t value[] = { 0, // 0 off @@ -110,6 +116,12 @@ static void BACKLIGHT_Sound(void) gK5startup = false; } +void BACKLIGHT_UpdateTickless(void) { + while(gUpdateBacklight) { + BACKLIGHT_Update(); + SYSTEM_DelayMs(10); + } +} void BACKLIGHT_TurnOn(void) { @@ -134,27 +146,23 @@ void BACKLIGHT_TurnOn(void) backlightOn = true; -#ifdef ENABLE_FEAT_F4HWN - if(gK5startup == true) { - #if defined(ENABLE_FMRADIO) && defined(ENABLE_SPECTRUM) - BACKLIGHT_SetBrightness(gEeprom.BACKLIGHT_MAX); - #else - for(uint8_t i = 0; i <= gEeprom.BACKLIGHT_MAX; i++) - { - BACKLIGHT_SetBrightness(i); - SYSTEM_DelayMs(50); - } - #endif - - BACKLIGHT_Sound(); - } - else - { - BACKLIGHT_SetBrightness(gEeprom.BACKLIGHT_MAX); - } -#else BACKLIGHT_SetBrightness(gEeprom.BACKLIGHT_MAX); + +#ifdef ENABLE_FEAT_F4HWN + if(gK5startup == true) +#else + static bool startup = true; + + if(startup) #endif + { + BACKLIGHT_UpdateTickless(); +#ifdef ENABLE_FEAT_F4HWN + BACKLIGHT_Sound(); +#else + startup = false; +#endif + } switch (gEeprom.BACKLIGHT_TIME) { default: @@ -190,18 +198,11 @@ bool BACKLIGHT_IsOn() return backlightOn; } -static uint8_t currentBrightness = 0; - -void BACKLIGHT_SetBrightness(uint8_t brigtness) +static void BACKLIGHT_SetHardwareBrightness(uint8_t brightness) { // printf("BL: %d\n", brigtness); - if (currentBrightness == brigtness) - { - return; - } - - if (0 == brigtness) + if (0 == brightness) { LL_TIM_DisableCounter(TIMx); LL_DMA_DisableChannel(DMA1, DMA_CHANNEL); @@ -210,7 +211,7 @@ void BACKLIGHT_SetBrightness(uint8_t brigtness) } else { - const uint32_t level = (uint32_t)(value[brigtness]) * DUTY_CYCLE_LEVELS / 255; + const uint32_t level = (uint32_t)(brightness) * DUTY_CYCLE_LEVELS / 255; if (level >= DUTY_CYCLE_LEVELS) { LL_TIM_DisableCounter(TIMx); @@ -231,11 +232,45 @@ void BACKLIGHT_SetBrightness(uint8_t brigtness) } } } +} - currentBrightness = brigtness; +void BACKLIGHT_Update(void) +{ + if (gUpdateBacklight) { + currentBrightness += fadeStep; + + if ((fadeStep > 0 && currentBrightness >= targetBrightness) || + (fadeStep < 0 && currentBrightness <= targetBrightness)) { + currentBrightness = targetBrightness; + gUpdateBacklight = false; + } + + BACKLIGHT_SetHardwareBrightness((uint8_t)currentBrightness); + } +} + +void BACKLIGHT_SetBrightness(uint8_t targetIndex) +{ + if (currentIndex == targetIndex) { + return; + } + + currentIndex = targetIndex; + targetBrightness = value[targetIndex]; + + int16_t diff = targetBrightness - currentBrightness; + + if (diff == 0) { + gUpdateBacklight = false; + return; + } + + fadeStep = diff > 0 ? -(-diff >> 4) : diff >> 4; + + gUpdateBacklight = true; } uint8_t BACKLIGHT_GetBrightness(void) { - return currentBrightness; + return currentIndex; } diff --git a/App/driver/backlight.h b/App/driver/backlight.h index 368ada78..b0d0e7b9 100644 --- a/App/driver/backlight.h +++ b/App/driver/backlight.h @@ -40,10 +40,12 @@ typedef enum { #endif void BACKLIGHT_InitHardware(); +void BACKLIGHT_UpdateTickless(void); void BACKLIGHT_TurnOn(); void BACKLIGHT_TurnOff(); bool BACKLIGHT_IsOn(); void BACKLIGHT_SetBrightness(uint8_t brigtness); +void BACKLIGHT_Update(void); uint8_t BACKLIGHT_GetBrightness(void); #endif From 9f0547f6afb24822db7592d4fdc8d6e3f813d33b Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Sat, 14 Mar 2026 05:34:17 +0100 Subject: [PATCH 14/50] Fix spectrum: reliable serial key input + USB-C unplug freeze fix (issue #212) --- App/app/aircopy.c | 6 +-- App/app/app.c | 2 +- App/app/breakout.c | 2 +- App/app/spectrum.c | 8 +++- App/driver/st7565.c | 2 +- App/driver/uart.c | 91 ++++++++++++++++++++++++++++--------------- App/screenshot.c | 38 +++++++++++------- App/screenshot.h | 3 +- App/ui/welcome.c | 2 +- App/usb/usbd_cdc_if.c | 7 +++- 10 files changed, 106 insertions(+), 55 deletions(-) diff --git a/App/app/aircopy.c b/App/app/aircopy.c index 9d716d17..34276077 100644 --- a/App/app/aircopy.c +++ b/App/app/aircopy.c @@ -139,7 +139,7 @@ static void AIRCOPY_clear() crc[i] = 0; } #ifdef ENABLE_FEAT_F4HWN_SCREENSHOT - getScreenShot(true); + SCREENSHOT_Update(true); #endif } @@ -167,7 +167,7 @@ static inline void AIRCOPY_CheckComplete(void) { gAircopyState = AIRCOPY_COMPLETE; #ifdef ENABLE_FEAT_F4HWN_SCREENSHOT - getScreenShot(false); + SCREENSHOT_Update(false); #endif } } @@ -212,7 +212,7 @@ bool AIRCOPY_SendMessage(void) if (CurrentSegmentIndex >= map->num_segments) { gAircopyState = AIRCOPY_COMPLETE; #ifdef ENABLE_FEAT_F4HWN_SCREENSHOT - getScreenShot(false); + SCREENSHOT_Update(false); #endif return 0; } diff --git a/App/app/app.c b/App/app/app.c index f26eb57b..264bf7ce 100644 --- a/App/app/app.c +++ b/App/app/app.c @@ -1406,7 +1406,7 @@ void APP_TimeSlice10ms(void) #ifdef ENABLE_FEAT_F4HWN_SCREENSHOT if (gUpdateDisplayCurrent || gUpdateStatusCurrent) { - getScreenShot(false); + SCREENSHOT_Update(false); } #endif diff --git a/App/app/breakout.c b/App/app/breakout.c index fdf1fcb5..26460830 100644 --- a/App/app/breakout.c +++ b/App/app/breakout.c @@ -484,7 +484,7 @@ void APP_RunBreakout(void) { // For screenshot #ifdef ENABLE_FEAT_F4HWN_SCREENSHOT - getScreenShot(false); + SCREENSHOT_Update(false); #endif } diff --git a/App/app/spectrum.c b/App/app/spectrum.c index 2fa38995..e6b9d8ea 100644 --- a/App/app/spectrum.c +++ b/App/app/spectrum.c @@ -1667,6 +1667,12 @@ static void UpdateListening() static void Tick() { +#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT + // Parse incoming packets on every tick so serial keys are never missed, + // regardless of whether the screen needs redrawing. + SCREENSHOT_ParseInput(); +#endif + if (gNextTimeslice) { gNextTimeslice = false; @@ -1737,7 +1743,7 @@ static void Tick() Render(); // For screenshot #ifdef ENABLE_FEAT_F4HWN_SCREENSHOT - getScreenShot(false); + SCREENSHOT_Update(false); #endif redrawScreen = false; } diff --git a/App/driver/st7565.c b/App/driver/st7565.c index 1be740a0..b6b0a655 100644 --- a/App/driver/st7565.c +++ b/App/driver/st7565.c @@ -165,7 +165,7 @@ void ST7565_DrawLine(const unsigned int Column, const unsigned int Line, const u { ST7565_BlitScreen(line + 1); #ifdef ENABLE_FEAT_F4HWN_SCREENSHOT - getScreenShot(true); // Force immediate capture + SCREENSHOT_Update(true); // Force immediate capture #endif } diff --git a/App/driver/uart.c b/App/driver/uart.c index de99a5e7..73c87d6b 100644 --- a/App/driver/uart.c +++ b/App/driver/uart.c @@ -155,43 +155,70 @@ void UART_LogSend(const void *pBuffer, uint32_t Size) #ifdef ENABLE_FEAT_F4HWN_SCREENSHOT bool UART_IsCableConnected(void) { + typedef enum { + STATE_IDLE = 0, + STATE_KA_1, + STATE_KA_2, + STATE_KA_3, + STATE_KEY_1, + STATE_KEY_2, + STATE_KEY_3, + STATE_KEY_3L, + } ParseState_t; + + static uint8_t read_ptr = 0; + static ParseState_t state = STATE_IDLE; + bool connected = false; - for (size_t i = 0; i < sizeof(UART_DMA_Buffer); i++) { - uint8_t b = UART_DMA_Buffer[i]; + // DMA write position: NbData counts DOWN from 256 + uint8_t write_ptr = (uint8_t)(sizeof(UART_DMA_Buffer) - + LL_DMA_GetDataLength(DMA1, DMA_CHANNEL)); - if (b == 0x55) { - // Keepalive byte — viewer is alive - UART_DMA_Buffer[i] = 0x00; - connected = true; - } - else if (b == 0xAA) { - // Possible start of a key packet: 0xAA 0x55 - // type 0x03 = short press, 0x04 = long press - size_t i1 = (i + 1) % sizeof(UART_DMA_Buffer); - size_t i2 = (i + 2) % sizeof(UART_DMA_Buffer); - size_t i3 = (i + 3) % sizeof(UART_DMA_Buffer); - - if (UART_DMA_Buffer[i1] == 0x55 && - (UART_DMA_Buffer[i2] == UART_TYPE_KEY || - UART_DMA_Buffer[i2] == UART_TYPE_KEY_LONG)) - { - uint8_t type = UART_DMA_Buffer[i2]; - uint8_t keyCode = UART_DMA_Buffer[i3]; - - // Consume all 4 bytes - UART_DMA_Buffer[i] = 0x00; - UART_DMA_Buffer[i1] = 0x00; - UART_DMA_Buffer[i2] = 0x00; - UART_DMA_Buffer[i3] = 0x00; - - if (type == UART_TYPE_KEY_LONG) - KEYBOARD_InjectKeyLong(keyCode); - else - KEYBOARD_InjectKey(keyCode); + uint8_t processed = 0; + while (read_ptr != write_ptr && processed < sizeof(UART_DMA_Buffer)) + { + uint8_t b = UART_DMA_Buffer[read_ptr++]; + // read_ptr wraps naturally at 256 since it's uint8_t + processed++; + switch (state) + { + case STATE_IDLE: + if (b == 0x55) state = STATE_KA_1; + else if (b == 0xAA) state = STATE_KEY_1; + break; + case STATE_KA_1: + state = (b == 0xAA) ? STATE_KA_2 : STATE_IDLE; + break; + case STATE_KA_2: + state = (b == 0x00) ? STATE_KA_3 : STATE_IDLE; + break; + case STATE_KA_3: + if (b == 0x00) connected = true; + state = STATE_IDLE; + break; + case STATE_KEY_1: + state = (b == 0x55) ? STATE_KEY_2 : STATE_IDLE; + break; + case STATE_KEY_2: + if (b == UART_TYPE_KEY) state = STATE_KEY_3; + else if (b == UART_TYPE_KEY_LONG) state = STATE_KEY_3L; + else state = STATE_IDLE; + break; + case STATE_KEY_3: + KEYBOARD_InjectKey(b); connected = true; - } + state = STATE_IDLE; + break; + case STATE_KEY_3L: + KEYBOARD_InjectKeyLong(b); + connected = true; + state = STATE_IDLE; + break; + default: + state = STATE_IDLE; + break; } } diff --git a/App/screenshot.c b/App/screenshot.c index 73c18c29..6781c905 100644 --- a/App/screenshot.c +++ b/App/screenshot.c @@ -21,7 +21,26 @@ #include "driver/vcp.h" #include "driver/keyboard.h" -static void Screenshot_Send(const uint8_t *buf, uint16_t len) +// SRAM optimization: minimize static allocations +// - previousFrame: 1024 bytes (REQUIRED - need to compare for delta) +// - No currentFrame or deltaFrame static buffers +static uint8_t previousFrame[1024] = {0}; +static uint8_t forcedBlock = 0; +static uint8_t keepAlive = 10; + +void SCREENSHOT_ParseInput(void) +{ + if (UART_IsCableConnected()) { + keepAlive = 10; + gUSB_ScreenshotEnabled = false; + } + if (VCP_ScreenshotPing()) { + keepAlive = 10; + gUSB_ScreenshotEnabled = true; + } +} + +static void SCREENSHOT_Send(const uint8_t *buf, uint16_t len) { if (gUSB_ScreenshotEnabled) { cdc_acm_data_send_with_dtr(buf, len); @@ -30,14 +49,7 @@ static void Screenshot_Send(const uint8_t *buf, uint16_t len) } } -// SRAM optimization: minimize static allocations -// - previousFrame: 1024 bytes (REQUIRED - need to compare for delta) -// - No currentFrame or deltaFrame static buffers -static uint8_t previousFrame[1024] = {0}; -static uint8_t forcedBlock = 0; -static uint8_t keepAlive = 10; - -void getScreenShot(bool force) +void SCREENSHOT_Update(bool force) { // Build frame in a temporary stack buffer // This is 1024 bytes but it's temporary and gets freed after the function @@ -135,7 +147,7 @@ void getScreenShot(bool force) // New format: sends 0xFF before header // Old format: doesn't exist, so viewers can differentiate uint8_t versionMarker = 0xFF; - Screenshot_Send(&versionMarker, 1); + SCREENSHOT_Send(&versionMarker, 1); // ==== Send header ==== uint8_t header[5] = { @@ -144,7 +156,7 @@ void getScreenShot(bool force) (uint8_t)(deltaLen & 0xFF) }; - Screenshot_Send(header, 5); + SCREENSHOT_Send(header, 5); // ==== SECOND PASS: Send only changed chunks ==== uint8_t chunk[9]; @@ -157,12 +169,12 @@ void getScreenShot(bool force) chunk[0] = chunkIdx; memcpy(&chunk[1], cur, 8); - Screenshot_Send(chunk, 9); + SCREENSHOT_Send(chunk, 9); // Update previousFrame for next comparison memcpy(prev, cur, 8); } uint8_t end = 0x0A; - Screenshot_Send(&end, 1); + SCREENSHOT_Send(&end, 1); } \ No newline at end of file diff --git a/App/screenshot.h b/App/screenshot.h index d2811879..1ad17c4b 100644 --- a/App/screenshot.h +++ b/App/screenshot.h @@ -17,6 +17,7 @@ #ifndef SCREENSHOT_H #define SCREENSHOT_H -void getScreenShot(bool force); +void SCREENSHOT_Update(bool force); +void SCREENSHOT_ParseInput(void); #endif \ No newline at end of file diff --git a/App/ui/welcome.c b/App/ui/welcome.c index cb6f3d9d..0cecf63b 100644 --- a/App/ui/welcome.c +++ b/App/ui/welcome.c @@ -222,7 +222,7 @@ void UI_DisplayWelcome(void) ST7565_BlitFullScreen(); #ifdef ENABLE_FEAT_F4HWN_SCREENSHOT - getScreenShot(true); + SCREENSHOT_Update(true); #endif } } \ No newline at end of file diff --git a/App/usb/usbd_cdc_if.c b/App/usb/usbd_cdc_if.c index 61bace17..080c9f6e 100644 --- a/App/usb/usbd_cdc_if.c +++ b/App/usb/usbd_cdc_if.c @@ -190,8 +190,13 @@ void cdc_acm_data_send_with_dtr(const uint8_t *buf, uint32_t size) { ep_tx_busy_flag = true; usbd_ep_start_write(CDC_IN_EP, buf, size); - while (ep_tx_busy_flag) + uint32_t timeout = 100000; + while (ep_tx_busy_flag && --timeout) ; + if (!timeout) { + ep_tx_busy_flag = false; + dtr_enable = 0; // Consider USB disconnected + } } } From 5cb6bbdf7a5588ad0fc53768480cea0e3cda6823 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Sat, 14 Mar 2026 17:47:23 +0100 Subject: [PATCH 15/50] Overlap between Live DTMF and Unlock Keyboard (issue #215) --- App/ui/main.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/App/ui/main.c b/App/ui/main.c index 5db22460..46770676 100644 --- a/App/ui/main.c +++ b/App/ui/main.c @@ -1522,7 +1522,7 @@ void UI_DisplayMain(void) if (rx || gCurrentFunction == FUNCTION_FOREGROUND || gCurrentFunction == FUNCTION_POWER_SAVE) { #if 1 - if (gSetting_live_DTMF_decoder && gDTMF_RX_live[0] != 0) + if (gSetting_live_DTMF_decoder && gDTMF_RX_live[0] != 0 && gKeypadLocked == 0) { // show live DTMF decode const unsigned int len = strlen(gDTMF_RX_live); const unsigned int idx = (len > (17 - 5)) ? len - (17 - 5) : 0; // limit to last 'n' chars From f5d6f5782bba20bc14ec57540fc7b4b0fd7adf41 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Sat, 14 Mar 2026 19:36:33 +0100 Subject: [PATCH 16/50] Fix breakout: reliable key handling from K5Viewer (issue #214) --- App/app/breakout.c | 48 +++++++++++++++++++++------------------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/App/app/breakout.c b/App/app/breakout.c index 26460830..b55b178b 100644 --- a/App/app/breakout.c +++ b/App/app/breakout.c @@ -416,31 +416,26 @@ static KEY_Code_t GetKey() // HandleUserInput static bool HandleUserInput() { - // Store previous key state kbd.prev = kbd.current; - - // Get the current key kbd.current = GetKey(); - - // Detect valid key press continuation (same key still pressed) - if (kbd.current != KEY_INVALID && kbd.current == kbd.prev) - { - kbd.counter = 1; - OnKeyDown(kbd.current); - - // Special handling for MENU key - if(kbd.current == KEY_MENU) - { - kbd.counter = 0; - SYSTEM_DelayMs(250); - } - } - else + if (kbd.current == KEY_INVALID) + return true; + + // Movement keys: dispatch on every tick while held + if (kbd.current == KEY_UP || kbd.current == KEY_DOWN || + kbd.current == KEY_4 || kbd.current == KEY_0) { - kbd.counter = 0; + OnKeyDown(kbd.current); + return true; } - + + // Action keys (MENU, EXIT): dispatch only on rising edge + if (kbd.current != kbd.prev) + { + OnKeyDown(kbd.current); + } + return true; } @@ -448,7 +443,6 @@ static bool HandleUserInput() static void Tick() { HandleUserInput(); - HandleUserInput(); } // APP_RunBreakout @@ -481,11 +475,6 @@ void APP_RunBreakout(void) { if(swap == 0) { blockAnim = (blockAnim + 1) % 4; - - // For screenshot - #ifdef ENABLE_FEAT_F4HWN_SCREENSHOT - SCREENSHOT_Update(false); - #endif } swap = (swap + 1) % 4; @@ -508,5 +497,12 @@ void APP_RunBreakout(void) { ST7565_BlitStatusLine(); // Blank status line ST7565_BlitFullScreen(); + + #ifdef ENABLE_FEAT_F4HWN_SCREENSHOT + if(isPaused || swap == 0) + { + SCREENSHOT_Update(false); + } + #endif } } \ No newline at end of file From 470d1959ba80eb8c7aac2f1d7b54aed5cf869de1 Mon Sep 17 00:00:00 2001 From: mrkusypl Date: Sun, 15 Mar 2026 01:31:53 +0100 Subject: [PATCH 17/50] K5Viewer: More reliable key handling --- App/app/app.c | 6 ++++++ App/app/breakout.c | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/App/app/app.c b/App/app/app.c index 264bf7ce..7f172faf 100644 --- a/App/app/app.c +++ b/App/app/app.c @@ -892,6 +892,12 @@ static void HandleVox(void) void APP_Update(void) { +#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT + // Parse incoming packets on every tick so serial keys are never missed, + // regardless of whether the screen needs redrawing. + SCREENSHOT_ParseInput(); +#endif + #ifdef ENABLE_VOICE if (gFlagPlayQueuedVoice) { AUDIO_PlayQueuedVoice(); diff --git a/App/app/breakout.c b/App/app/breakout.c index b55b178b..1e667772 100644 --- a/App/app/breakout.c +++ b/App/app/breakout.c @@ -416,6 +416,12 @@ static KEY_Code_t GetKey() // HandleUserInput static bool HandleUserInput() { +#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT + // Parse incoming packets on every tick so serial keys are never missed, + // regardless of whether the screen needs redrawing. + SCREENSHOT_ParseInput(); +#endif + kbd.prev = kbd.current; kbd.current = GetKey(); From f2666a624800b8800d5a9de94fed2ae24ccd082e Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Sun, 15 Mar 2026 17:35:56 +0100 Subject: [PATCH 18/50] Screenshot force full frame on reconnection --- App/screenshot.c | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/App/screenshot.c b/App/screenshot.c index 6781c905..7fd1544d 100644 --- a/App/screenshot.c +++ b/App/screenshot.c @@ -27,6 +27,7 @@ static uint8_t previousFrame[1024] = {0}; static uint8_t forcedBlock = 0; static uint8_t keepAlive = 10; +static bool wasConnected = false; void SCREENSHOT_ParseInput(void) { @@ -74,11 +75,21 @@ void SCREENSHOT_Update(bool force) } if (keepAlive > 0) { - if (--keepAlive == 0) return; + if (--keepAlive == 0) { + // Connection just lost → reset state for next reconnection + wasConnected = false; + return; + } } else { return; } + // Connection is alive — detect reconnection and force full frame + if (!wasConnected) { + force = true; + wasConnected = true; + } + // ==== BUILD FRAME ONCE ==== // Status line: 8 bit layers × 128 columns for (uint8_t b = 0; b < 8; b++) { From 9574376d3816fbc3f257a61b4c113f567f46c59b Mon Sep 17 00:00:00 2001 From: mrkusypl Date: Mon, 16 Mar 2026 00:52:28 +0100 Subject: [PATCH 19/50] Code refactoring; improvements to K5Viewer and the UI --- App/app/aircopy.c | 27 ++++++-------- App/app/app.c | 10 +++-- App/app/main.c | 87 +++++++++++++++++++++---------------------- App/app/menu.c | 83 ++++++++++++++++++++++++----------------- App/driver/keyboard.c | 62 +++++++++++++++++++++++++----- App/driver/keyboard.h | 19 +++++++--- App/driver/uart.c | 54 +-------------------------- App/driver/vcp.c | 71 +++++------------------------------ App/misc.c | 12 ++++++ App/misc.h | 2 + App/screenshot.c | 31 ++++++--------- App/ui/fmradio.c | 6 +++ App/ui/status.c | 46 ++++++++++------------- App/ui/welcome.c | 6 +-- 14 files changed, 240 insertions(+), 276 deletions(-) diff --git a/App/app/aircopy.c b/App/app/aircopy.c index 34276077..5469818a 100644 --- a/App/app/aircopy.c +++ b/App/app/aircopy.c @@ -331,10 +331,6 @@ void AIRCOPY_StorePacket(void) static void AIRCOPY_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) { - if (bKeyHeld || !bKeyPressed) { - return; - } - INPUTBOX_Append(Key); gRequestDisplayScreen = DISPLAY_AIRCOPY; @@ -379,10 +375,6 @@ static void AIRCOPY_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) static void AIRCOPY_Key_EXIT(bool bKeyPressed, bool bKeyHeld) { - if (bKeyHeld || !bKeyPressed) { - return; - } - if (gInputBoxIndex == 0) { gAircopyStep = 1; gFSKWriteIndex = 0; @@ -405,10 +397,6 @@ static void AIRCOPY_Key_EXIT(bool bKeyPressed, bool bKeyHeld) static void AIRCOPY_Key_MENU(bool bKeyPressed, bool bKeyHeld) { - if (bKeyHeld || !bKeyPressed) { - return; - } - gAircopyStep = 1; gFSKWriteIndex = 0; gAirCopyBlockNumber = 0; @@ -427,10 +415,6 @@ static void AIRCOPY_Key_MENU(bool bKeyPressed, bool bKeyHeld) static void AIRCOPY_Key_UP_DOWN(bool bKeyPressed, bool bKeyHeld, int8_t Direction) { - if (bKeyHeld || !bKeyPressed) { - return; - } - if (!gEeprom.SET_NAV) { Direction = -Direction; } @@ -450,6 +434,14 @@ static void AIRCOPY_Key_UP_DOWN(bool bKeyPressed, bool bKeyHeld, int8_t Directio void AIRCOPY_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) { + if (bKeyHeld || !bKeyPressed) { + return; + } + + if (Key != KEY_PTT) { + gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL; + } + switch (Key) { case KEY_0...KEY_9: AIRCOPY_Key_DIGITS(Key, bKeyPressed, bKeyHeld); @@ -464,7 +456,10 @@ void AIRCOPY_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) case KEY_DOWN: AIRCOPY_Key_UP_DOWN(bKeyPressed, bKeyHeld, Key == KEY_UP ? 1 : -1); break; + case KEY_PTT: + break; default: + gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL; break; } } diff --git a/App/app/app.c b/App/app/app.c index 7f172faf..30a937e0 100644 --- a/App/app/app.c +++ b/App/app/app.c @@ -1609,9 +1609,11 @@ void APP_TimeSlice500ms(void) } #endif + const int m = UI_MENU_GetCurrentMenuId(); + if (gBacklightCountdown_500ms > 0 && !gAskToSave && !gCssBackgroundScan // don't turn off backlight if user is in backlight menu option - && !(gScreenToDisplay == DISPLAY_MENU && (UI_MENU_GetCurrentMenuId() == MENU_ABR || UI_MENU_GetCurrentMenuId() == MENU_ABR_MAX)) + && !(gScreenToDisplay == DISPLAY_MENU && (m == MENU_ABR || m == MENU_ABR_MAX || m == MENU_ABR_MIN)) && --gBacklightCountdown_500ms == 0 && gEeprom.BACKLIGHT_TIME < 61 ) { @@ -1729,8 +1731,10 @@ void APP_TimeSlice500ms(void) if (exit_menu) { gMenuCountdown = 0; - if (gEeprom.BACKLIGHT_TIME == 0) { - BACKLIGHT_TurnOff(); + const int m = UI_MENU_GetCurrentMenuId(); + + if (gScreenToDisplay == DISPLAY_MENU && (m == MENU_ABR || m == MENU_ABR_MAX || m == MENU_ABR_MIN)) { + BACKLIGHT_TurnOn(); } if (gInputBoxIndex > 0 || gDTMF_InputMode) { diff --git a/App/app/main.c b/App/app/main.c index 69f0e4c9..8bef1e97 100644 --- a/App/app/main.c +++ b/App/app/main.c @@ -99,16 +99,15 @@ static void processFKeyFunction(const KEY_Code_t Key, const bool beep) { uint8_t Vfo = gEeprom.TX_VFO; -#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS - if(gEeprom.MENU_LOCK == true) { - if(Key == 2) { // Enable A/B only - gVfoConfigureMode = VFO_CONFIGURE; - COMMON_SwitchVFOs(); - if (beep) - gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL; - } + if (beep) + gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL; - return; // prevent F function if MENU LOCK is true +#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS + if(gEeprom.MENU_LOCK == true && Key != 2) { + gUpdateStatus = true; + gWasFKeyPressed = false; + + return; } #endif @@ -128,7 +127,6 @@ static void processFKeyFunction(const KEY_Code_t Key, const bool beep) if (!IS_FREQ_CHANNEL(gTxVfo->CHANNEL_SAVE)) { gWasFKeyPressed = false; gUpdateStatus = true; - gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL; #ifdef ENABLE_COPY_CHAN_TO_VFO if (!gEeprom.VFO_OPEN || gCssBackgroundScan) { @@ -192,9 +190,6 @@ static void processFKeyFunction(const KEY_Code_t Key, const bool beep) gRequestDisplayScreen = DISPLAY_MAIN; - if (beep) - gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL; - break; case KEY_2: @@ -202,8 +197,6 @@ static void processFKeyFunction(const KEY_Code_t Key, const bool beep) gVfoConfigureMode = VFO_CONFIGURE; #endif COMMON_SwitchVFOs(); - if (beep) - gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL; break; case KEY_3: @@ -211,8 +204,6 @@ static void processFKeyFunction(const KEY_Code_t Key, const bool beep) gVfoConfigureMode = VFO_CONFIGURE; #endif COMMON_SwitchVFOMode(); - if (beep) - gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL; break; @@ -222,8 +213,6 @@ static void processFKeyFunction(const KEY_Code_t Key, const bool beep) gBackup_CROSS_BAND_RX_TX = gEeprom.CROSS_BAND_RX_TX; gEeprom.CROSS_BAND_RX_TX = CROSS_BAND_OFF; gUpdateStatus = true; - if (beep) - gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL; SCANNER_Start(false); gRequestDisplayScreen = DISPLAY_SCANNER; @@ -550,7 +539,13 @@ static void MAIN_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) return; } - gKeyInputCountdown = (gInputBoxIndex == totalDigits) ? (key_input_timeout_500ms / 16) : (key_input_timeout_500ms / 3); + gKeyInputCountdown = (gInputBoxIndex >= totalDigits) ? (key_input_timeout_500ms / 16) : (key_input_timeout_500ms / 3); + + if (gInputBoxIndex >= totalDigits) { + gInputBoxIndex = totalDigits; + + return; + } const char *inputStr = INPUTBOX_GetAscii(); uint8_t inputLength = gInputBoxIndex; @@ -643,6 +638,12 @@ static void MAIN_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) gWasFKeyPressed = false; gUpdateStatus = true; + #ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS + if(gEeprom.MENU_LOCK == true && Key != 2) { + return; + } + #endif + if(Key == 8) { ACTION_BackLightOnDemand(); @@ -656,11 +657,6 @@ static void MAIN_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) #ifdef ENABLE_FEAT_F4HWN_GAME else if(Key == 7) { - #ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS - if(gEeprom.MENU_LOCK == true) { - return; - } - #endif APP_RunBreakout(); return; } @@ -839,7 +835,12 @@ static void MAIN_Key_MENU(bool bKeyPressed, bool bKeyHeld) } #ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS - if(gEeprom.MENU_LOCK == false) { + if(gEeprom.MENU_LOCK == true) { + gUpdateStatus = true; + gWasFKeyPressed = false; + + return; + } #endif gFlagRefreshSetting = true; @@ -847,10 +848,6 @@ static void MAIN_Key_MENU(bool bKeyPressed, bool bKeyHeld) #ifdef ENABLE_VOICE gAnotherVoiceID = VOICE_ID_MENU; #endif - - #ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS - } - #endif } else { gRequestDisplayScreen = DISPLAY_MAIN; @@ -860,13 +857,6 @@ static void MAIN_Key_MENU(bool bKeyPressed, bool bKeyHeld) static void MAIN_Key_STAR(bool bKeyPressed, bool bKeyHeld) { - -#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS - if(gEeprom.MENU_LOCK == true) { - return; // prevent F function if MENU LOCK is true - } -#endif - if (gCurrentFunction == FUNCTION_TRANSMIT) return; @@ -876,6 +866,20 @@ static void MAIN_Key_STAR(bool bKeyPressed, bool bKeyHeld) return; } + if (!bKeyHeld && bKeyPressed) { // star key pressed + gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL; // beep when key is pressed + return; // don't use the key till it's released + } + + #ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS + if(gEeprom.MENU_LOCK == true) { + gUpdateStatus = true; + gWasFKeyPressed = false; + + return; // prevent F function if MENU LOCK is true + } + #endif + if (bKeyHeld && !gWasFKeyPressed){ // long press if (!bKeyPressed) // released return; @@ -898,12 +902,6 @@ static void MAIN_Key_STAR(bool bKeyPressed, bool bKeyHeld) gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL; return; } - - if (bKeyPressed) { // just pressed - return; - } - - // just released if (!gWasFKeyPressed) // pressed without the F-key { @@ -916,7 +914,6 @@ static void MAIN_Key_STAR(bool bKeyPressed, bool bKeyHeld) #endif ) { // start entering a DTMF string - gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL; memcpy(gDTMF_InputBox, gDTMF_String, MIN(sizeof(gDTMF_InputBox), sizeof(gDTMF_String) - 1)); gDTMF_InputBox_Index = 0; gDTMF_InputMode = true; @@ -958,7 +955,7 @@ static void MAIN_Key_UP_DOWN(bool bKeyPressed, bool bKeyHeld, int8_t Direction) #ifdef ENABLE_FEAT_F4HWN // Set Squelch F + UP or Down if(gWasFKeyPressed) { - processFKeyFunction(Direction == 1 ? KEY_UP : KEY_DOWN, false); + processFKeyFunction(Direction == 1 ? KEY_UP : KEY_DOWN, true); return; } #endif diff --git a/App/app/menu.c b/App/app/menu.c index 9c307ef0..6d58b49a 100644 --- a/App/app/menu.c +++ b/App/app/menu.c @@ -1547,12 +1547,14 @@ static void MENU_Key_0_to_9(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) return; } - if (UI_MENU_GetCurrentMenuId() == MENU_MEM_CH || - UI_MENU_GetCurrentMenuId() == MENU_DEL_CH || - UI_MENU_GetCurrentMenuId() == MENU_1_CALL || - UI_MENU_GetCurrentMenuId() == MENU_S_PRI_CH_1 || - UI_MENU_GetCurrentMenuId() == MENU_S_PRI_CH_2 || - UI_MENU_GetCurrentMenuId() == MENU_MEM_NAME) + const int m = UI_MENU_GetCurrentMenuId(); + + if (m == MENU_MEM_CH || + m == MENU_DEL_CH || + m == MENU_1_CALL || + m == MENU_S_PRI_CH_1 || + m == MENU_S_PRI_CH_2 || + m == MENU_MEM_NAME) { // enter 4-digit channel number if (gInputBoxIndex < 4) @@ -1692,19 +1694,21 @@ static void MENU_Key_MENU(const bool bKeyPressed, const bool bKeyHeld) if (!gIsInSubMenu) { + const int m = UI_MENU_GetCurrentMenuId(); + #ifdef ENABLE_VOICE - if (UI_MENU_GetCurrentMenuId() != MENU_SCR) + if (m != MENU_SCR) gAnotherVoiceID = MenuList[gMenuCursor].voice_id; #endif - if (UI_MENU_GetCurrentMenuId() == MENU_UPCODE - || UI_MENU_GetCurrentMenuId() == MENU_DWCODE + if (m == MENU_UPCODE + || m == MENU_DWCODE #ifdef ENABLE_DTMF_CALLING - || UI_MENU_GetCurrentMenuId() == MENU_ANI_ID + || m == MENU_ANI_ID #endif ) return; #if 1 - if (UI_MENU_GetCurrentMenuId() == MENU_DEL_CH || UI_MENU_GetCurrentMenuId() == MENU_MEM_NAME) + if (m == MENU_DEL_CH || m == MENU_MEM_NAME) if (!RADIO_CheckValidChannel(gSubMenuSelection, false, 0)) return; // invalid channel #endif @@ -1712,7 +1716,7 @@ static void MENU_Key_MENU(const bool bKeyPressed, const bool bKeyHeld) gAskForConfirmation = 0; gIsInSubMenu = true; -// if (UI_MENU_GetCurrentMenuId() != MENU_D_LIST) +// if (m != MENU_D_LIST) { gInputBoxIndex = 0; edit_index = -1; @@ -1763,10 +1767,12 @@ static void MENU_Key_MENU(const bool bKeyPressed, const bool bKeyHeld) if (gIsInSubMenu) { - if (UI_MENU_GetCurrentMenuId() == MENU_RESET || - UI_MENU_GetCurrentMenuId() == MENU_MEM_CH || - UI_MENU_GetCurrentMenuId() == MENU_DEL_CH || - UI_MENU_GetCurrentMenuId() == MENU_MEM_NAME) + const int m = UI_MENU_GetCurrentMenuId(); + + if (m == MENU_RESET || + m == MENU_MEM_CH || + m == MENU_DEL_CH || + m == MENU_MEM_NAME) { switch (gAskForConfirmation) { @@ -1853,7 +1859,8 @@ static void MENU_Key_STAR(const bool bKeyPressed, const bool bKeyHeld) if (gRxVfo->Modulation == MODULATION_FM) #endif { - if ((UI_MENU_GetCurrentMenuId() == MENU_R_CTCS || UI_MENU_GetCurrentMenuId() == MENU_R_DCS) && gIsInSubMenu) + const int m = UI_MENU_GetCurrentMenuId(); + if ((m == MENU_R_CTCS || m == MENU_R_DCS) && gIsInSubMenu) { // scan CTCSS or DCS to find the tone/code of the incoming signal if (!SCANNER_IsScanning()) MENU_StartCssScan(); @@ -1925,9 +1932,11 @@ static void MENU_Key_UP_DOWN(bool bKeyPressed, bool bKeyHeld, int8_t Direction) gRequestDisplayScreen = DISPLAY_MENU; - if (UI_MENU_GetCurrentMenuId() != MENU_ABR - && UI_MENU_GetCurrentMenuId() != MENU_ABR_MIN - && UI_MENU_GetCurrentMenuId() != MENU_ABR_MAX + const int m = UI_MENU_GetCurrentMenuId(); + + if (m != MENU_ABR + && m != MENU_ABR_MIN + && m != MENU_ABR_MAX && gEeprom.BACKLIGHT_TIME == 0) // backlight always off and not in the backlight menu { BACKLIGHT_TurnOff(); @@ -1953,8 +1962,10 @@ static void MENU_Key_UP_DOWN(bool bKeyPressed, bool bKeyHeld, int8_t Direction) } VFO = 0; + + const int m = UI_MENU_GetCurrentMenuId(); - switch (UI_MENU_GetCurrentMenuId()) + switch (m) { case MENU_DEL_CH: case MENU_1_CALL: @@ -1970,19 +1981,21 @@ static void MENU_Key_UP_DOWN(bool bKeyPressed, bool bKeyHeld, int8_t Direction) return; } - if(UI_MENU_GetCurrentMenuId() == MENU_S_PRI_CH_1 || UI_MENU_GetCurrentMenuId() == MENU_S_PRI_CH_2) + if(m == MENU_S_PRI_CH_1 || m == MENU_S_PRI_CH_2) { static int16_t last; - if(Direction > 0 && gSubMenuSelection == MR_CHANNELS_MAX) - { - gSubMenuSelection = -1; - last = -1; - } - else if(Direction < 0 && gSubMenuSelection == MR_CHANNELS_MAX) - { - gSubMenuSelection = MR_CHANNELS_MAX; - last = MR_CHANNELS_MAX; + if(gSubMenuSelection == MR_CHANNELS_MAX) { + if(Direction > 0) + { + gSubMenuSelection = -1; + last = -1; + } + else if(Direction < 0) + { + gSubMenuSelection = MR_CHANNELS_MAX; + last = MR_CHANNELS_MAX; + } } Channel = RADIO_FindNextChannel(gSubMenuSelection + Direction, Direction, bCheckScanList, VFO); @@ -2067,11 +2080,13 @@ void MENU_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) if (gScreenToDisplay == DISPLAY_MENU) { - if (UI_MENU_GetCurrentMenuId() == MENU_VOL || + const int m = UI_MENU_GetCurrentMenuId(); + + if (m == MENU_VOL || #ifdef ENABLE_F_CAL_MENU - UI_MENU_GetCurrentMenuId() == MENU_F_CALI || + m == MENU_F_CALI || #endif - UI_MENU_GetCurrentMenuId() == MENU_BATCAL) + m == MENU_BATCAL) { gMenuCountdown = menu_timeout_long_500ms; } diff --git a/App/driver/keyboard.c b/App/driver/keyboard.c index c7f3875c..de3d4a13 100644 --- a/App/driver/keyboard.c +++ b/App/driver/keyboard.c @@ -36,30 +36,72 @@ bool gWasFKeyPressed = false; // Must exceed key_repeat_delay_10ms (40) to trigger ProcessKey(key, true, true). #define SERIAL_KEY_LONG_POLLS 45 +// Packet types for serial key injection (K5Viewer → radio) +#define SERIAL_KEY_TYPE 0x03 +#define SERIAL_KEY_TYPE_LONG 0x04 + volatile KEY_Code_t gKeyFromSerial = KEY_INVALID; static uint8_t gSerialKeyHoldCount = 0; static uint8_t gSerialKeyLong = 0; // 0 = short press, 1 = long press -// Inject a short press from serial (UART or VCP). +// Inject a short or long press from serial (UART or VCP). // KEY_PTT is explicitly blocked — PTT release cannot be guaranteed over serial. -void KEYBOARD_InjectKey(uint8_t keyCode) +void KEYBOARD_InjectKey(uint8_t keyCode, bool keyLong) { if (keyCode < KEY_INVALID && keyCode != KEY_PTT) { gKeyFromSerial = (KEY_Code_t)keyCode; gSerialKeyHoldCount = 0; - gSerialKeyLong = 0; + gSerialKeyLong = keyLong; } } -// Inject a long press from serial (UART or VCP). -// KEY_PTT is explicitly blocked — PTT release cannot be guaranteed over serial. -void KEYBOARD_InjectKeyLong(uint8_t keyCode) +bool KEYBOARD_ProcessProtocolByte(ParseState_t *state, uint8_t b) { - if (keyCode < KEY_INVALID && keyCode != KEY_PTT) { - gKeyFromSerial = (KEY_Code_t)keyCode; - gSerialKeyHoldCount = 0; - gSerialKeyLong = 1; + bool connected = false; + + switch (*state) + { + case STATE_IDLE: + if (b == 0x55) *state = STATE_KA_1; + else if (b == 0xAA) *state = STATE_KEY_1; + break; + + case STATE_KA_1: + *state = (b == 0xAA) ? STATE_KA_2 : STATE_IDLE; + break; + + case STATE_KA_2: + *state = (b == 0x00) ? STATE_KA_3 : STATE_IDLE; + break; + + case STATE_KA_3: + if (b == 0x00) connected = true; + *state = STATE_IDLE; + break; + + case STATE_KEY_1: + *state = (b == 0x55) ? STATE_KEY_2 : STATE_IDLE; + break; + + case STATE_KEY_2: + if (b == SERIAL_KEY_TYPE) *state = STATE_KEY_3; + else if (b == SERIAL_KEY_TYPE_LONG) *state = STATE_KEY_3L; + else *state = STATE_IDLE; + break; + + case STATE_KEY_3: + case STATE_KEY_3L: + KEYBOARD_InjectKey(b, *state == STATE_KEY_3L); + connected = true; + *state = STATE_IDLE; + break; + + default: + *state = STATE_IDLE; + break; } + + return connected; } #endif diff --git a/App/driver/keyboard.h b/App/driver/keyboard.h index 1decc46b..b9dfb97c 100644 --- a/App/driver/keyboard.h +++ b/App/driver/keyboard.h @@ -45,6 +45,17 @@ enum KEY_Code_e { }; typedef enum KEY_Code_e KEY_Code_t; +typedef enum { + STATE_IDLE = 0, + STATE_KA_1, + STATE_KA_2, + STATE_KA_3, + STATE_KEY_1, + STATE_KEY_2, + STATE_KEY_3, + STATE_KEY_3L, +} ParseState_t; + extern KEY_Code_t gKeyReading0; extern KEY_Code_t gKeyReading1; extern uint16_t gDebounceCounter; @@ -54,11 +65,9 @@ extern bool gWasFKeyPressed; // Serial-injected key (written by UART/VCP parser, consumed by KEYBOARD_Poll). extern volatile KEY_Code_t gKeyFromSerial; -// Inject a short press received from serial (UART or VCP). -void KEYBOARD_InjectKey(uint8_t keyCode); - -// Inject a long press received from serial (UART or VCP). -void KEYBOARD_InjectKeyLong(uint8_t keyCode); +// Inject a short or long press received from serial (UART or VCP). +void KEYBOARD_InjectKey(uint8_t keyCode, bool keyLong); +bool KEYBOARD_ProcessProtocolByte(ParseState_t *state, uint8_t b); #endif KEY_Code_t KEYBOARD_Poll(void); diff --git a/App/driver/uart.c b/App/driver/uart.c index 73c87d6b..b9b0e2a6 100644 --- a/App/driver/uart.c +++ b/App/driver/uart.c @@ -25,9 +25,6 @@ #ifdef ENABLE_FEAT_F4HWN_SCREENSHOT #include "driver/keyboard.h" -// Packet types for serial key injection (K5Viewer → radio) -#define UART_TYPE_KEY 0x03 -#define UART_TYPE_KEY_LONG 0x04 #endif #define USARTx USART1 @@ -155,17 +152,6 @@ void UART_LogSend(const void *pBuffer, uint32_t Size) #ifdef ENABLE_FEAT_F4HWN_SCREENSHOT bool UART_IsCableConnected(void) { - typedef enum { - STATE_IDLE = 0, - STATE_KA_1, - STATE_KA_2, - STATE_KA_3, - STATE_KEY_1, - STATE_KEY_2, - STATE_KEY_3, - STATE_KEY_3L, - } ParseState_t; - static uint8_t read_ptr = 0; static ParseState_t state = STATE_IDLE; @@ -182,44 +168,8 @@ void UART_LogSend(const void *pBuffer, uint32_t Size) // read_ptr wraps naturally at 256 since it's uint8_t processed++; - switch (state) - { - case STATE_IDLE: - if (b == 0x55) state = STATE_KA_1; - else if (b == 0xAA) state = STATE_KEY_1; - break; - case STATE_KA_1: - state = (b == 0xAA) ? STATE_KA_2 : STATE_IDLE; - break; - case STATE_KA_2: - state = (b == 0x00) ? STATE_KA_3 : STATE_IDLE; - break; - case STATE_KA_3: - if (b == 0x00) connected = true; - state = STATE_IDLE; - break; - case STATE_KEY_1: - state = (b == 0x55) ? STATE_KEY_2 : STATE_IDLE; - break; - case STATE_KEY_2: - if (b == UART_TYPE_KEY) state = STATE_KEY_3; - else if (b == UART_TYPE_KEY_LONG) state = STATE_KEY_3L; - else state = STATE_IDLE; - break; - case STATE_KEY_3: - KEYBOARD_InjectKey(b); - connected = true; - state = STATE_IDLE; - break; - case STATE_KEY_3L: - KEYBOARD_InjectKeyLong(b); - connected = true; - state = STATE_IDLE; - break; - default: - state = STATE_IDLE; - break; - } + if(KEYBOARD_ProcessProtocolByte(&state, b)) + connected = true; } return connected; diff --git a/App/driver/vcp.c b/App/driver/vcp.c index 6082fc72..f9ccbb5e 100644 --- a/App/driver/vcp.c +++ b/App/driver/vcp.c @@ -20,9 +20,6 @@ #ifdef ENABLE_FEAT_F4HWN_SCREENSHOT #include "driver/keyboard.h" -// Packet types for serial key injection (K5Viewer → radio) -#define VCP_TYPE_KEY 0x03 -#define VCP_TYPE_KEY_LONG 0x04 #endif uint8_t VCP_RxBuf[VCP_RX_BUF_SIZE]; @@ -63,19 +60,8 @@ bool VCP_ScreenshotPing(void) // KEY_1 → 0x55 → KEY_2 (else IDLE) // KEY_2 → 0x03 → KEY_3 (short press, else check long) // KEY_2 → 0x04 → KEY_3L (long press, else IDLE) - // KEY_3 → → InjectKey(b), IDLE - // KEY_3L → → InjectKeyLong(b), IDLE - - typedef enum { - STATE_IDLE = 0, - STATE_KA_1, - STATE_KA_2, - STATE_KA_3, - STATE_KEY_1, - STATE_KEY_2, - STATE_KEY_3, - STATE_KEY_3L, - } ParseState_t; + // KEY_3 → → KEYBOARD_InjectKey(b, false), IDLE + // KEY_3L → → KEYBOARD_InjectKey(b, true), IDLE static uint32_t read_ptr = 0; static ParseState_t state = STATE_IDLE; @@ -91,57 +77,18 @@ bool VCP_ScreenshotPing(void) while (read_ptr != write_ptr && processed < VCP_RX_BUF_SIZE) { uint8_t b = VCP_RxBuf[read_ptr]; + + // Clear the old byte after reading - prevents the button from being + // pressed automatically after a certain amount of time. + VCP_RxBuf[read_ptr] = 0x00; + read_ptr++; if (read_ptr >= VCP_RX_BUF_SIZE) read_ptr = 0; processed++; - switch (state) - { - case STATE_IDLE: - if (b == 0x55) state = STATE_KA_1; - else if (b == 0xAA) state = STATE_KEY_1; - break; - - case STATE_KA_1: - state = (b == 0xAA) ? STATE_KA_2 : STATE_IDLE; - break; - - case STATE_KA_2: - state = (b == 0x00) ? STATE_KA_3 : STATE_IDLE; - break; - - case STATE_KA_3: - if (b == 0x00) connected = true; - state = STATE_IDLE; - break; - - case STATE_KEY_1: - state = (b == 0x55) ? STATE_KEY_2 : STATE_IDLE; - break; - - case STATE_KEY_2: - if (b == VCP_TYPE_KEY) state = STATE_KEY_3; - else if (b == VCP_TYPE_KEY_LONG) state = STATE_KEY_3L; - else state = STATE_IDLE; - break; - - case STATE_KEY_3: - KEYBOARD_InjectKey(b); - connected = true; - state = STATE_IDLE; - break; - - case STATE_KEY_3L: - KEYBOARD_InjectKeyLong(b); - connected = true; - state = STATE_IDLE; - break; - - default: - state = STATE_IDLE; - break; - } + if(KEYBOARD_ProcessProtocolByte(&state, b)) + connected = true; } return connected; diff --git a/App/misc.c b/App/misc.c index b923617e..372d3708 100644 --- a/App/misc.c +++ b/App/misc.c @@ -634,4 +634,16 @@ void MR_PrintCacheStats(void) // cache_hits, cache_misses, MR_GetCacheHitRate()); } +#endif + +#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT + bool SCREENSHOT_IsLocked(void) + { + if (gUART_LockScreenshot > 0) { + gUART_LockScreenshot--; + return true; + } + + return false; + } #endif \ No newline at end of file diff --git a/App/misc.h b/App/misc.h index d9179a93..7b1df678 100644 --- a/App/misc.h +++ b/App/misc.h @@ -323,6 +323,8 @@ extern volatile bool gTxTimeoutReached; #ifdef ENABLE_FEAT_F4HWN_SCREENSHOT extern volatile uint8_t gUART_LockScreenshot; // lock screenshot if Chirp is used extern bool gUSB_ScreenshotEnabled; + + bool SCREENSHOT_IsLocked(void); #endif #endif diff --git a/App/screenshot.c b/App/screenshot.c index 7fd1544d..bfef82e8 100644 --- a/App/screenshot.c +++ b/App/screenshot.c @@ -26,17 +26,19 @@ // - No currentFrame or deltaFrame static buffers static uint8_t previousFrame[1024] = {0}; static uint8_t forcedBlock = 0; -static uint8_t keepAlive = 10; -static bool wasConnected = false; +static uint8_t keepAlive = 0; void SCREENSHOT_ParseInput(void) { + if (SCREENSHOT_IsLocked()) + return; + if (UART_IsCableConnected()) { - keepAlive = 10; + keepAlive = 7; gUSB_ScreenshotEnabled = false; } - if (VCP_ScreenshotPing()) { - keepAlive = 10; + else if (VCP_ScreenshotPing()) { + keepAlive = 7; gUSB_ScreenshotEnabled = true; } } @@ -58,21 +60,10 @@ void SCREENSHOT_Update(bool force) uint16_t index = 0; uint8_t acc = 0; uint8_t bitCount = 0; + static bool wasConnected = false; - if (gUART_LockScreenshot > 0) { - gUART_LockScreenshot--; + if (SCREENSHOT_IsLocked()) return; - } - - if (UART_IsCableConnected()) { - keepAlive = 10; - gUSB_ScreenshotEnabled = false; - } - - if (VCP_ScreenshotPing()) { - keepAlive = 10; - gUSB_ScreenshotEnabled = true; - } if (keepAlive > 0) { if (--keepAlive == 0) { @@ -97,7 +88,7 @@ void SCREENSHOT_Update(bool force) uint8_t bit = (gStatusLine[i] >> b) & 0x01; acc |= (bit << bitCount++); if (bitCount == 8) { - frameBuffer[index++] = acc; + frameBuffer[index++] = gSetting_set_inv ? ~acc : acc; acc = 0; bitCount = 0; } @@ -111,7 +102,7 @@ void SCREENSHOT_Update(bool force) uint8_t bit = (gFrameBuffer[l][i] >> b) & 0x01; acc |= (bit << bitCount++); if (bitCount == 8) { - frameBuffer[index++] = acc; + frameBuffer[index++] = gSetting_set_inv ? ~acc : acc; acc = 0; bitCount = 0; } diff --git a/App/ui/fmradio.c b/App/ui/fmradio.c index fc1e3096..02dd5920 100644 --- a/App/ui/fmradio.c +++ b/App/ui/fmradio.c @@ -35,6 +35,12 @@ void UI_DisplayFM(void) char *pPrintStr = String; UI_DisplayClear(); +#ifdef ENABLE_FEAT_F4HWN + if (gEeprom.KEY_LOCK && gKeypadLocked > 0) { + UI_PrintStringSmallBold("UNLOCK KEYBOARD", 12, 0, 5); + } +#endif + UI_PrintString("FM", 2, 0, 0, 8); sprintf(String, "%d%s-%dM", diff --git a/App/ui/status.c b/App/ui/status.c index cb37526e..b2a97bc9 100644 --- a/App/ui/status.c +++ b/App/ui/status.c @@ -173,31 +173,32 @@ void UI_DisplayStatus() else #endif { - #ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS - if(gEeprom.MENU_LOCK == true) { - memcpy(line + x + 2, gFontRO, sizeof(gFontRO)); - } - else - { - #endif - uint8_t dw = (gEeprom.DUAL_WATCH != DUAL_WATCH_OFF) + (gEeprom.CROSS_BAND_RX_TX != CROSS_BAND_OFF) * 2; - if(dw == 1 || dw == 3) { // DWR - dual watch + respond - if(gDualWatchActive) - memcpy(line + x + (dw==1?0:2), gFontDWR, sizeof(gFontDWR) - (dw==1?0:5)); - else - memcpy(line + x + 3, gFontHold, sizeof(gFontHold)); - } - else if(dw == 2) { // XB - crossband - memcpy(line + x + 2, gFontXB, sizeof(gFontXB)); + if(!gAirCopyBootMode) { + #ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS + if(gEeprom.MENU_LOCK == true) { + memcpy(line + x + 2, gFontRO, sizeof(gFontRO)); } else { - if(!gAirCopyBootMode) + #endif + uint8_t dw = (gEeprom.DUAL_WATCH != DUAL_WATCH_OFF) + (gEeprom.CROSS_BAND_RX_TX != CROSS_BAND_OFF) * 2; + if(dw == 1 || dw == 3) { // DWR - dual watch + respond + if(gDualWatchActive) + memcpy(line + x + (dw==1?0:2), gFontDWR, sizeof(gFontDWR) - (dw==1?0:5)); + else + memcpy(line + x + 3, gFontHold, sizeof(gFontHold)); + } + else if(dw == 2) { // XB - crossband + memcpy(line + x + 2, gFontXB, sizeof(gFontXB)); + } + else + { memcpy(line + x + 2, gFontMO, sizeof(gFontMO)); + } + #ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS } - #ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS + #endif } - #endif } } x += sizeof(gFontDWR) + 3; @@ -239,15 +240,8 @@ void UI_DisplayStatus() size = sizeof(gFontKeyLock); } else if (gWasFKeyPressed) { - #ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS - if (!gEeprom.MENU_LOCK) { - src = gFontF; - size = sizeof(gFontF); - } - #else src = gFontF; size = sizeof(gFontF); - #endif } #ifdef ENABLE_FEAT_F4HWN else if (gMute) { diff --git a/App/ui/welcome.c b/App/ui/welcome.c index 0cecf63b..d94057fe 100644 --- a/App/ui/welcome.c +++ b/App/ui/welcome.c @@ -221,8 +221,8 @@ void UI_DisplayWelcome(void) //ST7565_BlitStatusLine(); // blank status line : I think it's useless ST7565_BlitFullScreen(); - #ifdef ENABLE_FEAT_F4HWN_SCREENSHOT - SCREENSHOT_Update(true); - #endif + // #ifdef ENABLE_FEAT_F4HWN_SCREENSHOT + // SCREENSHOT_Update(true); // I think that's useless too + // #endif } } \ No newline at end of file From f4d8abd7d93a12098c131e58034d1ef5869dd30a Mon Sep 17 00:00:00 2001 From: mrkusypl Date: Mon, 16 Mar 2026 14:57:57 +0100 Subject: [PATCH 20/50] Increase the keepAlive value, fix the CHIRP connection while maintaining the fix for the button getting stuck --- App/driver/vcp.c | 13 ++++++++----- App/screenshot.c | 4 ++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/App/driver/vcp.c b/App/driver/vcp.c index f9ccbb5e..054dcf41 100644 --- a/App/driver/vcp.c +++ b/App/driver/vcp.c @@ -78,17 +78,20 @@ bool VCP_ScreenshotPing(void) { uint8_t b = VCP_RxBuf[read_ptr]; - // Clear the old byte after reading - prevents the button from being - // pressed automatically after a certain amount of time. - VCP_RxBuf[read_ptr] = 0x00; - read_ptr++; if (read_ptr >= VCP_RX_BUF_SIZE) read_ptr = 0; processed++; - if(KEYBOARD_ProcessProtocolByte(&state, b)) + if(KEYBOARD_ProcessProtocolByte(&state, b)) { + // Clear the last 4 bytes after reading - prevents the button from + // being pressed automatically after a certain amount of time. + for (uint8_t i = 1; i <= 4; i++) { + VCP_RxBuf[(write_ptr - i) & 0xFF] = 0x00; + } + connected = true; + } } return connected; diff --git a/App/screenshot.c b/App/screenshot.c index bfef82e8..09fe54e2 100644 --- a/App/screenshot.c +++ b/App/screenshot.c @@ -34,11 +34,11 @@ void SCREENSHOT_ParseInput(void) return; if (UART_IsCableConnected()) { - keepAlive = 7; + keepAlive = 15; gUSB_ScreenshotEnabled = false; } else if (VCP_ScreenshotPing()) { - keepAlive = 7; + keepAlive = 15; gUSB_ScreenshotEnabled = true; } } From 67231fd326fae26fed24aa7d8bb8be649c0958d9 Mon Sep 17 00:00:00 2001 From: mrkusypl Date: Mon, 16 Mar 2026 15:04:45 +0100 Subject: [PATCH 21/50] Replacing 'write_ptr' with 'read_ptr' when deleting old bytes --- App/driver/vcp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/App/driver/vcp.c b/App/driver/vcp.c index 054dcf41..8e136d9c 100644 --- a/App/driver/vcp.c +++ b/App/driver/vcp.c @@ -87,7 +87,7 @@ bool VCP_ScreenshotPing(void) // Clear the last 4 bytes after reading - prevents the button from // being pressed automatically after a certain amount of time. for (uint8_t i = 1; i <= 4; i++) { - VCP_RxBuf[(write_ptr - i) & 0xFF] = 0x00; + VCP_RxBuf[(read_ptr - i) & 0xFF] = 0x00; } connected = true; From 315d7a6b591c9e94a0cc99b0d4f8072fd0779b8b Mon Sep 17 00:00:00 2001 From: mrkusypl Date: Mon, 16 Mar 2026 23:57:17 +0100 Subject: [PATCH 22/50] An even better way to fix a button that keeps getting stuck in K5Viewer --- App/driver/vcp.c | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/App/driver/vcp.c b/App/driver/vcp.c index 8e136d9c..021b6359 100644 --- a/App/driver/vcp.c +++ b/App/driver/vcp.c @@ -63,11 +63,11 @@ bool VCP_ScreenshotPing(void) // KEY_3 → → KEYBOARD_InjectKey(b, false), IDLE // KEY_3L → → KEYBOARD_InjectKey(b, true), IDLE - static uint32_t read_ptr = 0; + static uint8_t read_ptr = 0; static ParseState_t state = STATE_IDLE; bool connected = false; - uint32_t write_ptr = VCP_RxBufPointer; // snapshot once — ISR may update concurrently + uint8_t write_ptr = VCP_RxBufPointer; // snapshot once — ISR may update concurrently // Cap bytes processed per call to VCP_RX_BUF_SIZE. // Prevents unbounded loop if the ISR write pointer laps read_ptr @@ -83,15 +83,8 @@ bool VCP_ScreenshotPing(void) read_ptr = 0; processed++; - if(KEYBOARD_ProcessProtocolByte(&state, b)) { - // Clear the last 4 bytes after reading - prevents the button from - // being pressed automatically after a certain amount of time. - for (uint8_t i = 1; i <= 4; i++) { - VCP_RxBuf[(read_ptr - i) & 0xFF] = 0x00; - } - + if(KEYBOARD_ProcessProtocolByte(&state, b)) connected = true; - } } return connected; From e77e558d646bbeea04feaf78c09d26e76364dbf7 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Wed, 18 Mar 2026 01:07:03 +0100 Subject: [PATCH 23/50] Revert welcome.c and fix default keepAlive to 3 (PR #218) --- App/screenshot.c | 2 +- App/ui/welcome.c | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/App/screenshot.c b/App/screenshot.c index 09fe54e2..e73355bb 100644 --- a/App/screenshot.c +++ b/App/screenshot.c @@ -26,7 +26,7 @@ // - No currentFrame or deltaFrame static buffers static uint8_t previousFrame[1024] = {0}; static uint8_t forcedBlock = 0; -static uint8_t keepAlive = 0; +static uint8_t keepAlive = 3; void SCREENSHOT_ParseInput(void) { diff --git a/App/ui/welcome.c b/App/ui/welcome.c index d94057fe..0cecf63b 100644 --- a/App/ui/welcome.c +++ b/App/ui/welcome.c @@ -221,8 +221,8 @@ void UI_DisplayWelcome(void) //ST7565_BlitStatusLine(); // blank status line : I think it's useless ST7565_BlitFullScreen(); - // #ifdef ENABLE_FEAT_F4HWN_SCREENSHOT - // SCREENSHOT_Update(true); // I think that's useless too - // #endif + #ifdef ENABLE_FEAT_F4HWN_SCREENSHOT + SCREENSHOT_Update(true); + #endif } } \ No newline at end of file From a1f2f0cdb93be0cebf5fd96f2b107c1001006dd7 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Thu, 19 Mar 2026 03:41:54 +0100 Subject: [PATCH 24/50] Fix: replace map() with IARU-compliant S-meter thresholds --- App/ui/main.c | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/App/ui/main.c b/App/ui/main.c index 46770676..ef86b60c 100644 --- a/App/ui/main.c +++ b/App/ui/main.c @@ -328,22 +328,27 @@ void DisplayRSSIBar(const bool now) #endif + dBmCorrTable[gRxVfo->Band]; - // S9 = -93 dBm, S0 = -141 dBm (IARU standard) - const int16_t s9_dBm = -93; - const int16_t s0_dBm = -141; - + // IARU VHF/UHF S-meter: S9 = -93 dBm, 1 S-unit = 6 dB + // S(n) threshold = -93 + (n - 9) * 6 uint8_t s_level = 0; uint8_t overS9dBm = 0; uint8_t overS9Bars = 0; - if (rssi_dBm <= s9_dBm) { - // Signal <= S9 : map between S0 and S9 - s_level = map(rssi_dBm, s0_dBm, s9_dBm, 0, 9); - } else { - // Signal > S9 : compute over-S9 - s_level = 9; - overS9dBm = map(rssi_dBm, s9_dBm, s9_dBm + 40, 0, 40); - overS9Bars = map(overS9dBm, 0, 40, 0, 4); + if (rssi_dBm >= -93) s_level = 9; // S9 = -93 dBm + else if (rssi_dBm >= -99) s_level = 8; // S8 = -99 dBm + else if (rssi_dBm >= -105) s_level = 7; // S7 = -105 dBm + else if (rssi_dBm >= -111) s_level = 6; // S6 = -111 dBm + else if (rssi_dBm >= -117) s_level = 5; // S5 = -117 dBm + else if (rssi_dBm >= -123) s_level = 4; // S4 = -123 dBm + else if (rssi_dBm >= -129) s_level = 3; // S3 = -129 dBm + else if (rssi_dBm >= -135) s_level = 2; // S2 = -135 dBm + else if (rssi_dBm >= -141) s_level = 1; // S1 = -141 dBm + else s_level = 0; // S0 (below -141 dBm) + + if (s_level == 9) { + // Compute over-S9 dB directly + overS9dBm = (uint8_t)MIN(rssi_dBm - (-93), 40); + overS9Bars = overS9dBm / 10; } #else const int16_t s0_dBm = -gEeprom.S0_LEVEL; // S0 .. base level @@ -1619,4 +1624,4 @@ void UI_DisplayMain(void) #endif ST7565_BlitFullScreen(); -} +} \ No newline at end of file From e28177cb9b5ab50c43c4fd66dc56bd994aaeed40 Mon Sep 17 00:00:00 2001 From: mrkusypl Date: Thu, 19 Mar 2026 23:09:39 +0100 Subject: [PATCH 25/50] Major code refactoring --- App/app/action.c | 57 ++++-------------- App/app/aircopy.c | 17 +++--- App/app/app.c | 136 +++++++++++++++++------------------------- App/app/breakout.c | 14 +---- App/app/chFrScanner.c | 9 +++ App/app/chFrScanner.h | 4 ++ App/app/flashlight.c | 32 ++++++---- App/app/fm.c | 22 ++++--- App/app/main.c | 5 +- App/app/spectrum.c | 12 +--- App/driver/keyboard.c | 12 +++- App/driver/keyboard.h | 3 +- App/main.c | 89 ++++++++------------------- App/screenshot.c | 36 +++++------ App/settings.c | 24 ++++---- App/ui/main.c | 29 +++++---- 16 files changed, 209 insertions(+), 292 deletions(-) diff --git a/App/app/action.c b/App/app/action.c index fd53936f..753f2e1c 100644 --- a/App/app/action.c +++ b/App/app/action.c @@ -554,55 +554,24 @@ void ACTION_Wn(void) BK4819_SetFilterBandwidth(BK4819_FILTER_BW_AM, true); return; } + + const bool isRx = FUNCTION_IsRx(); + + VFO_Info_t *pVfo = isRx ? gRxVfo : gTxVfo; + pVfo->CHANNEL_BANDWIDTH = !pVfo->CHANNEL_BANDWIDTH; + uint8_t bw = pVfo->CHANNEL_BANDWIDTH; + #ifdef ENABLE_FEAT_F4HWN_NARROWER - bool narrower = 0; - if (FUNCTION_IsRx()) + if (isRx && bw == BANDWIDTH_NARROW && gSetting_set_nfm == 1) { - gRxVfo->CHANNEL_BANDWIDTH = (gRxVfo->CHANNEL_BANDWIDTH == 0) ? 1: 0; - if(gRxVfo->CHANNEL_BANDWIDTH == BANDWIDTH_NARROW && gSetting_set_nfm == 1) - { - narrower = 1; - } - - #ifdef ENABLE_AM_FIX - BK4819_SetFilterBandwidth(gRxVfo->CHANNEL_BANDWIDTH + narrower, true); - #else - BK4819_SetFilterBandwidth(gRxVfo->CHANNEL_BANDWIDTH + narrower, false); - #endif + bw++; } - else - { - gTxVfo->CHANNEL_BANDWIDTH = (gTxVfo->CHANNEL_BANDWIDTH == 0) ? 1: 0; - if(gTxVfo->CHANNEL_BANDWIDTH == BANDWIDTH_NARROW && gSetting_set_nfm == 1) - { - narrower = 1; - } + #endif - #ifdef ENABLE_AM_FIX - BK4819_SetFilterBandwidth(gTxVfo->CHANNEL_BANDWIDTH, true); - #else - BK4819_SetFilterBandwidth(gTxVfo->CHANNEL_BANDWIDTH, false); - #endif - } + #ifdef ENABLE_AM_FIX + BK4819_SetFilterBandwidth(bw, true); #else - if (FUNCTION_IsRx()) - { - gRxVfo->CHANNEL_BANDWIDTH = (gRxVfo->CHANNEL_BANDWIDTH == 0) ? 1: 0; - #ifdef ENABLE_AM_FIX - BK4819_SetFilterBandwidth(gRxVfo->CHANNEL_BANDWIDTH, true); - #else - BK4819_SetFilterBandwidth(gRxVfo->CHANNEL_BANDWIDTH, false); - #endif - } - else - { - gTxVfo->CHANNEL_BANDWIDTH = (gTxVfo->CHANNEL_BANDWIDTH == 0) ? 1: 0; - #ifdef ENABLE_AM_FIX - BK4819_SetFilterBandwidth(gTxVfo->CHANNEL_BANDWIDTH, true); - #else - BK4819_SetFilterBandwidth(gTxVfo->CHANNEL_BANDWIDTH, false); - #endif - } + BK4819_SetFilterBandwidth(bw, false); #endif } diff --git a/App/app/aircopy.c b/App/app/aircopy.c index 5469818a..60991e5c 100644 --- a/App/app/aircopy.c +++ b/App/app/aircopy.c @@ -172,6 +172,13 @@ static inline void AIRCOPY_CheckComplete(void) } } +static inline void AIRCOPY_Obfuscation(void) +{ + for (unsigned int i = 0; i < 34; i++) { + g_FSK_Buffer[i + 1] ^= Obfuscation[i % 8]; + } +} + // ============================================================================ // Send/Receive Functions // ============================================================================ @@ -223,9 +230,7 @@ bool AIRCOPY_SendMessage(void) g_FSK_Buffer[34] = CRC_Calculate(&g_FSK_Buffer[1], 2 + 64); - for (unsigned int i = 0; i < 34; i++) { - g_FSK_Buffer[i + 1] ^= Obfuscation[i % 8]; - } + AIRCOPY_Obfuscation(); RADIO_SetTxParameters(); @@ -261,9 +266,7 @@ void AIRCOPY_StorePacket(void) return; } - for (unsigned int i = 0; i < 34; i++) { - g_FSK_Buffer[i + 1] ^= Obfuscation[i % 8]; - } + AIRCOPY_Obfuscation(); uint16_t Crc = CRC_Calculate(&g_FSK_Buffer[1], 2 + 64); if (g_FSK_Buffer[34] != Crc) { @@ -291,7 +294,7 @@ void AIRCOPY_StorePacket(void) } const AIRCOPY_Segment_t *seg = AIRCOPY_FindSegmentForOffset(Offset); - + if (seg == NULL) { gErrorsDuringAirCopy++; AIRCOPY_CheckComplete(); diff --git a/App/app/app.c b/App/app/app.c index 30a937e0..3b63c23d 100644 --- a/App/app/app.c +++ b/App/app/app.c @@ -1165,8 +1165,21 @@ void APP_Update(void) } } +void StopTransmitting(void) { + ProcessKey(KEY_PTT, false, false); + gPttIsPressed = false; + if (gKeyReading1 != KEY_INVALID) + gPttWasReleased = true; + + #ifdef ENABLE_FEAT_F4HWN + #if defined(ENABLE_FEAT_F4HWN_CTR) || defined(ENABLE_FEAT_F4HWN_INV) + ST7565_ContrastAndInv(); + #endif + #endif +} + // called every 10ms -static void CheckKeys(void) +void CheckKeys(void) { #ifdef ENABLE_DTMF_CALLING if(gSetting_KILLED){ @@ -1181,75 +1194,63 @@ static void CheckKeys(void) #endif // -------------------- PTT ------------------------ + const bool serialConfigInProgress = SerialConfigInProgress(); + +#ifdef ENABLE_FEAT_F4HWN + const bool isPressed = GPIO_IsPttPressed() && !serialConfigInProgress; +#else + const bool isPressed = !GPIO_CheckBit(&GPIOC->DATA, GPIOC_PIN_PTT) && !serialConfigInProgress; +#endif + #ifdef ENABLE_FEAT_F4HWN if (gSetting_set_ptt_session) { - if (GPIO_IsPttPressed() && !SerialConfigInProgress() && gPttOnePushCounter == 0) - { // PTT pressed - if (++gPttDebounceCounter >= 3) // 30ms - { // start transmitting - boot_counter_10ms = 0; + if ((isPressed && (gPttOnePushCounter == 0 || gPttOnePushCounter == 2)) || + (!isPressed && (gPttOnePushCounter == 1 || gPttOnePushCounter == 3)) || + serialConfigInProgress) + { + if (++gPttDebounceCounter >= 3 || (serialConfigInProgress && gPttOnePushCounter > 0)) + { gPttDebounceCounter = 0; - gPttIsPressed = true; - gPttOnePushCounter = 1; - ProcessKey(KEY_PTT, true, false); + + if (gPttOnePushCounter == 0) + { // start transmitting + boot_counter_10ms = 0; + gPttIsPressed = true; + gPttOnePushCounter = 1; + ProcessKey(KEY_PTT, true, false); + } + else if (gPttOnePushCounter == 3 || serialConfigInProgress) + { // stop transmitting + StopTransmitting(); + gPttOnePushCounter = 0; + } + else + gPttOnePushCounter++; } - } - else if ((!GPIO_IsPttPressed() || SerialConfigInProgress()) && gPttOnePushCounter == 1) - { - // PTT released or serial comms config in progress - if (++gPttDebounceCounter >= 3 || SerialConfigInProgress()) // 30ms - { // stop transmitting - gPttOnePushCounter = 2; - } - } - else if (GPIO_IsPttPressed() && !SerialConfigInProgress() && gPttOnePushCounter == 2) - { // PTT pressed again - if (++gPttDebounceCounter >= 3 || SerialConfigInProgress()) // 30ms - { // stop transmitting - gPttOnePushCounter = 3; - } - } - else if ((!GPIO_IsPttPressed() || SerialConfigInProgress()) && gPttOnePushCounter == 3) - { // PTT released or serial comms config in progress - if (++gPttDebounceCounter >= 3 || SerialConfigInProgress()) // 30ms - { // stop transmitting - ProcessKey(KEY_PTT, false, false); - gPttIsPressed = false; - if (gKeyReading1 != KEY_INVALID) - gPttWasReleased = true; - gPttOnePushCounter = 0; - #if defined(ENABLE_FEAT_F4HWN_CTR) || defined(ENABLE_FEAT_F4HWN_INV) - ST7565_ContrastAndInv(); - #endif - } - } + } else gPttDebounceCounter = 0; //gDebug = gPttOnePushCounter; - } - else + } + else +#endif { if (gPttIsPressed) { - if (!GPIO_IsPttPressed() || SerialConfigInProgress()) + if (!isPressed) { // PTT released or serial comms config in progress - if (++gPttDebounceCounter >= 3 || SerialConfigInProgress()) // 30ms + if (++gPttDebounceCounter >= 3 || serialConfigInProgress) // 30ms { // stop transmitting - ProcessKey(KEY_PTT, false, false); - gPttIsPressed = false; - if (gKeyReading1 != KEY_INVALID) - gPttWasReleased = true; - #if defined(ENABLE_FEAT_F4HWN_CTR) || defined(ENABLE_FEAT_F4HWN_INV) - ST7565_ContrastAndInv(); - #endif + gPttDebounceCounter = 0; + StopTransmitting(); } - } - else + } + else gPttDebounceCounter = 0; } - else if (GPIO_IsPttPressed() && !SerialConfigInProgress()) + else if (isPressed) { // PTT pressed if (++gPttDebounceCounter >= 3) // 30ms { // start transmitting @@ -1259,38 +1260,9 @@ static void CheckKeys(void) ProcessKey(KEY_PTT, true, false); } } - else - gPttDebounceCounter = 0; - } -#else - if (gPttIsPressed) - { - if (GPIO_CheckBit(&GPIOC->DATA, GPIOC_PIN_PTT) || SerialConfigInProgress()) - { // PTT released or serial comms config in progress - if (++gPttDebounceCounter >= 3 || SerialConfigInProgress()) // 30ms - { // stop transmitting - ProcessKey(KEY_PTT, false, false); - gPttIsPressed = false; - if (gKeyReading1 != KEY_INVALID) - gPttWasReleased = true; - } - } else gPttDebounceCounter = 0; } - else if (!GPIO_CheckBit(&GPIOC->DATA, GPIOC_PIN_PTT) && !SerialConfigInProgress()) - { // PTT pressed - if (++gPttDebounceCounter >= 3) // 30ms - { // start transmitting - boot_counter_10ms = 0; - gPttDebounceCounter = 0; - gPttIsPressed = true; - ProcessKey(KEY_PTT, true, false); - } - } - else - gPttDebounceCounter = 0; -#endif // --------------------- OTHER KEYS ---------------------------- diff --git a/App/app/breakout.c b/App/app/breakout.c index 1e667772..cbbf4c3f 100644 --- a/App/app/breakout.c +++ b/App/app/breakout.c @@ -401,18 +401,6 @@ static void OnKeyDown(uint8_t key) } } - -// Key -static KEY_Code_t GetKey() -{ - KEY_Code_t btn = KEYBOARD_Poll(); - if (btn == KEY_INVALID && GPIO_IsPttPressed()) - { - btn = KEY_PTT; - } - return btn; -} - // HandleUserInput static bool HandleUserInput() { @@ -423,7 +411,7 @@ static bool HandleUserInput() #endif kbd.prev = kbd.current; - kbd.current = GetKey(); + kbd.current = KEYBOARD_GetKey(); if (kbd.current == KEY_INVALID) return true; diff --git a/App/app/chFrScanner.c b/App/app/chFrScanner.c index f6c6d479..b64e0dd1 100644 --- a/App/app/chFrScanner.c +++ b/App/app/chFrScanner.c @@ -37,6 +37,15 @@ uint8_t initialCROSS_BAND_RX_TX; static void NextFreqChannel(void); static void NextMemChannel(void); +#if defined(ENABLE_FEAT_F4HWN_RESUME_STATE) || defined(ENABLE_SCAN_RANGES) + void CHFRSCANNER_ScanRange(void) { + gScanRangeStart = gScanRangeStart ? 0 : gTxVfo->pRX->Frequency; + gScanRangeStop = gEeprom.VfoInfo[!gEeprom.TX_VFO].freq_config_RX.Frequency; + if(gScanRangeStart > gScanRangeStop) + SWAP(gScanRangeStart, gScanRangeStop); + } +#endif + void CHFRSCANNER_Start(const bool storeBackupSettings, const int8_t scan_direction) { if (storeBackupSettings) { diff --git a/App/app/chFrScanner.h b/App/app/chFrScanner.h index 1b425627..51b2c9a6 100644 --- a/App/app/chFrScanner.h +++ b/App/app/chFrScanner.h @@ -20,6 +20,10 @@ void CHFRSCANNER_Stop(void); void CHFRSCANNER_Start(const bool storeBackupSettings, const int8_t scan_direction); void CHFRSCANNER_ContinueScanning(void); +#if defined(ENABLE_FEAT_F4HWN_RESUME_STATE) || defined(ENABLE_SCAN_RANGES) + void CHFRSCANNER_ScanRange(void); +#endif + #ifdef ENABLE_FEAT_F4HWN extern uint32_t lastFoundFrqOrChan; extern uint32_t lastFoundFrqOrChanOld; diff --git a/App/app/flashlight.c b/App/app/flashlight.c index f72ea081..49c948c0 100644 --- a/App/app/flashlight.c +++ b/App/app/flashlight.c @@ -51,20 +51,26 @@ static inline void Flashlight_Toggle(){ GPIO_TogglePin(GPIO_PIN_FLASHLIGHT); } void ACTION_FlashLight(void) { - switch (gFlashLightState) { - case FLASHLIGHT_OFF: - gFlashLightState++; - Flashlight_TurnOn(); - break; - case FLASHLIGHT_ON: - case FLASHLIGHT_BLINK: - gFlashLightState++; - break; - case FLASHLIGHT_SOS: - default: - gFlashLightState = 0; - Flashlight_TurnOff(); + // switch (gFlashLightState) { + // case FLASHLIGHT_OFF: + // Flashlight_TurnOn(); + // break; + // case FLASHLIGHT_ON: + // case FLASHLIGHT_BLINK: + // break; + // case FLASHLIGHT_SOS: + // default: + // Flashlight_TurnOff(); + // } + + if(gFlashLightState == FLASHLIGHT_OFF) { + Flashlight_TurnOn(); } + else if (gFlashLightState == FLASHLIGHT_SOS) { + Flashlight_TurnOff(); + } + + gFlashLightState = (gFlashLightState + 1) % 4; } #else void ACTION_FlashLight(void) diff --git a/App/app/fm.c b/App/app/fm.c index d3f53e3a..5e79f95c 100644 --- a/App/app/fm.c +++ b/App/app/fm.c @@ -129,6 +129,18 @@ void FM_EraseChannels(void) memset(gFM_Channels, 0xFF, sizeof(gFM_Channels)); } +uint16_t FM_WrapFrequency(uint16_t Frequency) { + const uint16_t freqLoLimit = BK1080_GetFreqLoLimit(gEeprom.FM_Band); + const uint16_t freqHiLimit = BK1080_GetFreqHiLimit(gEeprom.FM_Band); + + if (Frequency < freqLoLimit) + return freqHiLimit; + else if (Frequency > freqHiLimit) + return freqLoLimit; + + return Frequency; +} + void FM_Tune(uint16_t Frequency, int8_t Step, bool bFlag) { AUDIO_AudioPathOff(); @@ -145,10 +157,7 @@ void FM_Tune(uint16_t Frequency, int8_t Step, bool bFlag) if (!bFlag) { Frequency += Step; - if (Frequency < BK1080_GetFreqLoLimit(gEeprom.FM_Band)) - Frequency = BK1080_GetFreqHiLimit(gEeprom.FM_Band); - else if (Frequency > BK1080_GetFreqHiLimit(gEeprom.FM_Band)) - Frequency = BK1080_GetFreqLoLimit(gEeprom.FM_Band); + Frequency = FM_WrapFrequency(Frequency); gEeprom.FM_FrequencyPlaying = Frequency; } @@ -513,10 +522,7 @@ static void Key_UP_DOWN(uint8_t state, int8_t Step) else { uint16_t Frequency = gEeprom.FM_SelectedFrequency + Step; - if (Frequency < BK1080_GetFreqLoLimit(gEeprom.FM_Band)) - Frequency = BK1080_GetFreqHiLimit(gEeprom.FM_Band); - else if (Frequency > BK1080_GetFreqHiLimit(gEeprom.FM_Band)) - Frequency = BK1080_GetFreqLoLimit(gEeprom.FM_Band); + Frequency = FM_WrapFrequency(Frequency); gEeprom.FM_FrequencyPlaying = Frequency; gEeprom.FM_SelectedFrequency = gEeprom.FM_FrequencyPlaying; diff --git a/App/app/main.c b/App/app/main.c index 8bef1e97..46f65e70 100644 --- a/App/app/main.c +++ b/App/app/main.c @@ -61,10 +61,7 @@ static void toggle_chan_scanlist(void) if(!IS_MR_CHANNEL(gTxVfo->CHANNEL_SAVE)) { #ifdef ENABLE_SCAN_RANGES - gScanRangeStart = gScanRangeStart ? 0 : gTxVfo->pRX->Frequency; - gScanRangeStop = gEeprom.VfoInfo[!gEeprom.TX_VFO].freq_config_RX.Frequency; - if(gScanRangeStart > gScanRangeStop) - SWAP(gScanRangeStart, gScanRangeStop); + CHFRSCANNER_ScanRange(); #endif return; } diff --git a/App/app/spectrum.c b/App/app/spectrum.c index e6b9d8ea..3ecb2821 100644 --- a/App/app/spectrum.c +++ b/App/app/spectrum.c @@ -270,16 +270,6 @@ static void GUI_DisplaySmallest(const char *pString, uint8_t x, uint8_t y, // Utility functions -static KEY_Code_t GetKey() -{ - KEY_Code_t btn = KEYBOARD_Poll(); - if (btn == KEY_INVALID && GPIO_IsPttPressed()) - { - btn = KEY_PTT; - } - return btn; -} - static int clamp(int v, int min, int max) { return v <= min ? min : (v >= max ? max : v); @@ -1517,7 +1507,7 @@ static void Render() static bool HandleUserInput() { kbd.prev = kbd.current; - kbd.current = GetKey(); + kbd.current = KEYBOARD_GetKey(); if (kbd.current != KEY_INVALID && kbd.current == kbd.prev) { diff --git a/App/driver/keyboard.c b/App/driver/keyboard.c index de3d4a13..9b3e7815 100644 --- a/App/driver/keyboard.c +++ b/App/driver/keyboard.c @@ -46,7 +46,7 @@ static uint8_t gSerialKeyLong = 0; // 0 = short press, 1 = long press // Inject a short or long press from serial (UART or VCP). // KEY_PTT is explicitly blocked — PTT release cannot be guaranteed over serial. -void KEYBOARD_InjectKey(uint8_t keyCode, bool keyLong) +static inline void KEYBOARD_InjectKey(uint8_t keyCode, bool keyLong) { if (keyCode < KEY_INVALID && keyCode != KEY_PTT) { gKeyFromSerial = (KEY_Code_t)keyCode; @@ -250,4 +250,14 @@ KEY_Code_t KEYBOARD_Poll(void) GPIO_SetOutputPin(PIN_COLS); return Key; +} + +KEY_Code_t KEYBOARD_GetKey(void) +{ + KEY_Code_t btn = KEYBOARD_Poll(); + if (btn == KEY_INVALID && GPIO_IsPttPressed()) + { + btn = KEY_PTT; + } + return btn; } \ No newline at end of file diff --git a/App/driver/keyboard.h b/App/driver/keyboard.h index b9dfb97c..a4e1b406 100644 --- a/App/driver/keyboard.h +++ b/App/driver/keyboard.h @@ -65,11 +65,10 @@ extern bool gWasFKeyPressed; // Serial-injected key (written by UART/VCP parser, consumed by KEYBOARD_Poll). extern volatile KEY_Code_t gKeyFromSerial; -// Inject a short or long press received from serial (UART or VCP). -void KEYBOARD_InjectKey(uint8_t keyCode, bool keyLong); bool KEYBOARD_ProcessProtocolByte(ParseState_t *state, uint8_t b); #endif KEY_Code_t KEYBOARD_Poll(void); +KEY_Code_t KEYBOARD_GetKey(void); #endif \ No newline at end of file diff --git a/App/main.c b/App/main.c index c3153b37..1eb13710 100644 --- a/App/main.c +++ b/App/main.c @@ -277,77 +277,38 @@ void Main(void) #endif } - /* #ifdef ENABLE_FEAT_F4HWN_RESUME_STATE - if(gEeprom.CURRENT_STATE == 2 || gEeprom.CURRENT_STATE == 5) - { - gScanRangeStart = gScanRangeStart ? 0 : gTxVfo->pRX->Frequency; - gScanRangeStop = gEeprom.VfoInfo[!gEeprom.TX_VFO].freq_config_RX.Frequency; - if(gScanRangeStart > gScanRangeStop) - { - SWAP(gScanRangeStart, gScanRangeStop); - } - } - switch (gEeprom.CURRENT_STATE) { - case 1: - gEeprom.SCAN_LIST_DEFAULT = gEeprom.CURRENT_LIST; - CHFRSCANNER_Start(true, SCAN_FWD); - break; + if (gEeprom.CURRENT_STATE == 2 || gEeprom.CURRENT_STATE == 5) + CHFRSCANNER_ScanRange(); - case 2: - CHFRSCANNER_Start(true, SCAN_FWD); - break; + switch (gEeprom.CURRENT_STATE) { + case 1: + gEeprom.SCAN_LIST_DEFAULT = gEeprom.CURRENT_LIST; + CHFRSCANNER_Start(true, SCAN_FWD); + break; - #ifdef ENABLE_FMRADIO - case 3: - ACTION_FM(); - GUI_SelectNextDisplay(gRequestDisplayScreen); - break; - #endif + case 2: + CHFRSCANNER_Start(true, SCAN_FWD); + break; - #ifdef ENABLE_SPECTRUM - case 4: - APP_RunSpectrum(); - break; - case 5: - APP_RunSpectrum(); - break; - #endif + #ifdef ENABLE_FMRADIO + case 3: + ACTION_FM(); + GUI_SelectNextDisplay(gRequestDisplayScreen); + break; + #endif - default: - // No action for CURRENT_STATE == 0 or other unexpected values - break; - } - #endif - */ + #ifdef ENABLE_SPECTRUM + case 4: + case 5: + APP_RunSpectrum(); + break; + #endif - #ifdef ENABLE_FEAT_F4HWN_RESUME_STATE - if (gEeprom.CURRENT_STATE == 2 || gEeprom.CURRENT_STATE == 5) { - gScanRangeStart = gScanRangeStart ? 0 : gTxVfo->pRX->Frequency; - gScanRangeStop = gEeprom.VfoInfo[!gEeprom.TX_VFO].freq_config_RX.Frequency; - if (gScanRangeStart > gScanRangeStop) { - SWAP(gScanRangeStart, gScanRangeStop); - } + default: + // No action for CURRENT_STATE == 0 or other unexpected values + break; } - - if (gEeprom.CURRENT_STATE == 1) { - gEeprom.SCAN_LIST_DEFAULT = gEeprom.CURRENT_LIST; - } - - if (gEeprom.CURRENT_STATE == 1 || gEeprom.CURRENT_STATE == 2) { - CHFRSCANNER_Start(true, SCAN_FWD); - } - #ifdef ENABLE_FMRADIO - else if (gEeprom.CURRENT_STATE == 3) { - ACTION_FM(); - GUI_SelectNextDisplay(gRequestDisplayScreen); - } - #endif - #ifdef ENABLE_SPECTRUM - else if (gEeprom.CURRENT_STATE == 4 || gEeprom.CURRENT_STATE == 5) { - APP_RunSpectrum(); - } - #endif #endif while (true) { diff --git a/App/screenshot.c b/App/screenshot.c index e73355bb..8ca66c2c 100644 --- a/App/screenshot.c +++ b/App/screenshot.c @@ -52,6 +52,18 @@ static void SCREENSHOT_Send(const uint8_t *buf, uint16_t len) } } +void SCREENSHOT_Line(uint8_t *src, uint8_t *dest, uint16_t *idx, bool inv) { + for (uint8_t b = 0; b < 8; b++) { + for (uint8_t i = 0; i < 128; i += 8) { + uint8_t acc = 0; + for (uint8_t k = 0; k < 8; k++) { + if (src[i + k] & (1 << b)) acc |= (1 << k); + } + dest[(*idx)++] = inv ? ~acc : acc; + } + } +} + void SCREENSHOT_Update(bool force) { // Build frame in a temporary stack buffer @@ -83,31 +95,11 @@ void SCREENSHOT_Update(bool force) // ==== BUILD FRAME ONCE ==== // Status line: 8 bit layers × 128 columns - for (uint8_t b = 0; b < 8; b++) { - for (uint8_t i = 0; i < 128; i++) { - uint8_t bit = (gStatusLine[i] >> b) & 0x01; - acc |= (bit << bitCount++); - if (bitCount == 8) { - frameBuffer[index++] = gSetting_set_inv ? ~acc : acc; - acc = 0; - bitCount = 0; - } - } - } + SCREENSHOT_Line(gStatusLine, frameBuffer, &index, gSetting_set_inv); // Frame buffer: 7 lines × 8 bit layers × 128 columns for (uint8_t l = 0; l < 7; l++) { - for (uint8_t b = 0; b < 8; b++) { - for (uint8_t i = 0; i < 128; i++) { - uint8_t bit = (gFrameBuffer[l][i] >> b) & 0x01; - acc |= (bit << bitCount++); - if (bitCount == 8) { - frameBuffer[index++] = gSetting_set_inv ? ~acc : acc; - acc = 0; - bitCount = 0; - } - } - } + SCREENSHOT_Line(gFrameBuffer[l], frameBuffer, &index, gSetting_set_inv); } if (bitCount > 0) diff --git a/App/settings.c b/App/settings.c index 5523c347..045ab651 100644 --- a/App/settings.c +++ b/App/settings.c @@ -617,16 +617,20 @@ void SETTINGS_FetchChannelName(char *s, const uint16_t channel) void SETTINGS_FactoryReset(bool bIsAll) { - PY25Q16_SectorErase(0x000000); - PY25Q16_SectorErase(0x001000); - PY25Q16_SectorErase(0x002000); - PY25Q16_SectorErase(0x003000); - PY25Q16_SectorErase(0x004000); - PY25Q16_SectorErase(0x005000); - PY25Q16_SectorErase(0x006000); - PY25Q16_SectorErase(0x007000); - PY25Q16_SectorErase(0x008000); - PY25Q16_SectorErase(0x009000); + // PY25Q16_SectorErase(0x000000); + // PY25Q16_SectorErase(0x001000); + // PY25Q16_SectorErase(0x002000); + // PY25Q16_SectorErase(0x003000); + // PY25Q16_SectorErase(0x004000); + // PY25Q16_SectorErase(0x005000); + // PY25Q16_SectorErase(0x006000); + // PY25Q16_SectorErase(0x007000); + // PY25Q16_SectorErase(0x008000); + // PY25Q16_SectorErase(0x009000); + + for (uint32_t addr = 0x000000; addr <= 0x009000; addr += 0x1000) { + PY25Q16_SectorErase(addr); + } // 0d60 - 0e30 if (bIsAll) diff --git a/App/ui/main.c b/App/ui/main.c index ef86b60c..86d2a2c4 100644 --- a/App/ui/main.c +++ b/App/ui/main.c @@ -334,16 +334,23 @@ void DisplayRSSIBar(const bool now) uint8_t overS9dBm = 0; uint8_t overS9Bars = 0; - if (rssi_dBm >= -93) s_level = 9; // S9 = -93 dBm - else if (rssi_dBm >= -99) s_level = 8; // S8 = -99 dBm - else if (rssi_dBm >= -105) s_level = 7; // S7 = -105 dBm - else if (rssi_dBm >= -111) s_level = 6; // S6 = -111 dBm - else if (rssi_dBm >= -117) s_level = 5; // S5 = -117 dBm - else if (rssi_dBm >= -123) s_level = 4; // S4 = -123 dBm - else if (rssi_dBm >= -129) s_level = 3; // S3 = -129 dBm - else if (rssi_dBm >= -135) s_level = 2; // S2 = -135 dBm - else if (rssi_dBm >= -141) s_level = 1; // S1 = -141 dBm - else s_level = 0; // S0 (below -141 dBm) + // if (rssi_dBm >= -93) s_level = 9; // S9 = -93 dBm + // else if (rssi_dBm >= -99) s_level = 8; // S8 = -99 dBm + // else if (rssi_dBm >= -105) s_level = 7; // S7 = -105 dBm + // else if (rssi_dBm >= -111) s_level = 6; // S6 = -111 dBm + // else if (rssi_dBm >= -117) s_level = 5; // S5 = -117 dBm + // else if (rssi_dBm >= -123) s_level = 4; // S4 = -123 dBm + // else if (rssi_dBm >= -129) s_level = 3; // S3 = -129 dBm + // else if (rssi_dBm >= -135) s_level = 2; // S2 = -135 dBm + // else if (rssi_dBm >= -141) s_level = 1; // S1 = -141 dBm + // else s_level = 0; // S0 (below -141 dBm) + + s_level = (rssi_dBm + 147) / 6; + + if (s_level > 9) + s_level = 9; + else if (s_level < 0) + s_level = 0; if (s_level == 9) { // Compute over-S9 dB directly @@ -1624,4 +1631,4 @@ void UI_DisplayMain(void) #endif ST7565_BlitFullScreen(); -} \ No newline at end of file +} From 138e078984da01f204e97a44f9a20f0a8fa5326a Mon Sep 17 00:00:00 2001 From: mrkusypl Date: Fri, 20 Mar 2026 10:47:50 +0100 Subject: [PATCH 26/50] Fix for S-meter calculation, AirCopy - removed redundant code --- App/app/aircopy.c | 16 ---------------- App/ui/main.c | 8 ++++---- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/App/app/aircopy.c b/App/app/aircopy.c index 60991e5c..62d63e19 100644 --- a/App/app/aircopy.c +++ b/App/app/aircopy.c @@ -276,22 +276,6 @@ void AIRCOPY_StorePacket(void) } uint16_t Offset = g_FSK_Buffer[1]; - const AIRCOPY_TransferMap_t *map = AIRCOPY_GetCurrentMap(); - - // Validate offset is in the map - bool validOffset = false; - for (uint16_t i = 0; i < map->num_segments; i++) { - if (Offset >= map->segments[i].start_offset && Offset < map->segments[i].end_offset) { - validOffset = true; - break; - } - } - - if (!validOffset) { - gErrorsDuringAirCopy++; - AIRCOPY_CheckComplete(); - return; - } const AIRCOPY_Segment_t *seg = AIRCOPY_FindSegmentForOffset(Offset); diff --git a/App/ui/main.c b/App/ui/main.c index 86d2a2c4..58e43330 100644 --- a/App/ui/main.c +++ b/App/ui/main.c @@ -345,12 +345,12 @@ void DisplayRSSIBar(const bool now) // else if (rssi_dBm >= -141) s_level = 1; // S1 = -141 dBm // else s_level = 0; // S0 (below -141 dBm) - s_level = (rssi_dBm + 147) / 6; - - if (s_level > 9) + if (rssi_dBm >= -93) s_level = 9; - else if (s_level < 0) + else if (rssi_dBm < -141) s_level = 0; + else + s_level = (rssi_dBm + 147) / 6; if (s_level == 9) { // Compute over-S9 dB directly From 7c343a2b3e51210832604e069a1e68b23a5d189b Mon Sep 17 00:00:00 2001 From: mrkusypl Date: Fri, 20 Mar 2026 18:44:57 +0100 Subject: [PATCH 27/50] Enable beeps for Aircopy, minor code refactoring --- App/app/aircopy.c | 63 ++++++++++++++++++++--------------------------- App/app/main.c | 6 +++-- App/app/menu.c | 2 +- App/audio.c | 7 +----- App/screenshot.c | 8 +++--- 5 files changed, 37 insertions(+), 49 deletions(-) diff --git a/App/app/aircopy.c b/App/app/aircopy.c index 62d63e19..a2ec1322 100644 --- a/App/app/aircopy.c +++ b/App/app/aircopy.c @@ -312,16 +312,27 @@ void AIRCOPY_StorePacket(void) AIRCOPY_CheckComplete(); } +static void AIRCOPY_InitTransfer(bool isSendMode) +{ + gAircopyStep = 1; + gFSKWriteIndex = 0; + gAirCopyBlockNumber = 0; + gInputBoxIndex = 0; + gAirCopyIsSendMode = isSendMode; + + AIRCOPY_clear(); + + gAircopyState = AIRCOPY_TRANSFER; +} + // ============================================================================ // Key Processing // ============================================================================ -static void AIRCOPY_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) +static void AIRCOPY_Key_DIGITS(KEY_Code_t Key) { INPUTBOX_Append(Key); - gRequestDisplayScreen = DISPLAY_AIRCOPY; - if (gInputBoxIndex < 6) { #ifdef ENABLE_VOICE gAnotherVoiceID = (VOICE_ID_t)Key; @@ -356,51 +367,31 @@ static void AIRCOPY_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) BK4819_ResetFSK(); return; } - - gRequestDisplayScreen = DISPLAY_AIRCOPY; } -static void AIRCOPY_Key_EXIT(bool bKeyPressed, bool bKeyHeld) +static void AIRCOPY_Key_EXIT() { if (gInputBoxIndex == 0) { - gAircopyStep = 1; - gFSKWriteIndex = 0; - gAirCopyBlockNumber = 0; - gInputBoxIndex = 0; + AIRCOPY_InitTransfer(0); // Mode: Receive gErrorsDuringAirCopy = lErrorsDuringAirCopy = 0; - gAirCopyIsSendMode = 0; - - AIRCOPY_clear(); BK4819_PrepareFSKReceive(); - - gAircopyState = AIRCOPY_TRANSFER; + } else { gInputBox[--gInputBoxIndex] = 10; } - - gRequestDisplayScreen = DISPLAY_AIRCOPY; } -static void AIRCOPY_Key_MENU(bool bKeyPressed, bool bKeyHeld) +static void AIRCOPY_Key_MENU() { - gAircopyStep = 1; - gFSKWriteIndex = 0; - gAirCopyBlockNumber = 0; - gInputBoxIndex = 0; - gAirCopyIsSendMode = 1; + AIRCOPY_InitTransfer(1); // Mode: Send + g_FSK_Buffer[0] = 0xABCD; g_FSK_Buffer[1] = 0; g_FSK_Buffer[35] = 0xDCBA; - - AIRCOPY_clear(); - - GUI_DisplayScreen(); - - gAircopyState = AIRCOPY_TRANSFER; } -static void AIRCOPY_Key_UP_DOWN(bool bKeyPressed, bool bKeyHeld, int8_t Direction) +static void AIRCOPY_Key_UP_DOWN(int8_t Direction) { if (!gEeprom.SET_NAV) { Direction = -Direction; @@ -415,8 +406,6 @@ static void AIRCOPY_Key_UP_DOWN(bool bKeyPressed, bool bKeyHeld, int8_t Directio gAircopyCurrentMapIndex = (gAircopyCurrentMapIndex + AIRCOPY_NUM_MAPS - 1) % AIRCOPY_NUM_MAPS; break; } - - gRequestDisplayScreen = DISPLAY_AIRCOPY; } void AIRCOPY_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) @@ -431,17 +420,17 @@ void AIRCOPY_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) switch (Key) { case KEY_0...KEY_9: - AIRCOPY_Key_DIGITS(Key, bKeyPressed, bKeyHeld); + AIRCOPY_Key_DIGITS(Key); break; case KEY_MENU: - AIRCOPY_Key_MENU(bKeyPressed, bKeyHeld); + AIRCOPY_Key_MENU(); break; case KEY_EXIT: - AIRCOPY_Key_EXIT(bKeyPressed, bKeyHeld); + AIRCOPY_Key_EXIT(); break; case KEY_UP: case KEY_DOWN: - AIRCOPY_Key_UP_DOWN(bKeyPressed, bKeyHeld, Key == KEY_UP ? 1 : -1); + AIRCOPY_Key_UP_DOWN(Key == KEY_UP ? 1 : -1); break; case KEY_PTT: break; @@ -449,6 +438,8 @@ void AIRCOPY_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL; break; } + + gRequestDisplayScreen = DISPLAY_AIRCOPY; } #endif \ No newline at end of file diff --git a/App/app/main.c b/App/app/main.c index 46f65e70..7ff59a43 100644 --- a/App/app/main.c +++ b/App/app/main.c @@ -294,8 +294,9 @@ static void processFKeyFunction(const KEY_Code_t Key, const bool beep) } gWasFKeyPressed = false; + + break; } - break; case KEY_SIDE1: case KEY_SIDE2: { @@ -313,8 +314,9 @@ static void processFKeyFunction(const KEY_Code_t Key, const bool beep) } gWasFKeyPressed = false; + + break; } - break; #endif default: diff --git a/App/app/menu.c b/App/app/menu.c index 6d58b49a..fdaaf997 100644 --- a/App/app/menu.c +++ b/App/app/menu.c @@ -1785,7 +1785,7 @@ static void MENU_Key_MENU(const bool bKeyPressed, const bool bKeyHeld) UI_DisplayMenu(); - if (UI_MENU_GetCurrentMenuId() == MENU_RESET) + if (m == MENU_RESET) { #ifdef ENABLE_VOICE AUDIO_SetVoiceID(0, VOICE_ID_CONFIRM); diff --git a/App/audio.c b/App/audio.c index bb2c6073..022515b8 100644 --- a/App/audio.c +++ b/App/audio.c @@ -53,12 +53,7 @@ void AUDIO_PlayBeep(BEEP_Type_t Beep) #endif !gEeprom.BEEP_CONTROL) return; - -#ifdef ENABLE_AIRCOPY - if (gScreenToDisplay == DISPLAY_AIRCOPY) - return; -#endif - + if (gCurrentFunction == FUNCTION_RECEIVE) return; diff --git a/App/screenshot.c b/App/screenshot.c index 8ca66c2c..b56e44a9 100644 --- a/App/screenshot.c +++ b/App/screenshot.c @@ -52,14 +52,14 @@ static void SCREENSHOT_Send(const uint8_t *buf, uint16_t len) } } -void SCREENSHOT_Line(uint8_t *src, uint8_t *dest, uint16_t *idx, bool inv) { +void SCREENSHOT_Line(uint8_t *src, uint8_t *dest, uint16_t *idx) { for (uint8_t b = 0; b < 8; b++) { for (uint8_t i = 0; i < 128; i += 8) { uint8_t acc = 0; for (uint8_t k = 0; k < 8; k++) { if (src[i + k] & (1 << b)) acc |= (1 << k); } - dest[(*idx)++] = inv ? ~acc : acc; + dest[(*idx)++] = gSetting_set_inv ? ~acc : acc; } } } @@ -95,11 +95,11 @@ void SCREENSHOT_Update(bool force) // ==== BUILD FRAME ONCE ==== // Status line: 8 bit layers × 128 columns - SCREENSHOT_Line(gStatusLine, frameBuffer, &index, gSetting_set_inv); + SCREENSHOT_Line(gStatusLine, frameBuffer, &index); // Frame buffer: 7 lines × 8 bit layers × 128 columns for (uint8_t l = 0; l < 7; l++) { - SCREENSHOT_Line(gFrameBuffer[l], frameBuffer, &index, gSetting_set_inv); + SCREENSHOT_Line(gFrameBuffer[l], frameBuffer, &index); } if (bitCount > 0) From 3d4ffd7900eab5bc18e53fc7db93266267ffa8e8 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Sat, 21 Mar 2026 00:16:14 +0100 Subject: [PATCH 28/50] Fix VFO frequency input final digit handling --- App/app/main.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/App/app/main.c b/App/app/main.c index 7ff59a43..2222668b 100644 --- a/App/app/main.c +++ b/App/app/main.c @@ -540,7 +540,7 @@ static void MAIN_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) gKeyInputCountdown = (gInputBoxIndex >= totalDigits) ? (key_input_timeout_500ms / 16) : (key_input_timeout_500ms / 3); - if (gInputBoxIndex >= totalDigits) { + if (gInputBoxIndex > totalDigits) { gInputBoxIndex = totalDigits; return; From 0b3a390cb98e260a2ab29d25262ab62e7bb7e34a Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Sat, 21 Mar 2026 00:49:40 +0100 Subject: [PATCH 29/50] Update donors --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f2d86274..4a606a49 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Anyway, have fun. # Donations -Special thanks to Jean-Cyrille F6IWW (3 times), Fabrice 14RC123, David F4BPP, Olivier 14RC206, Frédéric F4ESO, Stéphane F5LGW (2 times), Jorge Ornelas (4 times), Laurent F4AXK, Christophe Morel, Clayton W0LED, Pierre Antoine F6FWB, Jean-Claude 14FRS3306, Thierry F4GVO, Eric F1NOU, PricelessToolkit, Ady M6NYJ, Tom McGovern (4 times), Joseph Roth, Pierre-Yves Colin, Frank DJ7FG, Marcel Testaz, Brian Frobisher, Yannick F4JFO, Paolo Bussola, Dirk DL8DF, Levente Szőke (2 times), Bernard-Michel Herrera, Jérôme Saintespes, Paul Davies, RS (3 times), Johan F4WAT, Robert Wörle, Rafael Sundorf, Paul Harker, Peter Fintl, Pascal F4ICR (2 times), Mike DL2MF (3 times), Eric KI1C (2 times), Phil G0ELM, Jérôme Lambert, Eliot Vedel, Alfonso EA7KDF, Jean-François F1EVM, Robert DC1RDB (2 times), Ian KE2CHJ, Daryl VK3AWA, Roberto Brunelli, Robert Boardman, Stephen Oliver, Nicolas F4INE, William Bruno, Daniel OK2VLK, Tayler Chew, Peter DL7RFP, Philippe Kopp, Rune LA6YMA, Jeremy Luna, Steef Wagenaar (2 times), Zhuo BG7SGA, Jamie M0JLB, Antoine LIBERT, Vince K0DKR, Julia DF7JA, Ken 2E0UMK, Victor TI2SYS and Tobi DG9LAY for their [donations](https://www.paypal.com/paypalme/F4HWN). That’s so kind of them. Thanks so much 🙏🏻 +Special thanks to Jean-Cyrille F6IWW (3 times), Fabrice 14RC123, David F4BPP, Olivier 14RC206, Frédéric F4ESO, Stéphane F5LGW (2 times), Jorge Ornelas (4 times), Laurent F4AXK, Christophe Morel, Clayton W0LED, Pierre Antoine F6FWB, Jean-Claude 14FRS3306, Thierry F4GVO, Eric F1NOU, PricelessToolkit, Ady M6NYJ, Tom McGovern (4 times), Joseph Roth, Pierre-Yves Colin, Frank DJ7FG, Marcel Testaz, Brian Frobisher, Yannick F4JFO, Paolo Bussola, Dirk DL8DF, Levente Szőke (2 times), Bernard-Michel Herrera, Jérôme Saintespes, Paul Davies, RS (3 times), Johan F4WAT, Robert Wörle, Rafael Sundorf, Paul Harker, Peter Fintl, Pascal F4ICR (2 times), Mike DL2MF (3 times), Eric KI1C (2 times), Phil G0ELM, Jérôme Lambert, Eliot Vedel, Alfonso EA7KDF, Jean-François F1EVM, Robert DC1RDB (2 times), Ian KE2CHJ, Daryl VK3AWA, Roberto Brunelli, Robert Boardman, Stephen Oliver, Nicolas F4INE, William Bruno, Daniel OK2VLK, Tayler Chew, Peter DL7RFP, Philippe Kopp, Rune LA6YMA, Jeremy Luna, Steef Wagenaar (2 times), Zhuo BG7SGA, Jamie M0JLB, Antoine LIBERT, Vince K0DKR, Julia DF7JA, Ken 2E0UMK, Victor TI2SYS, Tobi DG9LAY and Deaglan K4DFQ for their [donations](https://www.paypal.com/paypalme/F4HWN). That’s so kind of them. Thanks so much 🙏🏻 ## Table of Contents From 2939620066e9e6d5453d0c7437ade6a0499ee797 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Sat, 21 Mar 2026 15:45:18 +0100 Subject: [PATCH 30/50] Update donors --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4a606a49..0cc7f879 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Anyway, have fun. # Donations -Special thanks to Jean-Cyrille F6IWW (3 times), Fabrice 14RC123, David F4BPP, Olivier 14RC206, Frédéric F4ESO, Stéphane F5LGW (2 times), Jorge Ornelas (4 times), Laurent F4AXK, Christophe Morel, Clayton W0LED, Pierre Antoine F6FWB, Jean-Claude 14FRS3306, Thierry F4GVO, Eric F1NOU, PricelessToolkit, Ady M6NYJ, Tom McGovern (4 times), Joseph Roth, Pierre-Yves Colin, Frank DJ7FG, Marcel Testaz, Brian Frobisher, Yannick F4JFO, Paolo Bussola, Dirk DL8DF, Levente Szőke (2 times), Bernard-Michel Herrera, Jérôme Saintespes, Paul Davies, RS (3 times), Johan F4WAT, Robert Wörle, Rafael Sundorf, Paul Harker, Peter Fintl, Pascal F4ICR (2 times), Mike DL2MF (3 times), Eric KI1C (2 times), Phil G0ELM, Jérôme Lambert, Eliot Vedel, Alfonso EA7KDF, Jean-François F1EVM, Robert DC1RDB (2 times), Ian KE2CHJ, Daryl VK3AWA, Roberto Brunelli, Robert Boardman, Stephen Oliver, Nicolas F4INE, William Bruno, Daniel OK2VLK, Tayler Chew, Peter DL7RFP, Philippe Kopp, Rune LA6YMA, Jeremy Luna, Steef Wagenaar (2 times), Zhuo BG7SGA, Jamie M0JLB, Antoine LIBERT, Vince K0DKR, Julia DF7JA, Ken 2E0UMK, Victor TI2SYS, Tobi DG9LAY and Deaglan K4DFQ for their [donations](https://www.paypal.com/paypalme/F4HWN). That’s so kind of them. Thanks so much 🙏🏻 +Special thanks to Jean-Cyrille F6IWW (3 times), Fabrice 14RC123, David F4BPP, Olivier 14RC206, Frédéric F4ESO, Stéphane F5LGW (2 times), Jorge Ornelas (4 times), Laurent F4AXK, Christophe Morel, Clayton W0LED, Pierre Antoine F6FWB, Jean-Claude 14FRS3306, Thierry F4GVO, Eric F1NOU, PricelessToolkit, Ady M6NYJ, Tom McGovern (4 times), Joseph Roth, Pierre-Yves Colin, Frank DJ7FG, Marcel Testaz, Brian Frobisher, Yannick F4JFO, Paolo Bussola, Dirk DL8DF, Levente Szőke (2 times), Bernard-Michel Herrera, Jérôme Saintespes, Paul Davies, RS (3 times), Johan F4WAT, Robert Wörle, Rafael Sundorf, Paul Harker, Peter Fintl, Pascal F4ICR (2 times), Mike DL2MF (3 times), Eric KI1C (2 times), Phil G0ELM, Jérôme Lambert, Eliot Vedel, Alfonso EA7KDF, Jean-François F1EVM, Robert DC1RDB (2 times), Ian KE2CHJ, Daryl VK3AWA, Roberto Brunelli, Robert Boardman, Stephen Oliver, Nicolas F4INE, William Bruno, Daniel OK2VLK, Tayler Chew, Peter DL7RFP, Philippe Kopp, Rune LA6YMA, Jeremy Luna, Steef Wagenaar (2 times), Zhuo BG7SGA, Jamie M0JLB, Antoine LIBERT, Vince K0DKR, Julia DF7JA, Ken 2E0UMK, Victor TI2SYS, Tobi DG9LAY, Deaglan K4DFQ and Catherine PALMER for their [donations](https://www.paypal.com/paypalme/F4HWN). That’s so kind of them. Thanks so much 🙏🏻 ## Table of Contents From 85dae05ad390b90c66b0c994b8ded0a8e79db968 Mon Sep 17 00:00:00 2001 From: mrkusypl Date: Sat, 21 Mar 2026 19:13:51 +0100 Subject: [PATCH 31/50] Minor code refactoring (24 B) --- App/ui/fmradio.c | 4 +--- App/ui/helper.c | 18 ++++++++++++++++++ App/ui/helper.h | 1 + App/ui/main.c | 45 +++++++-------------------------------------- 4 files changed, 27 insertions(+), 41 deletions(-) diff --git a/App/ui/fmradio.c b/App/ui/fmradio.c index 02dd5920..0e46f41e 100644 --- a/App/ui/fmradio.c +++ b/App/ui/fmradio.c @@ -36,9 +36,7 @@ void UI_DisplayFM(void) UI_DisplayClear(); #ifdef ENABLE_FEAT_F4HWN - if (gEeprom.KEY_LOCK && gKeypadLocked > 0) { - UI_PrintStringSmallBold("UNLOCK KEYBOARD", 12, 0, 5); - } + UI_DisplayUnlockKeyboard(5); #endif UI_PrintString("FM", 2, 0, 0, 8); diff --git a/App/ui/helper.c b/App/ui/helper.c index 764de28f..a0edd501 100644 --- a/App/ui/helper.c +++ b/App/ui/helper.c @@ -22,6 +22,7 @@ #include "ui/helper.h" #include "ui/inputbox.h" #include "misc.h" +#include "settings.h" void UI_GenerateChannelString(char *pString, const uint16_t Channel) @@ -322,6 +323,23 @@ static void sort(int16_t *a, int16_t *b) } } + void UI_DisplayUnlockKeyboard(uint8_t shift) { + if (gEeprom.KEY_LOCK && gKeypadLocked > 0) + { // tell user how to unlock the keyboard + + //memcpy(gFrameBuffer[shift] + 2, gFontKeyLock, sizeof(gFontKeyLock)); + UI_PrintStringSmallBold("UNLOCK KEYBOARD", 12, 0, shift); + //memcpy(gFrameBuffer[shift] + 120, gFontKeyLock, sizeof(gFontKeyLock)); + + /* + for (uint8_t i = 12; i < 116; i++) + { + gFrameBuffer[shift][i] ^= 0xFF; + } + */ + } + } + bool IsEmptyName(const char *name, uint8_t len) { if (name[0] == '\0' || name[0] == '\xff') return true; diff --git a/App/ui/helper.h b/App/ui/helper.h index 267a158e..0002f2cb 100644 --- a/App/ui/helper.h +++ b/App/ui/helper.h @@ -38,6 +38,7 @@ void UI_DrawPixelBuffer(uint8_t (*buffer)[128], uint8_t x, uint8_t y, bool black void PutPixel(uint8_t x, uint8_t y, bool fill); void PutPixelStatus(uint8_t x, uint8_t y, bool fill); void GUI_DisplaySmallest(const char *pString, uint8_t x, uint8_t y, bool statusbar, bool fill); + void UI_DisplayUnlockKeyboard(uint8_t shift); bool IsEmptyName(const char *name, uint8_t len); #endif void UI_DrawLineBuffer(uint8_t (*buffer)[128], int16_t x1, int16_t y1, int16_t x2, int16_t y2, bool black); diff --git a/App/ui/main.c b/App/ui/main.c index 58e43330..153303b2 100644 --- a/App/ui/main.c +++ b/App/ui/main.c @@ -77,19 +77,13 @@ static void DrawSmallPowerBars(uint8_t *p, unsigned int level) if(level>6) level = 6; - if(gSetting_set_gui) - { - for(uint8_t i = 0; i <= level; i++) { - char bar = (0xff << (6-i)) & 0x7F; - memset(p + 2 + i*3, bar, 2); - } - } - else - { - for(uint8_t i = 0; i <= level; i++) { - char bar = 0b00111110; - memset(p + 2 + i*3, bar, 2); + char bar = 0b00111110; + + for(uint8_t i = 0; i <= level; i++) { + if(gSetting_set_gui) { + bar = (0xff << (6-i)) & 0x7F; } + memset(p + 2 + i*3, bar, 2); } } #if defined ENABLE_AUDIO_BAR || defined ENABLE_RSSI_BAR @@ -563,32 +557,7 @@ void UI_DisplayMain(void) return; } #else - if (gEeprom.KEY_LOCK && gKeypadLocked > 0) - { // tell user how to unlock the keyboard - uint8_t shift = 3; - - /* - BK4819_ToggleGpioOut(BK4819_GPIO5_PIN1_RED, true); - SYSTEM_DelayMs(50); - BK4819_ToggleGpioOut(BK4819_GPIO5_PIN1_RED, false); - SYSTEM_DelayMs(50); - */ - - if(isMainOnly()) - { - shift = 5; - } - //memcpy(gFrameBuffer[shift] + 2, gFontKeyLock, sizeof(gFontKeyLock)); - UI_PrintStringSmallBold("UNLOCK KEYBOARD", 12, 0, shift); - //memcpy(gFrameBuffer[shift] + 120, gFontKeyLock, sizeof(gFontKeyLock)); - - /* - for (uint8_t i = 12; i < 116; i++) - { - gFrameBuffer[shift][i] ^= 0xFF; - } - */ - } + UI_DisplayUnlockKeyboard(isMainOnly() ? 5 : 3); #endif unsigned int activeTxVFO = gRxVfoIsActive ? gEeprom.RX_VFO : gEeprom.TX_VFO; From ff336cd5d9cfde68f6f24e9c3c3694c41e694430 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Sun, 22 Mar 2026 03:00:40 +0100 Subject: [PATCH 32/50] Retry this... --- App/radio.c | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/App/radio.c b/App/radio.c index bdb94c77..14b50b1b 100644 --- a/App/radio.c +++ b/App/radio.c @@ -926,8 +926,8 @@ void RADIO_SetupRegisters(bool switchToForeground) BK4819_EnableDTMF(); InterruptMask |= BK4819_REG_3F_DTMF_5TONE_FOUND; - //RADIO_SetupAGC(gRxVfo->Modulation == MODULATION_AM, false); - RADIO_SetupAGC(false, false); + RADIO_SetupAGC(gRxVfo->Modulation == MODULATION_AM, false); + //RADIO_SetupAGC(false, false); // enable/disable BK4819 selected interrupts BK4819_WriteRegister(BK4819_REG_3F, InterruptMask); @@ -1143,36 +1143,28 @@ void RADIO_SetModulation(ModulationMode_t modulation) BK4819_WriteRegister(BK4819_REG_3D, modulation == MODULATION_USB ? 0 : 0x2AAB); BK4819_SetRegValue(afcDisableRegSpec, modulation != MODULATION_FM); - //RADIO_SetupAGC(modulation == MODULATION_AM, false); - RADIO_SetupAGC(false, false); + RADIO_SetupAGC(modulation == MODULATION_AM, false); + //RADIO_SetupAGC(false, false); } void RADIO_SetupAGC(bool listeningAM, bool disable) { static uint8_t lastSettings; uint8_t newSettings = (listeningAM << 1) | disable; - if(lastSettings == newSettings) + if (lastSettings == newSettings) return; lastSettings = newSettings; - - if(!listeningAM) { // if not actively listening AM we don't need any AM specific regulation - BK4819_SetAGC(!disable); - BK4819_InitAGC(false); - } - else { #ifdef ENABLE_AM_FIX - if(gSetting_AM_fix) { // if AM fix active lock AGC so AM-fix can do it's job - BK4819_SetAGC(0); - AM_fix_enable(!disable); - } - else -#endif - { - BK4819_SetAGC(!disable); - BK4819_InitAGC(true); - } + if (listeningAM && gSetting_AM_fix) { + BK4819_SetAGC(0); + AM_fix_enable(!disable); + return; } +#endif + + BK4819_SetAGC(!disable); + BK4819_InitAGC(listeningAM); } void RADIO_SetVfoState(VfoState_t State) From f4a32b4350589de7fac846c92afe71cc520b4eba Mon Sep 17 00:00:00 2001 From: mrkusypl Date: Sun, 22 Mar 2026 16:19:08 +0100 Subject: [PATCH 33/50] Displaying power bars during TX when using SetPwr --- App/ui/main.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/App/ui/main.c b/App/ui/main.c index 153303b2..e49a8a6a 100644 --- a/App/ui/main.c +++ b/App/ui/main.c @@ -1157,9 +1157,15 @@ void UI_DisplayMain(void) Level = 2; } */ - Level = gRxVfo->OUTPUT_POWER - 1; + + uint8_t currentPower = gRxVfo->OUTPUT_POWER; + + if(currentPower == OUTPUT_POWER_USER) + Level = gSetting_set_pwr; + else + Level = currentPower - 1; } - else + else if (mode == VFO_MODE_RX) { // RX signal level #ifndef ENABLE_RSSI_BAR From aa69d698a77d78f95a7f382bf1fe8e28076d8540 Mon Sep 17 00:00:00 2001 From: mrkusypl Date: Sun, 22 Mar 2026 20:58:27 +0100 Subject: [PATCH 34/50] Code optimization (32 B); Restore full VFO state on long press of EXIT --- App/app/action.c | 27 ++++++++--------- App/app/app.c | 4 +-- App/app/common.c | 11 +++++++ App/app/main.c | 76 ++++++++++++++++++------------------------------ App/misc.c | 1 + App/misc.h | 1 + 6 files changed, 58 insertions(+), 62 deletions(-) diff --git a/App/app/action.c b/App/app/action.c index 753f2e1c..f62d4bb0 100644 --- a/App/app/action.c +++ b/App/app/action.c @@ -319,19 +319,23 @@ void ACTION_Handle(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) return; } - enum ACTION_OPT_t funcShort = ACTION_OPT_NONE; - enum ACTION_OPT_t funcLong = ACTION_OPT_NONE; + enum ACTION_OPT_t func = ACTION_OPT_NONE; switch(Key) { case KEY_SIDE1: - funcShort = gEeprom.KEY_1_SHORT_PRESS_ACTION; - funcLong = gEeprom.KEY_1_LONG_PRESS_ACTION; + if (bKeyHeld) + func = gEeprom.KEY_1_LONG_PRESS_ACTION; + else + func = gEeprom.KEY_1_SHORT_PRESS_ACTION; break; case KEY_SIDE2: - funcShort = gEeprom.KEY_2_SHORT_PRESS_ACTION; - funcLong = gEeprom.KEY_2_LONG_PRESS_ACTION; + if (bKeyHeld) + func = gEeprom.KEY_2_LONG_PRESS_ACTION; + else + func = gEeprom.KEY_2_SHORT_PRESS_ACTION; break; case KEY_MENU: - funcLong = gEeprom.KEY_M_LONG_PRESS_ACTION; + if (bKeyHeld) + func = gEeprom.KEY_M_LONG_PRESS_ACTION; break; default: break; @@ -347,17 +351,14 @@ void ACTION_Handle(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) if(!(bKeyHeld && !bKeyPressed)) // don't beep on released after hold gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL; - if (bKeyHeld || bKeyPressed) // held + if (bKeyHeld && !bKeyPressed) // button released after hold { - funcShort = funcLong; - - if (!bKeyPressed) //ignore release if held - return; + return; } // held or released after short press beyond this point - action_opt_table[funcShort](); + action_opt_table[func](); } diff --git a/App/app/app.c b/App/app/app.c index 3b63c23d..d9319ebd 100644 --- a/App/app/app.c +++ b/App/app/app.c @@ -1497,7 +1497,6 @@ void cancelUserInputModes(void) if (gDTMF_InputMode || gDTMF_InputBox_Index > 0) { DTMF_clear_input_box(); - gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL; gRequestDisplayScreen = DISPLAY_MAIN; gUpdateDisplay = true; } @@ -1539,6 +1538,7 @@ void APP_TimeSlice500ms(void) } cancelUserInputModes(); + gHasVfoBackup = false; } } @@ -1909,7 +1909,7 @@ static void ProcessKey(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) // cancel user input cancelUserInputModes(); - gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL; + gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL; if (gMonitor) ACTION_Monitor(); //turn off the monitor diff --git a/App/app/common.c b/App/app/common.c index d647efef..fdeb93c5 100644 --- a/App/app/common.c +++ b/App/app/common.c @@ -3,6 +3,7 @@ #include "functions.h" #include "misc.h" #include "settings.h" +#include "ui/inputbox.h" #include "ui/ui.h" void COMMON_KeypadLockToggle() @@ -29,6 +30,11 @@ void COMMON_SwitchVFOs() #endif gEeprom.TX_VFO ^= 1; + if (gInputBoxIndex > 0) { + gInputBoxIndex = 0; + gHasVfoBackup = false; + } + if (gEeprom.CROSS_BAND_RX_TX != CROSS_BAND_OFF) gEeprom.CROSS_BAND_RX_TX = gEeprom.TX_VFO + 1; if (gEeprom.DUAL_WATCH != DUAL_WATCH_OFF) @@ -49,6 +55,11 @@ void COMMON_SwitchVFOMode() if (gEeprom.VFO_OPEN) #endif { + if (gInputBoxIndex > 0) { + gInputBoxIndex = 0; + gHasVfoBackup = false; + } + if (IS_MR_CHANNEL(gTxVfo->CHANNEL_SAVE)) { // swap to frequency mode gEeprom.ScreenChannel[gEeprom.TX_VFO] = gEeprom.FreqChannel[gEeprom.TX_VFO]; diff --git a/App/app/main.c b/App/app/main.c index 2222668b..df341e2d 100644 --- a/App/app/main.c +++ b/App/app/main.c @@ -51,7 +51,26 @@ static VFO_Info_t gVfoBackup; static uint16_t gScreenChannelBackup = 0; static uint16_t gFreqChannelBackup = 0; -static bool gHasVfoBackup = false; + +static void VFO_RestoreBackup(void) { + if (gHasVfoBackup) { + const uint8_t Vfo = gEeprom.TX_VFO; + + // Restore indices + gEeprom.ScreenChannel[Vfo] = gScreenChannelBackup; + gEeprom.FreqChannel[Vfo] = gFreqChannelBackup; + + // Restore full VFO + memcpy(gTxVfo, &gVfoBackup, sizeof(VFO_Info_t)); + + // Save and apply + SETTINGS_SaveVfoIndices(); + RADIO_ConfigureSquelchAndOutputPower(gTxVfo); + RADIO_SetupRegisters(true); + + gHasVfoBackup = false; + } +} static void toggle_chan_scanlist(void) { // toggle the selected channels scanlist setting @@ -613,6 +632,7 @@ static void MAIN_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) } gInputBoxIndex = 0; + gHasVfoBackup = false; uint8_t Channel = (gInputBox[0] * 10) + gInputBox[1]; if (Channel >= 1 && Channel <= ARRAY_SIZE(NoaaFrequencyTable)) { @@ -673,35 +693,10 @@ static void MAIN_Key_EXIT(bool bKeyPressed, bool bKeyHeld) if (bKeyHeld) { // exit key held down if (bKeyPressed) { - if (gInputBoxIndex > 0 || gDTMF_InputBox_Index > 0 || gDTMF_InputMode) - { // cancel key input mode (channel/frequency entry) + // Restore full VFO state on long press EXIT + VFO_RestoreBackup(); - // Restore full VFO state on long press EXIT - if (gHasVfoBackup) { - const uint8_t Vfo = gEeprom.TX_VFO; - - // Restore indices - gEeprom.ScreenChannel[Vfo] = gScreenChannelBackup; - gEeprom.FreqChannel[Vfo] = gFreqChannelBackup; - - // Restore full VFO - memcpy(gTxVfo, &gVfoBackup, sizeof(VFO_Info_t)); - - // Save and apply - SETTINGS_SaveVfoIndices(); - RADIO_ConfigureSquelchAndOutputPower(gTxVfo); - RADIO_SetupRegisters(true); - - gHasVfoBackup = false; - } - - gDTMF_InputMode = false; - gDTMF_InputBox_Index = 0; - memset(gDTMF_String, 0, sizeof(gDTMF_String)); - gInputBoxIndex = 0; - gRequestDisplayScreen = DISPLAY_MAIN; - gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL; - } + gRequestDisplayScreen = DISPLAY_MAIN; } return; @@ -726,23 +721,8 @@ static void MAIN_Key_EXIT(bool bKeyPressed, bool bKeyHeld) gInputBox[--gInputBoxIndex] = 10; // Restore full VFO state when back to 0 - if (gInputBoxIndex == 0 && gHasVfoBackup) { - const uint8_t Vfo = gEeprom.TX_VFO; - - // Restore indices - gEeprom.ScreenChannel[Vfo] = gScreenChannelBackup; - gEeprom.FreqChannel[Vfo] = gFreqChannelBackup; - - // Restore full VFO - memcpy(gTxVfo, &gVfoBackup, sizeof(VFO_Info_t)); - - // Save and apply - SETTINGS_SaveVfoIndices(); - RADIO_ConfigureSquelchAndOutputPower(gTxVfo); - RADIO_SetupRegisters(true); - - gHasVfoBackup = false; - } + if (gInputBoxIndex == 0) + VFO_RestoreBackup(); gKeyInputCountdown = key_input_timeout_500ms; channelMoveSwitch(); @@ -967,8 +947,10 @@ static void MAIN_Key_UP_DOWN(bool bKeyPressed, bool bKeyHeld, int8_t Direction) uint16_t Channel = gEeprom.ScreenChannel[gEeprom.TX_VFO]; if (bKeyHeld || !bKeyPressed) { // key held or released - if (gInputBoxIndex > 0) + if (gInputBoxIndex > 0) { gInputBoxIndex = 0; + gHasVfoBackup = false; + } if (!bKeyPressed) { if (!bKeyHeld || IS_FREQ_CHANNEL(Channel)) diff --git a/App/misc.c b/App/misc.c index 372d3708..29a2d376 100644 --- a/App/misc.c +++ b/App/misc.c @@ -242,6 +242,7 @@ volatile uint16_t gScanPauseDelayIn_10ms; uint16_t gMenuCountdown; bool gPttWasReleased; bool gPttWasPressed; +bool gHasVfoBackup; uint8_t gKeypadLocked; bool gFlagReconfigureVfos; uint8_t gVfoConfigureMode; diff --git a/App/misc.h b/App/misc.h index 7b1df678..77a0bbbe 100644 --- a/App/misc.h +++ b/App/misc.h @@ -368,6 +368,7 @@ extern AlarmState_t gAlarmState; extern uint16_t gMenuCountdown; extern bool gPttWasReleased; extern bool gPttWasPressed; +extern bool gHasVfoBackup; extern bool gFlagReconfigureVfos; extern uint8_t gVfoConfigureMode; extern bool gFlagResetVfos; From 6fdb17d64a59d5696dd635fb6135ac3f032948fe Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Mon, 23 Mar 2026 00:31:16 +0100 Subject: [PATCH 35/50] Align BK4829 AGC runtime with stock firmware --- App/driver/bk4829.c | 54 ++++++++++++++------------------------------- App/radio.c | 11 +++++---- 2 files changed, 23 insertions(+), 42 deletions(-) diff --git a/App/driver/bk4829.c b/App/driver/bk4829.c index c5858801..bec97728 100644 --- a/App/driver/bk4829.c +++ b/App/driver/bk4829.c @@ -334,45 +334,9 @@ void BK4819_SetAGC(bool enable) void BK4819_InitAGC(bool amModulation) { - // REG_10, REG_11, REG_12 REG_13, REG_14 - // - // Rx AGC Gain Table[]. (Index Max->Min is 3,2,1,0,-1) - // - // <15:10> ??? - // - // <9:8> LNA Gain Short - // 3 = 0dB <<< 1o11 read from spectrum reference manual - // 2 = -24dB -19 -11 - // 1 = -30dB -24 -16 - // 0 = -33dB -28 -19 - // - // <7:5> LNA Gain - // 7 = 0dB - // 6 = -2dB - // 5 = -4dB - // 4 = -6dB - // 3 = -9dB - // 2 = -14dB <<< - // 1 = -19dB - // 0 = -24dB - // - // <4:3> MIXER Gain - // 3 = 0dB <<< - // 2 = -3dB - // 1 = -6dB - // 0 = -8dB - // - // <2:0> PGA Gain - // 7 = 0dB - // 6 = -3dB <<< - // 5 = -6dB - // 4 = -9dB - // 3 = -15dB - // 2 = -21dB - // 1 = -27dB - // 0 = -33dB - // + /* + // Old strategy BK4819_WriteRegister(BK4819_REG_13, 0x03BE); // 0x03BE / 000000 11 101 11 110 / -7dB BK4819_WriteRegister(BK4819_REG_12, 0x037B); // 0x037B / 000000 11 011 11 011 / -24dB BK4819_WriteRegister(BK4819_REG_11, 0x027B); // 0x027B / 000000 10 011 11 011 / -43dB @@ -387,7 +351,21 @@ void BK4819_InitAGC(bool amModulation) } BK4819_WriteRegister(BK4819_REG_7B, 0x8420); + */ + // New strategy + // Keep the runtime AGC profile aligned with the stock BK4829 firmware + // until modulation-specific AGC changes are proven on hardware. + + (void)amModulation; + + BK4819_WriteRegister(BK4819_REG_10, 0x0318); + BK4819_WriteRegister(BK4819_REG_11, 0x033A); + BK4819_WriteRegister(BK4819_REG_12, 0x03DB); + BK4819_WriteRegister(BK4819_REG_13, 0x03DF); + BK4819_WriteRegister(BK4819_REG_14, 0x0210); + BK4819_WriteRegister(BK4819_REG_49, 0x2AB2); + BK4819_WriteRegister(BK4819_REG_7B, 0x73DC); } int8_t BK4819_GetRxGain_dB(void) diff --git a/App/radio.c b/App/radio.c index 14b50b1b..e728f69e 100644 --- a/App/radio.c +++ b/App/radio.c @@ -1128,13 +1128,16 @@ void RADIO_SetModulation(ModulationMode_t modulation) BK4819_WriteRegister(0x31,uVar1 | 1); BK4819_WriteRegister(0x42,0x6f5c); BK4819_WriteRegister(0x2a,0x7434); - BK4819_WriteRegister(0x2b,0x300); + BK4819_WriteRegister(0x2b,0x400); BK4819_WriteRegister(0x2f,0x9990); //BK4819_WriteRegister(0x54, 0x9775); //BK4819_WriteRegister(0x55, 0x32c6); - BK4819_WriteRegister(0x54, 0x8846); - BK4819_WriteRegister(0x55, 0x38C0); + //BK4819_WriteRegister(0x54, 0x8846); + //BK4819_WriteRegister(0x55, 0x38C0); + + BK4819_WriteRegister(0x54, 0x9009); + BK4819_WriteRegister(0x55, 0x31a9); BK4819_SetFilterBandwidth(BK4819_FILTER_BW_AM, true); } @@ -1149,7 +1152,7 @@ void RADIO_SetModulation(ModulationMode_t modulation) void RADIO_SetupAGC(bool listeningAM, bool disable) { - static uint8_t lastSettings; + static uint8_t lastSettings = 0xFF; uint8_t newSettings = (listeningAM << 1) | disable; if (lastSettings == newSettings) return; From bbbc55306455cbddd783fffa4db818434a1963a0 Mon Sep 17 00:00:00 2001 From: mrkusypl Date: Tue, 24 Mar 2026 00:31:45 +0100 Subject: [PATCH 36/50] Improvements to the FM radio and side buttons --- App/app/action.c | 53 +++++++++++++++++++++++++++++++++++++++++++ App/app/app.c | 12 ++++------ App/app/fm.c | 35 +++++++++++++++++++++++----- App/app/main.c | 26 ++++++++------------- App/audio.c | 10 +++++++- App/driver/keyboard.c | 5 ++++ App/driver/keyboard.h | 2 ++ App/frequencies.c | 13 ++++------- App/main.c | 1 + App/ui/ui.c | 3 +-- 10 files changed, 119 insertions(+), 41 deletions(-) diff --git a/App/app/action.c b/App/app/action.c index f62d4bb0..3c7d40fd 100644 --- a/App/app/action.c +++ b/App/app/action.c @@ -290,6 +290,8 @@ void ACTION_SwitchDemodul(void) void ACTION_Handle(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) { + HideFKeyIcon(); + if (gScreenToDisplay == DISPLAY_MAIN && gDTMF_InputMode){ // entering DTMF code @@ -357,6 +359,55 @@ void ACTION_Handle(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) } // held or released after short press beyond this point + +#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS + if(gEeprom.MENU_LOCK) { // do not run these actions in RescueOps mode + switch (func) { + case ACTION_OPT_SCAN: + case ACTION_OPT_VFO_MR: + #ifdef ENABLE_FMRADIO + case ACTION_OPT_FM: + #endif + return; + break; + + default: + break; + } + } +#endif + +#ifdef ENABLE_FMRADIO + if (gFmRadioMode) { // do not run these actions in FM radio mode + switch (func) { + case ACTION_OPT_POWER: + case ACTION_OPT_MONITOR: + case ACTION_OPT_A_B: + case ACTION_OPT_VFO_MR: + case ACTION_OPT_SWITCH_DEMODUL: + #ifdef ENABLE_VOX + case ACTION_OPT_VOX: + #endif + #ifdef ENABLE_FEAT_F4HWN + case ACTION_OPT_RXMODE: + case ACTION_OPT_MAINONLY: + case ACTION_OPT_WN: + #ifdef ENABLE_FEAT_F4HWN_AUDIO + case ACTION_OPT_RXA: + #endif + #ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS + case ACTION_OPT_POWER_HIGH: + case ACTION_OPT_REMOVE_OFFSET: + #endif + #endif + gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL; + return; + + default: + break; + } + } +#endif action_opt_table[func](); } @@ -546,6 +597,8 @@ void ACTION_RxA(void) void ACTION_Ptt(void) { gSetting_set_ptt_session = !gSetting_set_ptt_session; + + ACTION_Update(); } void ACTION_Wn(void) diff --git a/App/app/app.c b/App/app/app.c index d9319ebd..9de9ab6c 100644 --- a/App/app/app.c +++ b/App/app/app.c @@ -1503,10 +1503,10 @@ void cancelUserInputModes(void) if (gWasFKeyPressed || gKeyInputCountdown > 0 || gInputBoxIndex > 0) { - gWasFKeyPressed = false; + HideFKeyIcon(); + gInputBoxIndex = 0; gKeyInputCountdown = 0; - gUpdateStatus = true; gUpdateDisplay = true; } } @@ -1528,7 +1528,7 @@ void APP_TimeSlice500ms(void) if (--gKeyInputCountdown == 0) { - if (IS_MR_CHANNEL(gTxVfo->CHANNEL_SAVE) && (gInputBoxIndex > 0 && gInputBoxIndex < 4)) + if (IS_MR_CHANNEL(gTxVfo->CHANNEL_SAVE) && (gInputBoxIndex > 0 && gInputBoxIndex < 4) && (!gFmRadioMode)) { channelMoveSwitch(); @@ -1724,13 +1724,12 @@ void APP_TimeSlice500ms(void) */ DTMF_clear_input_box(); - gWasFKeyPressed = false; + HideFKeyIcon(); gInputBoxIndex = 0; gAskToSave = false; gAskToDelete = false; - gUpdateStatus = true; gUpdateDisplay = true; GUI_DisplayType_t disp = DISPLAY_INVALID; @@ -2022,8 +2021,7 @@ static void ProcessKey(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) if (gWasFKeyPressed && (Key == KEY_PTT || Key == KEY_EXIT || Key == KEY_SIDE1 || Key == KEY_SIDE2)) { #endif // cancel the F-key - gWasFKeyPressed = false; - gUpdateStatus = true; + HideFKeyIcon(); } if (bFlag) { diff --git a/App/app/fm.c b/App/app/fm.c index 5e79f95c..6614f41b 100644 --- a/App/app/fm.c +++ b/App/app/fm.c @@ -257,6 +257,7 @@ static void Key_DIGITS(KEY_Code_t Key, uint8_t state) } INPUTBOX_Append(Key); + gKeyInputCountdown = key_input_timeout_500ms; gRequestDisplayScreen = DISPLAY_FM; @@ -272,6 +273,8 @@ static void Key_DIGITS(KEY_Code_t Key, uint8_t state) uint32_t Frequency; gInputBoxIndex = 0; + gKeyInputCountdown = 1; + Frequency = StrToUL(INPUTBOX_GetAscii()); if (Frequency < BK1080_GetFreqLoLimit(gEeprom.FM_Band) || BK1080_GetFreqHiLimit(gEeprom.FM_Band) < Frequency) { @@ -294,6 +297,8 @@ static void Key_DIGITS(KEY_Code_t Key, uint8_t state) uint8_t Channel; gInputBoxIndex = 0; + gKeyInputCountdown = 1; + Channel = ((gInputBox[0] * 10) + gInputBox[1]) - 1; if (State == STATE_MR_MODE) { @@ -336,8 +341,7 @@ static void Key_FUNC(KEY_Code_t Key, uint8_t state) bool autoScan = gWasFKeyPressed || (state == BUTTON_EVENT_HELD); gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL; - gWasFKeyPressed = false; - gUpdateStatus = true; + HideFKeyIcon(); gRequestDisplayScreen = DISPLAY_FM; switch (Key) { @@ -404,6 +408,7 @@ static void Key_EXIT(uint8_t state) } else { gInputBox[--gInputBoxIndex] = 10; + gKeyInputCountdown = key_input_timeout_500ms; if (gInputBoxIndex) { if (gInputBoxIndex != 1) { @@ -435,14 +440,25 @@ static void Key_EXIT(uint8_t state) static void Key_MENU(uint8_t state) { - if (state != BUTTON_EVENT_SHORT) + if (state == BUTTON_EVENT_HELD) { + ACTION_Handle(KEY_MENU, true, true); return; - + } + else if (state != BUTTON_EVENT_SHORT) { + return; + } gRequestDisplayScreen = DISPLAY_FM; gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL; + HideFKeyIcon(); + if (gFM_ScanState == FM_SCAN_OFF) { + if (gInputBoxIndex) { + gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL; + return; + } + if (!gEeprom.FM_IsMrMode) { if (gAskToSave) { gFM_Channels[gFM_ChannelPosition] = gEeprom.FM_FrequencyPlaying; @@ -479,6 +495,8 @@ static void Key_MENU(uint8_t state) static void Key_UP_DOWN(uint8_t state, int8_t Step) { + HideFKeyIcon(); + if (state == BUTTON_EVENT_PRESSED) { if (gInputBoxIndex) { gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL; @@ -563,9 +581,14 @@ void FM_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) case KEY_PTT: GENERIC_Key_PTT(bKeyPressed); break; - default: - if (!bKeyHeld && bKeyPressed) + case KEY_SIDE1: + case KEY_SIDE2: + if (state != BUTTON_EVENT_PRESSED) { gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL; + HideFKeyIcon(); + } + break; + default: break; } } diff --git a/App/app/main.c b/App/app/main.c index df341e2d..5eb8d7d2 100644 --- a/App/app/main.c +++ b/App/app/main.c @@ -120,8 +120,7 @@ static void processFKeyFunction(const KEY_Code_t Key, const bool beep) #ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS if(gEeprom.MENU_LOCK == true && Key != 2) { - gUpdateStatus = true; - gWasFKeyPressed = false; + HideFKeyIcon(); return; } @@ -141,8 +140,7 @@ static void processFKeyFunction(const KEY_Code_t Key, const bool beep) case KEY_1: if (!IS_FREQ_CHANNEL(gTxVfo->CHANNEL_SAVE)) { - gWasFKeyPressed = false; - gUpdateStatus = true; + HideFKeyIcon(); #ifdef ENABLE_COPY_CHAN_TO_VFO if (!gEeprom.VFO_OPEN || gCssBackgroundScan) { @@ -224,11 +222,10 @@ static void processFKeyFunction(const KEY_Code_t Key, const bool beep) break; case KEY_4: - gWasFKeyPressed = false; + HideFKeyIcon(); gBackup_CROSS_BAND_RX_TX = gEeprom.CROSS_BAND_RX_TX; - gEeprom.CROSS_BAND_RX_TX = CROSS_BAND_OFF; - gUpdateStatus = true; + gEeprom.CROSS_BAND_RX_TX = CROSS_BAND_OFF; SCANNER_Start(false); gRequestDisplayScreen = DISPLAY_SCANNER; @@ -339,8 +336,7 @@ static void processFKeyFunction(const KEY_Code_t Key, const bool beep) #endif default: - gUpdateStatus = true; - gWasFKeyPressed = false; + HideFKeyIcon(); if (beep) gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL; @@ -430,8 +426,7 @@ static void MAIN_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) gRequestDisplayScreen = DISPLAY_MAIN; } - gWasFKeyPressed = false; - gUpdateStatus = true; + HideFKeyIcon(); processFKeyFunction(Key, true); } @@ -654,8 +649,7 @@ static void MAIN_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) return; } - gWasFKeyPressed = false; - gUpdateStatus = true; + HideFKeyIcon(); #ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS if(gEeprom.MENU_LOCK == true && Key != 2) { @@ -815,8 +809,7 @@ static void MAIN_Key_MENU(bool bKeyPressed, bool bKeyHeld) #ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS if(gEeprom.MENU_LOCK == true) { - gUpdateStatus = true; - gWasFKeyPressed = false; + HideFKeyIcon(); return; } @@ -852,8 +845,7 @@ static void MAIN_Key_STAR(bool bKeyPressed, bool bKeyHeld) #ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS if(gEeprom.MENU_LOCK == true) { - gUpdateStatus = true; - gWasFKeyPressed = false; + HideFKeyIcon(); return; // prevent F function if MENU LOCK is true } diff --git a/App/audio.c b/App/audio.c index 022515b8..44295ef0 100644 --- a/App/audio.c +++ b/App/audio.c @@ -177,11 +177,19 @@ void AUDIO_PlayBeep(BEEP_Type_t Beep) SYSTEM_DelayMs(5); BK4819_WriteRegister(BK4819_REG_71, ToneConfig); +#ifdef ENABLE_FMRADIO + const bool isFmRadio = gFmRadioMode; + + if (isFmRadio) + SYSTEM_DelayMs(10); +#endif + + if (gEnableSpeaker) AUDIO_AudioPathOn(); #ifdef ENABLE_FMRADIO - if (gFmRadioMode) + if (isFmRadio) BK1080_Mute(false); #endif diff --git a/App/driver/keyboard.c b/App/driver/keyboard.c index 9b3e7815..0634ac1e 100644 --- a/App/driver/keyboard.c +++ b/App/driver/keyboard.c @@ -260,4 +260,9 @@ KEY_Code_t KEYBOARD_GetKey(void) btn = KEY_PTT; } return btn; +} + +void HideFKeyIcon(void) { + gWasFKeyPressed = false; + gUpdateStatus = true; } \ No newline at end of file diff --git a/App/driver/keyboard.h b/App/driver/keyboard.h index a4e1b406..3fe092a9 100644 --- a/App/driver/keyboard.h +++ b/App/driver/keyboard.h @@ -71,4 +71,6 @@ bool KEYBOARD_ProcessProtocolByte(ParseState_t *state, uint8_t b); KEY_Code_t KEYBOARD_Poll(void); KEY_Code_t KEYBOARD_GetKey(void); +void HideFKeyIcon(void); + #endif \ No newline at end of file diff --git a/App/frequencies.c b/App/frequencies.c index 1a71b950..53a9bb06 100644 --- a/App/frequencies.c +++ b/App/frequencies.c @@ -163,11 +163,8 @@ int32_t TX_freq_check(const uint32_t Frequency) { // return '0' if TX frequency is allowed // otherwise return '-1' - if (Frequency < frequencyBandTable[0].lower || Frequency > frequencyBandTable[BAND_N_ELEM - 1].upper) - return -1; // not allowed outside this range - - if (Frequency >= BX4819_band1.upper && Frequency < BX4819_band2.lower) - return -1; // BX chip does not work in this range + if (RX_freq_check(Frequency)) + return -1; switch (gSetting_F_LOCK) { @@ -280,10 +277,10 @@ int32_t RX_freq_check(const uint32_t Frequency) // otherwise return '-1' if (Frequency < frequencyBandTable[0].lower || Frequency > frequencyBandTable[BAND_N_ELEM - 1].upper) - return -1; + return -1; // not allowed outside this range if (Frequency >= BX4819_band1.upper && Frequency < BX4819_band2.lower) - return -1; + return -1; // BX chip does not work in this range - return 0; // OK frequency + return 0; // OK frequency } diff --git a/App/main.c b/App/main.c index 1eb13710..e3522787 100644 --- a/App/main.c +++ b/App/main.c @@ -129,6 +129,7 @@ void Main(void) if (BootMode == BOOT_MODE_RESCUE_OPS) { gEeprom.MENU_LOCK = !gEeprom.MENU_LOCK; + gEeprom.CURRENT_STATE = 0; // Don't resume is active... SETTINGS_SaveSettings(); } diff --git a/App/ui/ui.c b/App/ui/ui.c index f6906289..387a2000 100644 --- a/App/ui/ui.c +++ b/App/ui/ui.c @@ -94,9 +94,8 @@ void GUI_SelectNextDisplay(GUI_DisplayType_t Display) gAskForConfirmation = 0; gAskToSave = false; gAskToDelete = false; - gWasFKeyPressed = false; - gUpdateStatus = true; + HideFKeyIcon(); } gScreenToDisplay = Display; From dea747c628ee150d83ea7b62e6ff9554e0081295 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Tue, 24 Mar 2026 03:49:52 +0100 Subject: [PATCH 37/50] Fix W/N (issue #233) --- App/app/action.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/App/app/action.c b/App/app/action.c index f62d4bb0..7a657d4d 100644 --- a/App/app/action.c +++ b/App/app/action.c @@ -550,16 +550,17 @@ void ACTION_Ptt(void) void ACTION_Wn(void) { - if (gRxVfo->Modulation == MODULATION_AM) + const bool isRx = FUNCTION_IsRx(); + VFO_Info_t *pVfo = isRx ? gRxVfo : gTxVfo; + + pVfo->CHANNEL_BANDWIDTH = !pVfo->CHANNEL_BANDWIDTH; + + if (pVfo->Modulation == MODULATION_AM) { BK4819_SetFilterBandwidth(BK4819_FILTER_BW_AM, true); return; } - const bool isRx = FUNCTION_IsRx(); - - VFO_Info_t *pVfo = isRx ? gRxVfo : gTxVfo; - pVfo->CHANNEL_BANDWIDTH = !pVfo->CHANNEL_BANDWIDTH; uint8_t bw = pVfo->CHANNEL_BANDWIDTH; #ifdef ENABLE_FEAT_F4HWN_NARROWER From 122cf48d3e879dc214a743baf92111034ef35267 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Tue, 24 Mar 2026 05:09:57 +0100 Subject: [PATCH 38/50] Clarify BK4829 REG_43 docs and keep stock AM/W/N behavior coherent --- App/driver/bk4829.c | 131 +++++++++++++++++++++----------------------- 1 file changed, 61 insertions(+), 70 deletions(-) diff --git a/App/driver/bk4829.c b/App/driver/bk4829.c index bec97728..15f35845 100644 --- a/App/driver/bk4829.c +++ b/App/driver/bk4829.c @@ -603,100 +603,91 @@ void BK4819_EnableVox(uint16_t VoxEnableThreshold, uint16_t VoxDisableThreshold) void BK4819_SetFilterBandwidth(const BK4819_FilterBandwidth_t Bandwidth, const bool weak_no_different) { - // REG_43 - // <15> 0 ??? + (void)weak_no_different; + + // REG_43 on BK4829, per BK4829 Registers Table. // - // <14:12> 4 RF filter bandwidth - // 0 = 1.7 kHz - // 1 = 2.0 kHz - // 2 = 2.5 kHz - // 3 = 3.0 kHz - // 4 = 3.75 kHz - // 5 = 4.0 kHz - // 6 = 4.25 kHz - // 7 = 4.5 kHz - // if <5> == 1, RF filter bandwidth * 2 + // <14:12> RF filter bandwidth (Apass = 0.1dB) + // 000 = 2.0 kHz + // 001 = 2.5 kHz + // 010 = 3.0 kHz + // 011 = 3.5 kHz + // 100 = 4.0 kHz + // 101 = 4.5 kHz + // 110 = 5.0 kHz + // 111 = 5.5 kHz + // if REG_43<5> == 1, RF filter bandwidth *= 2 // - // <11:9> 0 RF filter bandwidth when signal is weak - // 0 = 1.7 kHz - // 1 = 2.0 kHz - // 2 = 2.5 kHz - // 3 = 3.0 kHz - // 4 = 3.75 kHz - // 5 = 4.0 kHz - // 6 = 4.25 kHz - // 7 = 4.5 kHz - // if <5> == 1, RF filter bandwidth * 2 + // <11:9> RF filter bandwidth when signal is weak (Apass = 0.1dB) + // 000 = 2.0 kHz + // 001 = 2.5 kHz + // 010 = 3.0 kHz + // 011 = 3.5 kHz + // 100 = 4.0 kHz + // 101 = 4.5 kHz + // 110 = 5.0 kHz + // 111 = 5.5 kHz + // if REG_43<5> == 1, RF filter bandwidth *= 2 // - // <8:6> 1 AFTxLPF2 filter Band Width - // 1 = 2.5 kHz (for 12.5k channel space) - // 2 = 2.75 kHz - // 0 = 3.0 kHz (for 25k channel space) - // 3 = 3.5 kHz - // 4 = 4.5 kHz - // 5 = 4.25 kHz - // 6 = 4.0 kHz - // 7 = 3.75 kHz + // <8:6> AF Tx LPF2 filter bandwidth (Apass = 1dB) + // 100 = 5.5 kHz + // 101 = 5.0 kHz + // 110 = 4.5 kHz + // 111 = 4.0 kHz + // 000 = 3.0 kHz + // 001 = 2.5 kHz + // 010 = 2.75 kHz + // 011 = 3.5 kHz // - // <5:4> 0 BW Mode Selection - // 0 = 12.5k - // 1 = 6.25k - // 2 = 25k/20k + // <5:4> BW mode selection + // 00 = 12.5k + // 01 = 6.25k + // 10 = 25k/20k // - // <3> 1 ??? + // <2> gain after FM demodulation + // 0 = 0 dB + // 1 = 6 dB // - // <2> 0 Gain after FM Demodulation - // 0 = 0dB - // 1 = 6dB - // - // <1:0> 0 ??? + // <3> and <1:0> remain undocumented here. uint16_t val = 0; switch (Bandwidth) { case BK4819_FILTER_BW_WIDE: // 25kHz - // if (weak_no_different) { - // // make the RX bandwidth the same with weak signals - // val = 0x3628; // Old value 0x49a8 < v3.6 - // } else { - // // with weak RX signals the RX bandwidth is reduced - // val = 0x3428; // Old value 0x45a8 < v3.6 - // } + // 0x3028 = (0b011 << 12) | (0b000 << 9) | (0b000 << 6) | + // (0b10 << 4) | (1 << 3) + // = RF 3.5 kHz and weak-RF 2.0 kHz, both doubled because + // bit<5> is set in 25k/20k mode => RF 7.0 kHz, weak-RF + // 4.0 kHz, AF Tx LPF2 3.0 kHz, FM gain 0 dB. val = 0x3028; break; case BK4819_FILTER_BW_NARROW: // 12.5kHz - // if (weak_no_different) { - // // make the RX bandwidth the same with weak signals - // val = 0x3648; // Old value 0x4808 < v3.6 - // } else { - // // with weak RX signals the RX bandwidth is reduced - // val = 0x3448; // Old value 0x4408 < v3.6 - // } + // 0x4048 = (0b100 << 12) | (0b000 << 9) | (0b001 << 6) | + // (0b00 << 4) | (1 << 3) + // = RF 4.0 kHz, weak-RF 2.0 kHz, AF Tx LPF2 2.5 kHz, + // 12.5k mode, FM gain 0 dB. val = 0x4048; break; case BK4819_FILTER_BW_NARROWER: // 6.25kHz - // if (weak_no_different) { - // // make the RX bandwidth the same with weak signals - // val = 0x1348; // Old value 0x3658 < v3.6 - // } else { - // // with weak RX signals the RX bandwidth is reduced - // val = 0x1148; // Old value 0x3658 < v3.6 - // } + // 0x205C = (0b010 << 12) | (0b000 << 9) | (0b001 << 6) | + // (0b01 << 4) | (1 << 3) | (1 << 2) + // = RF 3.0 kHz, weak-RF 2.0 kHz, AF Tx LPF2 2.5 kHz, + // 6.25k mode, FM gain +6 dB. val = 0x205C; break; - case BK4819_FILTER_BW_AM: // 8.33kHz - // if (weak_no_different) { - // // make the RX bandwidth the same with weak signals - // val = 0x4858; - // } else { - // // with weak RX signals the RX bandwidth is reduced - // val = 0x4458; - // } + case BK4819_FILTER_BW_AM: // Stock AM preset + // 0x345C = (0b011 << 12) | (0b010 << 9) | (0b001 << 6) | + // (0b01 << 4) | (1 << 3) | (1 << 2) + // = RF 3.5 kHz, weak-RF 3.0 kHz, AF Tx LPF2 2.5 kHz, + // 6.25k mode, bit<2> set. The "8.33kHz AM" label used in + // the codebase is functional/empirical; it does not come + // directly from the BW mode bits of REG_43. val = 0x345C; break; + default: val = 0x5C; break; From 2e6dfe3bc778f5c7041ff124c5aa3573e5a024b1 Mon Sep 17 00:00:00 2001 From: mrkusypl Date: Wed, 25 Mar 2026 19:22:23 +0100 Subject: [PATCH 39/50] Revert changes related to RescueOps --- App/app/action.c | 17 ----------------- App/main.c | 1 - 2 files changed, 18 deletions(-) diff --git a/App/app/action.c b/App/app/action.c index 3c7d40fd..d2110c7c 100644 --- a/App/app/action.c +++ b/App/app/action.c @@ -360,23 +360,6 @@ void ACTION_Handle(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) // held or released after short press beyond this point -#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS - if(gEeprom.MENU_LOCK) { // do not run these actions in RescueOps mode - switch (func) { - case ACTION_OPT_SCAN: - case ACTION_OPT_VFO_MR: - #ifdef ENABLE_FMRADIO - case ACTION_OPT_FM: - #endif - return; - break; - - default: - break; - } - } -#endif - #ifdef ENABLE_FMRADIO if (gFmRadioMode) { // do not run these actions in FM radio mode switch (func) { diff --git a/App/main.c b/App/main.c index e3522787..1eb13710 100644 --- a/App/main.c +++ b/App/main.c @@ -129,7 +129,6 @@ void Main(void) if (BootMode == BOOT_MODE_RESCUE_OPS) { gEeprom.MENU_LOCK = !gEeprom.MENU_LOCK; - gEeprom.CURRENT_STATE = 0; // Don't resume is active... SETTINGS_SaveSettings(); } From 179c271618aef52c24866c90775d82a162c10cf2 Mon Sep 17 00:00:00 2001 From: mrkusypl Date: Wed, 25 Mar 2026 21:36:38 +0100 Subject: [PATCH 40/50] Save the squelch level adjusted using F+UP/DOWN --- App/app/main.c | 1 + 1 file changed, 1 insertion(+) diff --git a/App/app/main.c b/App/app/main.c index 5eb8d7d2..f06c6fb4 100644 --- a/App/app/main.c +++ b/App/app/main.c @@ -307,6 +307,7 @@ static void processFKeyFunction(const KEY_Code_t Key, const bool beep) gEeprom.SQUELCH_LEVEL = (gEeprom.SQUELCH_LEVEL > 0) ? gEeprom.SQUELCH_LEVEL - 1 : 0; } gVfoConfigureMode = VFO_CONFIGURE; + gRequestSaveSettings = true; } gWasFKeyPressed = false; From ab7b4ca094632609b6c5dcb56cac94068c050d5b Mon Sep 17 00:00:00 2001 From: mrkusypl Date: Wed, 25 Mar 2026 22:44:20 +0100 Subject: [PATCH 41/50] =?UTF-8?q?Adding=20the=20=E2=80=98gRequestSaveSquel?= =?UTF-8?q?ch=E2=80=99=20flag=20to=20the=20squelch=20level=20save?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/app/app.c | 1 + App/app/main.c | 1 - App/app/menu.c | 1 + App/misc.c | 1 + App/misc.h | 1 + App/settings.c | 5 ++++- 6 files changed, 8 insertions(+), 2 deletions(-) diff --git a/App/app/app.c b/App/app/app.c index 9de9ab6c..12a8777b 100644 --- a/App/app/app.c +++ b/App/app/app.c @@ -2138,6 +2138,7 @@ Skip: else flagSaveSettings = 1; gRequestSaveSettings = false; + gRequestSaveSquelch = false; gUpdateStatus = true; } diff --git a/App/app/main.c b/App/app/main.c index f06c6fb4..5eb8d7d2 100644 --- a/App/app/main.c +++ b/App/app/main.c @@ -307,7 +307,6 @@ static void processFKeyFunction(const KEY_Code_t Key, const bool beep) gEeprom.SQUELCH_LEVEL = (gEeprom.SQUELCH_LEVEL > 0) ? gEeprom.SQUELCH_LEVEL - 1 : 0; } gVfoConfigureMode = VFO_CONFIGURE; - gRequestSaveSettings = true; } gWasFKeyPressed = false; diff --git a/App/app/menu.c b/App/app/menu.c index fdaaf997..238c5964 100644 --- a/App/app/menu.c +++ b/App/app/menu.c @@ -480,6 +480,7 @@ void MENU_AcceptSetting(void) case MENU_SQL: gEeprom.SQUELCH_LEVEL = gSubMenuSelection; gVfoConfigureMode = VFO_CONFIGURE; + gRequestSaveSquelch = true; break; case MENU_STEP: diff --git a/App/misc.c b/App/misc.c index 29a2d376..54c64fe6 100644 --- a/App/misc.c +++ b/App/misc.c @@ -250,6 +250,7 @@ bool gFlagResetVfos; bool gRequestSaveVFO; uint16_t gRequestSaveChannel; bool gRequestSaveSettings; +bool gRequestSaveSquelch; #ifdef ENABLE_FMRADIO bool gRequestSaveFM; #endif diff --git a/App/misc.h b/App/misc.h index 77a0bbbe..dcbdbdc7 100644 --- a/App/misc.h +++ b/App/misc.h @@ -375,6 +375,7 @@ extern bool gFlagResetVfos; extern bool gRequestSaveVFO; extern uint16_t gRequestSaveChannel; extern bool gRequestSaveSettings; +extern bool gRequestSaveSquelch; #ifdef ENABLE_FMRADIO extern bool gRequestSaveFM; #endif diff --git a/App/settings.c b/App/settings.c index 045ab651..8a38f8b1 100644 --- a/App/settings.c +++ b/App/settings.c @@ -766,7 +766,10 @@ void SETTINGS_SaveSettings(void) #ifdef ENABLE_FEAT_F4HWN_AUDIO State[0] = gSetting_set_audio; #endif - State[1] = gEeprom.SQUELCH_LEVEL; + if (gRequestSaveSquelch) + { + State[1] = gEeprom.SQUELCH_LEVEL; + } State[2] = gEeprom.TX_TIMEOUT_TIMER; #ifdef ENABLE_NOAA State[3] = gEeprom.NOAA_AUTO_SCAN; From c51033db777332f6118f01e5b335231697d51c80 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Thu, 26 Mar 2026 03:24:17 +0100 Subject: [PATCH 42/50] bk4829: align RX squelch REG_4E with stock firmware --- App/driver/bk4829.c | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/App/driver/bk4829.c b/App/driver/bk4829.c index 15f35845..63d9ee3d 100644 --- a/App/driver/bk4829.c +++ b/App/driver/bk4829.c @@ -766,21 +766,20 @@ void BK4819_SetupSquelch( // <13:11> 5 Squelch = open Delay Setting // 0 ~ 7 // - // <10:9> 7 Squelch = close Delay Setting + // <10:9> 3 Squelch = close Delay Setting // 0 ~ 3 // - // <8> 0 ??? + // <8> 1 ??? // // <7:0> 8 Glitch threshold for Squelch = open // 0 ~ 255 // - BK4819_WriteRegister(BK4819_REG_4E, // 01 101 11 1 00000000 - - // original (*) - (1u << 14) | // 1 ??? - (5u << 11) | // *5 squelch = open delay .. 0 ~ 7 - (6u << 9) | // *3 squelch = close delay .. 0 ~ 3 - SquelchOpenGlitchThresh); // 0 ~ 255 + BK4819_WriteRegister(BK4819_REG_4E, + (1u << 14) | // 1 ??? + (5u << 11) | // 5 squelch = open delay .. 0 ~ 7 + (3u << 9) | // 3 squelch = close delay .. 0 ~ 3 + (1u << 8) | // 1 ??? (matches stock BK4829) + SquelchOpenGlitchThresh); // 0 ~ 255 // REG_4F From cebbda72ecb6cf23387e8da8705248ba6a198e65 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Thu, 26 Mar 2026 04:58:49 +0100 Subject: [PATCH 43/50] Add audio scope feature just for fun --- App/CMakeLists.txt | 1 + App/app/app.c | 13 +++++- App/ui/main.c | 100 ++++++++++++++++++++++++++++++++++++++++++++- App/ui/main.h | 7 ++++ CMakePresets.json | 2 + 5 files changed, 120 insertions(+), 3 deletions(-) diff --git a/App/CMakeLists.txt b/App/CMakeLists.txt index 697fbeda..97673365 100644 --- a/App/CMakeLists.txt +++ b/App/CMakeLists.txt @@ -161,6 +161,7 @@ enable_feature(ENABLE_SQUELCH_MORE_SENSITIVE) enable_feature(ENABLE_FASTER_CHANNEL_SCAN) enable_feature(ENABLE_RSSI_BAR) enable_feature(ENABLE_AUDIO_BAR) +enable_feature(ENABLE_FEAT_F4HWN_AUDIO_SCOPE) enable_feature(ENABLE_COPY_CHAN_TO_VFO) enable_feature(ENABLE_REDUCE_LOW_MID_TX_POWER) enable_feature(ENABLE_BYP_RAW_DEMODULATORS) diff --git a/App/app/app.c b/App/app/app.c index 12a8777b..b5ed912f 100644 --- a/App/app/app.c +++ b/App/app/app.c @@ -1364,12 +1364,23 @@ void APP_TimeSlice10ms(void) if (gCurrentFunction == FUNCTION_TRANSMIT) { // transmitting -#ifdef ENABLE_AUDIO_BAR +#if defined(ENABLE_AUDIO_BAR) && !defined(ENABLE_FEAT_F4HWN_AUDIO_SCOPE) if (gSetting_mic_bar && (gFlashLightBlinkCounter % (150 / 10)) == 0) // once every 150ms UI_DisplayAudioBar(); #endif } +#ifdef ENABLE_FEAT_F4HWN_AUDIO_SCOPE + // Sample audio amplitude every 30ms + if ((gFlashLightBlinkCounter % 3) == 0) + UI_AudioScope_AddSample(); + // Refresh display every 30ms during TX only (FM RX has no usable audio register) + if ((gFlashLightBlinkCounter % 3) == 0 && + gScreenToDisplay == DISPLAY_MAIN && + gCurrentFunction == FUNCTION_TRANSMIT) + UI_DisplayAudioScope(); +#endif + bool gUpdateDisplayCurrent = gUpdateDisplay; bool gUpdateStatusCurrent = gUpdateStatus; diff --git a/App/ui/main.c b/App/ui/main.c index e49a8a6a..1aa29248 100644 --- a/App/ui/main.c +++ b/App/ui/main.c @@ -158,15 +158,17 @@ static void DrawLevelBar(uint8_t xpos, uint8_t line, uint8_t level, uint8_t bars #endif #ifdef ENABLE_AUDIO_BAR - // Approximation of a logarithmic scale using integer arithmetic -uint8_t log2_approx(unsigned int value) { +static uint8_t log2_approx(unsigned int value) { uint8_t log = 0; while (value >>= 1) { log++; } return log; } +#endif + +#ifdef ENABLE_AUDIO_BAR void UI_DisplayAudioBar(void) { @@ -231,6 +233,92 @@ void UI_DisplayAudioBar(void) } #endif +#ifdef ENABLE_FEAT_F4HWN_AUDIO_SCOPE + +#define SCOPE_SAMPLES 42 + +static uint16_t g_scope_buf[SCOPE_SAMPLES]; +static uint8_t g_scope_write = 0; +static uint16_t g_scope_floor = 0; // persistent floor: snaps down fast, rises slowly + +void UI_AudioScope_AddSample(void) +{ + // REG_64 (VoiceAmplitudeOut) is only meaningful in TX (mic input). + // FM RX audio is frequency-encoded — no register gives the instantaneous waveform. + if (gCurrentFunction != FUNCTION_TRANSMIT) + return; + + const uint16_t value = BK4819_GetVoiceAmplitudeOut(); + g_scope_buf[g_scope_write] = value; + g_scope_write = (g_scope_write + 1) % SCOPE_SAMPLES; +} + +void UI_DisplayAudioScope(void) +{ + if (gLowBattery && !gLowBatteryConfirmed) + return; + +#ifdef ENABLE_FEAT_F4HWN + RxBlinkLed = 0; + RxBlinkLedCounter = 0; + BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, false); + const unsigned int line = isMainOnly() ? 5 : 3; +#else + const unsigned int line = 3; +#endif + + if (gScreenToDisplay != DISPLAY_MAIN +#ifdef ENABLE_DTMF_CALLING + || gDTMF_CallState != DTMF_CALL_STATE_NONE +#endif + ) + return; + +#if defined(ENABLE_ALARM) || defined(ENABLE_TX1750) + if (gAlarmState != ALARM_STATE_OFF) + return; +#endif + + uint8_t *p_line = gFrameBuffer[line]; + memset(p_line, 0, LCD_WIDTH); + + // Find min and max across current buffer + uint16_t min_val = g_scope_buf[0]; + uint16_t max_val = g_scope_buf[0]; + for (uint8_t i = 1; i < SCOPE_SAMPLES; i++) { + if (g_scope_buf[i] < min_val) min_val = g_scope_buf[i]; + if (g_scope_buf[i] > max_val) max_val = g_scope_buf[i]; + } + + // Floor tracks buffer minimum with asymmetric IIR: + // - drops toward min smoothly (~500ms), avoiding instant-snap ghost + // - rises slowly (+2/frame ≈ +67/s) to handle loud constant voice + if (g_scope_floor > min_val) + g_scope_floor -= ((g_scope_floor - min_val) >> 3) + 1u; + else + g_scope_floor += 2u; + + const uint16_t range = (max_val > g_scope_floor) ? (max_val - g_scope_floor) : 0; + + for (uint8_t i = 0; i < SCOPE_SAMPLES; i++) { + const uint8_t idx = (g_scope_write + i) % SCOPE_SAMPLES; + uint8_t height = 0; + if (range >= 50u) { + const uint16_t v = (g_scope_buf[idx] > g_scope_floor) ? (g_scope_buf[idx] - g_scope_floor) : 0u; + height = (uint8_t)((uint32_t)v * 7u / range); + } + // Filled column using bits 6..0 only (bit 7 always off to avoid overlap with text below) + // At silence (height 0): single pixel at bit 6 (baseline) + const uint8_t mask = (height > 0u) ? (uint8_t)((0x7Fu << (7u - height)) & 0x7Fu) : 0x40u; + // 2px column + 1px gap per sample + p_line[i * 3u] |= mask; + p_line[i * 3u + 1u] |= mask; + } + + ST7565_BlitLine(line); +} +#endif // ENABLE_FEAT_F4HWN_AUDIO_SCOPE + void DisplayRSSIBar(const bool now) { #if defined(ENABLE_RSSI_BAR) @@ -1474,6 +1562,14 @@ void UI_DisplayMain(void) const bool rx = FUNCTION_IsRx(); +#ifdef ENABLE_FEAT_F4HWN_AUDIO_SCOPE + if (gCurrentFunction == FUNCTION_TRANSMIT) { + // Reserve the line so no other element overwrites it. + // Actual drawing is handled exclusively by the app.c timeslice. + center_line = CENTER_LINE_AUDIO_SCOPE; + } + else +#endif #ifdef ENABLE_AUDIO_BAR if (gSetting_mic_bar && gCurrentFunction == FUNCTION_TRANSMIT) { center_line = CENTER_LINE_AUDIO_BAR; diff --git a/App/ui/main.h b/App/ui/main.h index 90fbb9fe..8dd98f47 100644 --- a/App/ui/main.h +++ b/App/ui/main.h @@ -17,10 +17,13 @@ #ifndef UI_MAIN_H #define UI_MAIN_H +#include + enum center_line_t { CENTER_LINE_NONE = 0, CENTER_LINE_IN_USE, CENTER_LINE_AUDIO_BAR, + CENTER_LINE_AUDIO_SCOPE, CENTER_LINE_RSSI, CENTER_LINE_AM_FIX_DATA, CENTER_LINE_DTMF_DEC, @@ -38,6 +41,10 @@ typedef enum center_line_t center_line_t; extern center_line_t center_line; void UI_DisplayAudioBar(void); +#ifdef ENABLE_FEAT_F4HWN_AUDIO_SCOPE +void UI_AudioScope_AddSample(void); +void UI_DisplayAudioScope(void); +#endif void UI_MAIN_TimeSlice500ms(void); void UI_DisplayMain(void); diff --git a/CMakePresets.json b/CMakePresets.json index e82784d7..c19fd039 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -60,6 +60,7 @@ "ENABLE_FEAT_F4HWN_RESCUE_OPS": false, "ENABLE_FEAT_F4HWN_VOL": false, "ENABLE_FEAT_F4HWN_AUDIO": false, + "ENABLE_FEAT_F4HWN_AUDIO_SCOPE": false, "ENABLE_FEAT_F4HWN_RESET_VFO": true, "ENABLE_FEAT_F4HWN_PMR": false, "ENABLE_FEAT_F4HWN_GMRS_FRS_MURS": false, @@ -189,6 +190,7 @@ "ENABLE_FEAT_F4HWN_RESCUE_OPS": true, "ENABLE_FEAT_F4HWN_VOL": true, "ENABLE_FEAT_F4HWN_AUDIO": true, + "ENABLE_FEAT_F4HWN_AUDIO_SCOPE": true, "ENABLE_SWD": true, "EDITION_STRING": "Fusion", "TARGET": "f4hwn.fusion" From 62320f5fbe9842f0b832abce396411f4a68332ba Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Thu, 26 Mar 2026 05:00:43 +0100 Subject: [PATCH 44/50] Update donors --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0cc7f879..bd9c39ed 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Anyway, have fun. # Donations -Special thanks to Jean-Cyrille F6IWW (3 times), Fabrice 14RC123, David F4BPP, Olivier 14RC206, Frédéric F4ESO, Stéphane F5LGW (2 times), Jorge Ornelas (4 times), Laurent F4AXK, Christophe Morel, Clayton W0LED, Pierre Antoine F6FWB, Jean-Claude 14FRS3306, Thierry F4GVO, Eric F1NOU, PricelessToolkit, Ady M6NYJ, Tom McGovern (4 times), Joseph Roth, Pierre-Yves Colin, Frank DJ7FG, Marcel Testaz, Brian Frobisher, Yannick F4JFO, Paolo Bussola, Dirk DL8DF, Levente Szőke (2 times), Bernard-Michel Herrera, Jérôme Saintespes, Paul Davies, RS (3 times), Johan F4WAT, Robert Wörle, Rafael Sundorf, Paul Harker, Peter Fintl, Pascal F4ICR (2 times), Mike DL2MF (3 times), Eric KI1C (2 times), Phil G0ELM, Jérôme Lambert, Eliot Vedel, Alfonso EA7KDF, Jean-François F1EVM, Robert DC1RDB (2 times), Ian KE2CHJ, Daryl VK3AWA, Roberto Brunelli, Robert Boardman, Stephen Oliver, Nicolas F4INE, William Bruno, Daniel OK2VLK, Tayler Chew, Peter DL7RFP, Philippe Kopp, Rune LA6YMA, Jeremy Luna, Steef Wagenaar (2 times), Zhuo BG7SGA, Jamie M0JLB, Antoine LIBERT, Vince K0DKR, Julia DF7JA, Ken 2E0UMK, Victor TI2SYS, Tobi DG9LAY, Deaglan K4DFQ and Catherine PALMER for their [donations](https://www.paypal.com/paypalme/F4HWN). That’s so kind of them. Thanks so much 🙏🏻 +Special thanks to Jean-Cyrille F6IWW (3 times), Fabrice 14RC123, David F4BPP, Olivier 14RC206, Frédéric F4ESO, Stéphane F5LGW (2 times), Jorge Ornelas (4 times), Laurent F4AXK, Christophe Morel, Clayton W0LED, Pierre Antoine F6FWB, Jean-Claude 14FRS3306, Thierry F4GVO, Eric F1NOU, PricelessToolkit, Ady M6NYJ, Tom McGovern (4 times), Joseph Roth, Pierre-Yves Colin, Frank DJ7FG, Marcel Testaz, Brian Frobisher, Yannick F4JFO, Paolo Bussola, Dirk DL8DF, Levente Szőke (2 times), Bernard-Michel Herrera, Jérôme Saintespes, Paul Davies, RS (3 times), Johan F4WAT, Robert Wörle, Rafael Sundorf, Paul Harker, Peter Fintl, Pascal F4ICR (2 times), Mike DL2MF (3 times), Eric KI1C (2 times), Phil G0ELM, Jérôme Lambert, Eliot Vedel, Alfonso EA7KDF, Jean-François F1EVM, Robert DC1RDB (2 times), Ian KE2CHJ, Daryl VK3AWA, Roberto Brunelli, Robert Boardman, Stephen Oliver, Nicolas F4INE, William Bruno, Daniel OK2VLK, Tayler Chew, Peter DL7RFP, Philippe Kopp, Rune LA6YMA, Jeremy Luna, Steef Wagenaar (2 times), Zhuo BG7SGA, Jamie M0JLB, Antoine LIBERT, Vince K0DKR, Julia DF7JA, Ken 2E0UMK, Victor TI2SYS, Tobi DG9LAY, Deaglan K4DFQ, Catherine PALMER and Brian WA6JFK for their [donations](https://www.paypal.com/paypalme/F4HWN). That’s so kind of them. Thanks so much 🙏🏻 ## Table of Contents From 249b0b2bb708953361386768ba899741f73af4a2 Mon Sep 17 00:00:00 2001 From: mrkusypl Date: Thu, 26 Mar 2026 17:47:21 +0100 Subject: [PATCH 45/50] Optimizing the Audio Scope code --- App/app/app.c | 16 ++++++++-------- App/ui/main.c | 24 +++++++++++------------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/App/app/app.c b/App/app/app.c index b5ed912f..f9e0b099 100644 --- a/App/app/app.c +++ b/App/app/app.c @@ -1371,14 +1371,14 @@ void APP_TimeSlice10ms(void) } #ifdef ENABLE_FEAT_F4HWN_AUDIO_SCOPE - // Sample audio amplitude every 30ms - if ((gFlashLightBlinkCounter % 3) == 0) - UI_AudioScope_AddSample(); - // Refresh display every 30ms during TX only (FM RX has no usable audio register) - if ((gFlashLightBlinkCounter % 3) == 0 && - gScreenToDisplay == DISPLAY_MAIN && - gCurrentFunction == FUNCTION_TRANSMIT) - UI_DisplayAudioScope(); + if (gCurrentFunction == FUNCTION_TRANSMIT) { // during TX only (FM RX has no usable audio register) + if (gSetting_mic_bar && (gFlashLightBlinkCounter % (30 / 10)) == 0) { // once every 30ms + // Sample audio amplitude + UI_AudioScope_AddSample(); + // Refresh display + UI_DisplayAudioScope(); + } + } #endif bool gUpdateDisplayCurrent = gUpdateDisplay; diff --git a/App/ui/main.c b/App/ui/main.c index 1aa29248..a6b9a62a 100644 --- a/App/ui/main.c +++ b/App/ui/main.c @@ -235,7 +235,7 @@ void UI_DisplayAudioBar(void) #ifdef ENABLE_FEAT_F4HWN_AUDIO_SCOPE -#define SCOPE_SAMPLES 42 +#define SCOPE_SAMPLES 43 static uint16_t g_scope_buf[SCOPE_SAMPLES]; static uint8_t g_scope_write = 0; @@ -245,8 +245,6 @@ void UI_AudioScope_AddSample(void) { // REG_64 (VoiceAmplitudeOut) is only meaningful in TX (mic input). // FM RX audio is frequency-encoded — no register gives the instantaneous waveform. - if (gCurrentFunction != FUNCTION_TRANSMIT) - return; const uint16_t value = BK4819_GetVoiceAmplitudeOut(); g_scope_buf[g_scope_write] = value; @@ -258,15 +256,6 @@ void UI_DisplayAudioScope(void) if (gLowBattery && !gLowBatteryConfirmed) return; -#ifdef ENABLE_FEAT_F4HWN - RxBlinkLed = 0; - RxBlinkLedCounter = 0; - BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, false); - const unsigned int line = isMainOnly() ? 5 : 3; -#else - const unsigned int line = 3; -#endif - if (gScreenToDisplay != DISPLAY_MAIN #ifdef ENABLE_DTMF_CALLING || gDTMF_CallState != DTMF_CALL_STATE_NONE @@ -279,6 +268,15 @@ void UI_DisplayAudioScope(void) return; #endif +#ifdef ENABLE_FEAT_F4HWN + RxBlinkLed = 0; + RxBlinkLedCounter = 0; + BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, false); + const unsigned int line = isMainOnly() ? 5 : 3; +#else + const unsigned int line = 3; +#endif + uint8_t *p_line = gFrameBuffer[line]; memset(p_line, 0, LCD_WIDTH); @@ -1563,7 +1561,7 @@ void UI_DisplayMain(void) const bool rx = FUNCTION_IsRx(); #ifdef ENABLE_FEAT_F4HWN_AUDIO_SCOPE - if (gCurrentFunction == FUNCTION_TRANSMIT) { + if (gSetting_mic_bar && gCurrentFunction == FUNCTION_TRANSMIT) { // Reserve the line so no other element overwrites it. // Actual drawing is handled exclusively by the app.c timeslice. center_line = CENTER_LINE_AUDIO_SCOPE; From 6fe1bec80c7811f4f932f37e7c85b949f8e13f59 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Fri, 27 Mar 2026 01:50:16 +0100 Subject: [PATCH 46/50] Improve Audio Scope --- App/app/app.c | 2 +- App/ui/main.c | 76 +++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 62 insertions(+), 16 deletions(-) diff --git a/App/app/app.c b/App/app/app.c index f9e0b099..d37fae64 100644 --- a/App/app/app.c +++ b/App/app/app.c @@ -1372,7 +1372,7 @@ void APP_TimeSlice10ms(void) #ifdef ENABLE_FEAT_F4HWN_AUDIO_SCOPE if (gCurrentFunction == FUNCTION_TRANSMIT) { // during TX only (FM RX has no usable audio register) - if (gSetting_mic_bar && (gFlashLightBlinkCounter % (30 / 10)) == 0) { // once every 30ms + if (gSetting_mic_bar && (gFlashLightBlinkCounter % (20 / 10)) == 0) { // once every 20ms // Sample audio amplitude UI_AudioScope_AddSample(); // Refresh display diff --git a/App/ui/main.c b/App/ui/main.c index a6b9a62a..0e24f452 100644 --- a/App/ui/main.c +++ b/App/ui/main.c @@ -235,20 +235,44 @@ void UI_DisplayAudioBar(void) #ifdef ENABLE_FEAT_F4HWN_AUDIO_SCOPE -#define SCOPE_SAMPLES 43 +#define SCOPE_SAMPLES 43 // number of columns (43 × 3px = 128px wide) +#define SCOPE_NOISE_GATE 50u // minimum range below which the display shows baseline +#define SCOPE_FLOOR_RISE 2u // floor rise per frame (+100 units/s at 20ms/frame) +#define SCOPE_FLOOR_DROP_SHR 3u // floor drop IIR shift: drop by (floor-min) >> N per frame (~160ms to halve) static uint16_t g_scope_buf[SCOPE_SAMPLES]; -static uint8_t g_scope_write = 0; -static uint16_t g_scope_floor = 0; // persistent floor: snaps down fast, rises slowly +static uint8_t g_scope_write = 0; +static uint16_t g_scope_floor = 0; // persistent floor: snaps down fast, rises slowly +static uint8_t g_scope_ready = 0; // number of valid samples since TX entry +static bool g_scope_floor_init = false; // floor initialised from real data yet? void UI_AudioScope_AddSample(void) { // REG_64 (VoiceAmplitudeOut) is only meaningful in TX (mic input). // FM RX audio is frequency-encoded — no register gives the instantaneous waveform. - const uint16_t value = BK4819_GetVoiceAmplitudeOut(); - g_scope_buf[g_scope_write] = value; - g_scope_write = (g_scope_write + 1) % SCOPE_SAMPLES; + static bool s_was_tx = false; + + if (gCurrentFunction != FUNCTION_TRANSMIT) { + s_was_tx = false; + return; + } + + if (!s_was_tx) { + // TX entry: full reset so every new transmission starts from a clean state + memset(g_scope_buf, 0, sizeof(g_scope_buf)); + g_scope_write = 0u; + g_scope_floor = 0u; + g_scope_ready = 0u; + g_scope_floor_init = false; + s_was_tx = true; + } + + g_scope_buf[g_scope_write] = BK4819_GetVoiceAmplitudeOut(); + g_scope_write = (g_scope_write + 1u) % SCOPE_SAMPLES; + + if (g_scope_ready < SCOPE_SAMPLES) + g_scope_ready++; } void UI_DisplayAudioScope(void) @@ -256,6 +280,21 @@ void UI_DisplayAudioScope(void) if (gLowBattery && !gLowBatteryConfirmed) return; + // Buffer not yet full: show a dotted baseline while samples warm up + if (g_scope_ready < SCOPE_SAMPLES) { +#ifdef ENABLE_FEAT_F4HWN + const unsigned int line = isMainOnly() ? 5u : 3u; +#else + const unsigned int line = 3u; +#endif + uint8_t *p_line = gFrameBuffer[line]; + memset(p_line, 0, LCD_WIDTH); + for (uint8_t x = 0u; x < LCD_WIDTH; x += 3u) + p_line[x] = 0x40u; // one dot every 3px, aligned with column grid + ST7565_BlitLine(line); + return; + } + if (gScreenToDisplay != DISPLAY_MAIN #ifdef ENABLE_DTMF_CALLING || gDTMF_CallState != DTMF_CALL_STATE_NONE @@ -283,25 +322,32 @@ void UI_DisplayAudioScope(void) // Find min and max across current buffer uint16_t min_val = g_scope_buf[0]; uint16_t max_val = g_scope_buf[0]; - for (uint8_t i = 1; i < SCOPE_SAMPLES; i++) { + for (uint8_t i = 1u; i < SCOPE_SAMPLES; i++) { if (g_scope_buf[i] < min_val) min_val = g_scope_buf[i]; if (g_scope_buf[i] > max_val) max_val = g_scope_buf[i]; } + // First real display frame: seed the floor from the actual buffer minimum + // to avoid full-height bars caused by a floor at 0 far below the voice data. + if (!g_scope_floor_init) { + g_scope_floor = min_val; + g_scope_floor_init = true; + } + // Floor tracks buffer minimum with asymmetric IIR: - // - drops toward min smoothly (~500ms), avoiding instant-snap ghost - // - rises slowly (+2/frame ≈ +67/s) to handle loud constant voice + // - drops toward min smoothly (SCOPE_FLOOR_DROP_SHR), avoiding instant-snap ghost + // - rises slowly (SCOPE_FLOOR_RISE/frame) to handle loud constant voice if (g_scope_floor > min_val) - g_scope_floor -= ((g_scope_floor - min_val) >> 3) + 1u; + g_scope_floor -= ((g_scope_floor - min_val) >> SCOPE_FLOOR_DROP_SHR) + 1u; else - g_scope_floor += 2u; + g_scope_floor += SCOPE_FLOOR_RISE; - const uint16_t range = (max_val > g_scope_floor) ? (max_val - g_scope_floor) : 0; + const uint16_t range = (max_val > g_scope_floor) ? (max_val - g_scope_floor) : 0u; - for (uint8_t i = 0; i < SCOPE_SAMPLES; i++) { + for (uint8_t i = 0u; i < SCOPE_SAMPLES; i++) { const uint8_t idx = (g_scope_write + i) % SCOPE_SAMPLES; - uint8_t height = 0; - if (range >= 50u) { + uint8_t height = 0u; + if (range >= SCOPE_NOISE_GATE) { const uint16_t v = (g_scope_buf[idx] > g_scope_floor) ? (g_scope_buf[idx] - g_scope_floor) : 0u; height = (uint8_t)((uint32_t)v * 7u / range); } From 4588e58af54250f96b6255525116c5d66470475d Mon Sep 17 00:00:00 2001 From: mrkusypl Date: Fri, 27 Mar 2026 17:18:16 +0100 Subject: [PATCH 47/50] An even better improvement to Audio Scope --- App/app/app.c | 11 +++----- App/ui/main.c | 75 ++++++++++++++++++++++++--------------------------- App/ui/main.h | 3 ++- 3 files changed, 40 insertions(+), 49 deletions(-) diff --git a/App/app/app.c b/App/app/app.c index d37fae64..247e2711 100644 --- a/App/app/app.c +++ b/App/app/app.c @@ -1371,14 +1371,9 @@ void APP_TimeSlice10ms(void) } #ifdef ENABLE_FEAT_F4HWN_AUDIO_SCOPE - if (gCurrentFunction == FUNCTION_TRANSMIT) { // during TX only (FM RX has no usable audio register) - if (gSetting_mic_bar && (gFlashLightBlinkCounter % (20 / 10)) == 0) { // once every 20ms - // Sample audio amplitude - UI_AudioScope_AddSample(); - // Refresh display - UI_DisplayAudioScope(); - } - } + if (gSetting_mic_bar && (gFlashLightBlinkCounter % (20 / 10)) == 0) // once every 20ms + // Sample audio amplitude and refresh display during TX only (FM RX has no usable audio register) + UI_DisplayAudioScope(); #endif bool gUpdateDisplayCurrent = gUpdateDisplay; diff --git a/App/ui/main.c b/App/ui/main.c index 0e24f452..38f5aa14 100644 --- a/App/ui/main.c +++ b/App/ui/main.c @@ -239,18 +239,20 @@ void UI_DisplayAudioBar(void) #define SCOPE_NOISE_GATE 50u // minimum range below which the display shows baseline #define SCOPE_FLOOR_RISE 2u // floor rise per frame (+100 units/s at 20ms/frame) #define SCOPE_FLOOR_DROP_SHR 3u // floor drop IIR shift: drop by (floor-min) >> N per frame (~160ms to halve) +#define SCOPE_VOLUME_MIN 200u // let's assume that the sound level in silence is 200 -static uint16_t g_scope_buf[SCOPE_SAMPLES]; -static uint8_t g_scope_write = 0; -static uint16_t g_scope_floor = 0; // persistent floor: snaps down fast, rises slowly -static uint8_t g_scope_ready = 0; // number of valid samples since TX entry -static bool g_scope_floor_init = false; // floor initialised from real data yet? - -void UI_AudioScope_AddSample(void) +void UI_DisplayAudioScope(void) { + static uint16_t g_scope_buf[SCOPE_SAMPLES]; + static uint8_t g_scope_write = 0; + static uint16_t g_scope_floor = SCOPE_VOLUME_MIN; // persistent floor: snaps down fast, rises slowly + static uint8_t g_scope_ready = 0; // number of valid samples since TX entry + // REG_64 (VoiceAmplitudeOut) is only meaningful in TX (mic input). // FM RX audio is frequency-encoded — no register gives the instantaneous waveform. +// ------------------------------ Sample audio amplitude ------------------------------ + static bool s_was_tx = false; if (gCurrentFunction != FUNCTION_TRANSMIT) { @@ -258,43 +260,40 @@ void UI_AudioScope_AddSample(void) return; } + // This prevents a sudden spike on the bar caused by release the PTT button + if (!GPIO_IsPttPressed() +#ifdef ENABLE_VOX + && !gEeprom.VOX_SWITCH +#endif + ) + return; + if (!s_was_tx) { // TX entry: full reset so every new transmission starts from a clean state - memset(g_scope_buf, 0, sizeof(g_scope_buf)); + for (uint8_t i = 0; i < SCOPE_SAMPLES; i++) g_scope_buf[i] = SCOPE_VOLUME_MIN; g_scope_write = 0u; - g_scope_floor = 0u; - g_scope_ready = 0u; - g_scope_floor_init = false; + g_scope_floor = SCOPE_VOLUME_MIN; s_was_tx = true; } g_scope_buf[g_scope_write] = BK4819_GetVoiceAmplitudeOut(); + + // Let’s assume that values below 200 may be incorrect + // to avoid full-height bars that may appear on the first TX + if (g_scope_ready < SCOPE_SAMPLES) { + g_scope_ready++; + + if (g_scope_buf[g_scope_write] < SCOPE_VOLUME_MIN) + g_scope_buf[g_scope_write] = SCOPE_VOLUME_MIN; + } + g_scope_write = (g_scope_write + 1u) % SCOPE_SAMPLES; - if (g_scope_ready < SCOPE_SAMPLES) - g_scope_ready++; -} +// --------------------------------- Refresh display --------------------------------- -void UI_DisplayAudioScope(void) -{ if (gLowBattery && !gLowBatteryConfirmed) return; - // Buffer not yet full: show a dotted baseline while samples warm up - if (g_scope_ready < SCOPE_SAMPLES) { -#ifdef ENABLE_FEAT_F4HWN - const unsigned int line = isMainOnly() ? 5u : 3u; -#else - const unsigned int line = 3u; -#endif - uint8_t *p_line = gFrameBuffer[line]; - memset(p_line, 0, LCD_WIDTH); - for (uint8_t x = 0u; x < LCD_WIDTH; x += 3u) - p_line[x] = 0x40u; // one dot every 3px, aligned with column grid - ST7565_BlitLine(line); - return; - } - if (gScreenToDisplay != DISPLAY_MAIN #ifdef ENABLE_DTMF_CALLING || gDTMF_CallState != DTMF_CALL_STATE_NONE @@ -327,13 +326,6 @@ void UI_DisplayAudioScope(void) if (g_scope_buf[i] > max_val) max_val = g_scope_buf[i]; } - // First real display frame: seed the floor from the actual buffer minimum - // to avoid full-height bars caused by a floor at 0 far below the voice data. - if (!g_scope_floor_init) { - g_scope_floor = min_val; - g_scope_floor_init = true; - } - // Floor tracks buffer minimum with asymmetric IIR: // - drops toward min smoothly (SCOPE_FLOOR_DROP_SHR), avoiding instant-snap ghost // - rises slowly (SCOPE_FLOOR_RISE/frame) to handle loud constant voice @@ -355,8 +347,11 @@ void UI_DisplayAudioScope(void) // At silence (height 0): single pixel at bit 6 (baseline) const uint8_t mask = (height > 0u) ? (uint8_t)((0x7Fu << (7u - height)) & 0x7Fu) : 0x40u; // 2px column + 1px gap per sample - p_line[i * 3u] |= mask; - p_line[i * 3u + 1u] |= mask; + + uint8_t *p_col = &p_line[i * 3u]; + p_col[0] = mask; + p_col[1] = mask; + } ST7565_BlitLine(line); diff --git a/App/ui/main.h b/App/ui/main.h index 8dd98f47..505c5072 100644 --- a/App/ui/main.h +++ b/App/ui/main.h @@ -40,9 +40,10 @@ typedef enum center_line_t center_line_t; extern center_line_t center_line; +#ifdef ENABLE_AUDIO_BAR void UI_DisplayAudioBar(void); +#endif #ifdef ENABLE_FEAT_F4HWN_AUDIO_SCOPE -void UI_AudioScope_AddSample(void); void UI_DisplayAudioScope(void); #endif void UI_MAIN_TimeSlice500ms(void); From 1f577ec82e05e8ee5a77ed8686730266c56f0bf1 Mon Sep 17 00:00:00 2001 From: mrkusypl Date: Sat, 28 Mar 2026 01:41:19 +0100 Subject: [PATCH 48/50] Fix for the Audio Scope in OnePush mode --- App/ui/main.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/App/ui/main.c b/App/ui/main.c index 38f5aa14..33357a39 100644 --- a/App/ui/main.c +++ b/App/ui/main.c @@ -264,6 +264,9 @@ void UI_DisplayAudioScope(void) if (!GPIO_IsPttPressed() #ifdef ENABLE_VOX && !gEeprom.VOX_SWITCH +#endif +#ifdef ENABLE_FEAT_F4HWN + && !gSetting_set_ptt_session #endif ) return; From db46cea0ebce220405965c7f30ff9a722ba44229 Mon Sep 17 00:00:00 2001 From: mrkusypl Date: Sat, 28 Mar 2026 18:15:20 +0100 Subject: [PATCH 49/50] Eliminating high bars in Audio Scope after releasing the DTMF/1750 button --- App/ui/main.c | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/App/ui/main.c b/App/ui/main.c index 33357a39..f30dbf62 100644 --- a/App/ui/main.c +++ b/App/ui/main.c @@ -279,16 +279,17 @@ void UI_DisplayAudioScope(void) s_was_tx = true; } - g_scope_buf[g_scope_write] = BK4819_GetVoiceAmplitudeOut(); - - // Let’s assume that values below 200 may be incorrect - // to avoid full-height bars that may appear on the first TX - if (g_scope_ready < SCOPE_SAMPLES) { + // The first 7 bars after turning on the radio + // will not display any values: they cause high bars. + if (g_scope_ready >= 7) + g_scope_buf[g_scope_write] = BK4819_GetVoiceAmplitudeOut(); + else g_scope_ready++; - if (g_scope_buf[g_scope_write] < SCOPE_VOLUME_MIN) - g_scope_buf[g_scope_write] = SCOPE_VOLUME_MIN; - } + // If the reading is 0, it is definitely an incorrect value + // caused by the microphone being muted - set it to 200. + if (g_scope_buf[g_scope_write] == 0) + g_scope_buf[g_scope_write] = SCOPE_VOLUME_MIN; g_scope_write = (g_scope_write + 1u) % SCOPE_SAMPLES; From 5987ec7b6425de971bd2f0df99f2fb553d0679e6 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Sat, 28 Mar 2026 23:22:00 +0100 Subject: [PATCH 50/50] New version 5.3.0 --- CMakePresets.json | 2 +- archive/f4hwn.fusion.v5.3.0.bin | Bin 0 -> 85384 bytes archive/f4hwn.fusion.v5.3.0.sa818.bin | Bin 0 -> 85384 bytes 3 files changed, 1 insertion(+), 1 deletion(-) create mode 100755 archive/f4hwn.fusion.v5.3.0.bin create mode 100755 archive/f4hwn.fusion.v5.3.0.sa818.bin diff --git a/CMakePresets.json b/CMakePresets.json index c19fd039..b4756d5b 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -71,7 +71,7 @@ "ENABLE_UART_RW_BK_REGS": false, "ENABLE_SWD": false, "VERSION_STRING_1": "v0.22", - "VERSION_STRING_2": "v5.2.0" + "VERSION_STRING_2": "v5.3.0" } }, { diff --git a/archive/f4hwn.fusion.v5.3.0.bin b/archive/f4hwn.fusion.v5.3.0.bin new file mode 100755 index 0000000000000000000000000000000000000000..3c64950424b936e304efe2d8e64f632a279cedf5 GIT binary patch literal 85384 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 B2NwVU literal 0 HcmV?d00001 diff --git a/archive/f4hwn.fusion.v5.3.0.sa818.bin b/archive/f4hwn.fusion.v5.3.0.sa818.bin new file mode 100755 index 0000000000000000000000000000000000000000..473cff7b9a749fe56cd3e5e84815f167050cf13e GIT binary patch literal 85384 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 literal 0 HcmV?d00001