From 0501422c64bb31a69238b7e0cf2acba16ad03bfb Mon Sep 17 00:00:00 2001 From: mrkusypl Date: Tue, 28 Apr 2026 17:45:34 +0200 Subject: [PATCH 01/33] Add long-press functionality to ChName, minor refactoring --- App/app/action.c | 16 ++++------------ App/app/app.c | 3 ++- App/app/menu.c | 47 ++++++++++++++++++++++++----------------------- App/app/scanner.c | 13 +++++-------- 4 files changed, 35 insertions(+), 44 deletions(-) diff --git a/App/app/action.c b/App/app/action.c index 8d88b947..dd59cfaa 100644 --- a/App/app/action.c +++ b/App/app/action.c @@ -343,22 +343,14 @@ void ACTION_Handle(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) break; } - if (!bKeyHeld && bKeyPressed) // button pushed - { + if (bKeyHeld != bKeyPressed) { // button pushed or released after hold + // (!bKeyHeld && bKeyPressed) or (bKeyHeld && !bKeyPressed) return; } - // held or released beyond this point + // held or released after short press - if(!(bKeyHeld && !bKeyPressed)) // don't beep on released after hold - gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL; - - if (bKeyHeld && !bKeyPressed) // button released after hold - { - return; - } - - // held or released after short press beyond this point + gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL; #ifdef ENABLE_FMRADIO if (gFmRadioMode) { // do not run these actions in FM radio mode diff --git a/App/app/app.c b/App/app/app.c index 7cfa3ca3..45243570 100644 --- a/App/app/app.c +++ b/App/app/app.c @@ -2088,7 +2088,8 @@ static void ProcessKey(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) } } #if defined(ENABLE_ALARM) || defined(ENABLE_TX1750) - else if ((!bKeyHeld && bKeyPressed) || (gAlarmState == ALARM_STATE_TX1750 && bKeyHeld && !bKeyPressed)) { + // else if ((!bKeyHeld && bKeyPressed) || (gAlarmState == ALARM_STATE_TX1750 && bKeyHeld && !bKeyPressed)) { + else if ((bKeyHeld != bKeyPressed) && (gAlarmState == ALARM_STATE_TX1750 || bKeyPressed)) { ALARM_Off(); if (gEeprom.REPEATER_TAIL_TONE_ELIMINATION == 0) diff --git a/App/app/menu.c b/App/app/menu.c index 63a33048..6f2f5b36 100644 --- a/App/app/menu.c +++ b/App/app/menu.c @@ -1507,6 +1507,13 @@ static const char* const char_map[10] = { "wxyz9" // KEY_9 }; +static bool MENU_IsEditingName() { + return !gCssBackgroundScan + && UI_MENU_GetCurrentMenuId() == MENU_MEM_NAME + && gIsInSubMenu + && edit_index >= 0; +} + static void MENU_Key_0_to_9(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) { uint8_t Offset; @@ -1708,12 +1715,7 @@ static void MENU_Key_0_to_9(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) static void MENU_Key_EXIT(bool bKeyPressed, bool bKeyHeld) { - const bool editing_name = !gCssBackgroundScan - && UI_MENU_GetCurrentMenuId() == MENU_MEM_NAME - && gIsInSubMenu - && edit_index >= 0; - - if (editing_name) + if (MENU_IsEditingName()) { if (!bKeyPressed) { @@ -1813,9 +1815,9 @@ Skip: static void MENU_Key_MENU(const bool bKeyPressed, const bool bKeyHeld) { - if (bKeyHeld || !bKeyPressed) + if (!bKeyPressed || (bKeyHeld && (!MENU_IsEditingName() || gAskForConfirmation))) return; - + gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL; gRequestDisplayScreen = DISPLAY_MENU; @@ -1884,8 +1886,12 @@ static void MENU_Key_MENU(const bool bKeyPressed, const bool bKeyHeld) { // editing the channel name characters edit_last_key = 255; - if (++edit_index < 10) - return; // next char + if (bKeyHeld) { + edit_index = 10; + } + else if (++edit_index < 10) { + return; + } // exit gFlagAcceptSetting = false; @@ -2013,23 +2019,18 @@ static void MENU_Key_UP_DOWN(bool bKeyPressed, bool bKeyHeld, int8_t Direction) uint16_t Channel; bool bCheckScanList; + if (!bKeyPressed) + return; + + if (!bKeyHeld) { + gInputBoxIndex = 0; + gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL; + } + if (!gEeprom.SET_NAV && gIsInSubMenu) { Direction = -Direction; } - if (!bKeyHeld) - { - if (!bKeyPressed) - return; - - gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL; - - gInputBoxIndex = 0; - } - else - if (!bKeyPressed) - return; - if (UI_MENU_GetCurrentMenuId() == MENU_MEM_NAME && gIsInSubMenu && edit_index >= 0) { // change the character if (edit_index < 10 && Direction != 0) diff --git a/App/app/scanner.c b/App/app/scanner.c index afb806ff..617291a2 100644 --- a/App/app/scanner.c +++ b/App/app/scanner.c @@ -243,16 +243,13 @@ static void SCANNER_Key_STAR(bool bKeyPressed, bool bKeyHeld) return; } -static void SCANNER_Key_UP_DOWN(bool bKeyPressed, bool pKeyHeld, int8_t Direction) +static void SCANNER_Key_UP_DOWN(bool bKeyPressed, bool bKeyHeld, int8_t Direction) { - if (pKeyHeld) { - if (!bKeyPressed) - return; + if (!bKeyPressed) { + return; } - else { - if (!bKeyPressed) - return; + if (!bKeyHeld) { gInputBoxIndex = 0; gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL; } @@ -266,7 +263,7 @@ static void SCANNER_Key_UP_DOWN(bool bKeyPressed, bool pKeyHeld, int8_t Directio gShowChPrefix = RADIO_CheckValidChannel(gScanChannel, false, 0); gRequestDisplayScreen = DISPLAY_SCANNER; } - else + else if (!bKeyHeld) gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL; } From 2c116f9503628e5f87fa88d7d531682317fd90d3 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Wed, 29 Apr 2026 00:35:14 +0200 Subject: [PATCH 02/33] Fix S-meter display during scan reception --- 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 576fa0fa..1b820b6d 100644 --- a/App/ui/main.c +++ b/App/ui/main.c @@ -2037,7 +2037,7 @@ void UI_DisplayMain(void) const bool rx = FUNCTION_IsRx(); #ifdef ENABLE_FEAT_F4HWN_SCAN_PROGRESS - if (gScanStateDir != SCAN_OFF && UI_DrawScanProgress()) + if (!rx && gScanStateDir != SCAN_OFF && UI_DrawScanProgress()) { center_line = CENTER_LINE_SCAN_PROGRESS; } From 037de071eb697ab5e9bb0c611c1ccd602fc63fe2 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Wed, 29 Apr 2026 00:41:17 +0200 Subject: [PATCH 03/33] Clamp displayed RSSI at S9+40 (-53 dBm) ceiling --- App/ui/main.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/App/ui/main.c b/App/ui/main.c index 1b820b6d..d650db40 100644 --- a/App/ui/main.c +++ b/App/ui/main.c @@ -920,6 +920,7 @@ void DisplayRSSIBar(const bool now) overS9dBm = (uint8_t)MIN(rssi_dBm - (-93), 40); overS9Bars = overS9dBm / 10; } + const int16_t display_rssi_dBm = (rssi_dBm > -53) ? -53 : rssi_dBm; #else const int16_t s0_dBm = -gEeprom.S0_LEVEL; // S0 .. base level const int16_t rssi_dBm = @@ -938,12 +939,12 @@ void DisplayRSSIBar(const bool now) #ifdef ENABLE_FEAT_F4HWN if (gSetting_set_gui) { - sprintf(str, "%3d", rssi_dBm); + sprintf(str, "%3d", display_rssi_dBm); UI_PrintStringSmallNormal(str, LCD_WIDTH + 8, 0, line - 1); } else { - sprintf(str, "% 4d %s", rssi_dBm, "dBm"); + sprintf(str, "% 4d %s", display_rssi_dBm, "dBm"); if(isMainOnly()) GUI_DisplaySmallest(str, 2, 41, false, true); else From a46604d8f3fc0d601d75c1f39e62977e3c93f097 Mon Sep 17 00:00:00 2001 From: mrkusypl Date: Wed, 29 Apr 2026 17:06:18 +0200 Subject: [PATCH 04/33] Major code refactoring (412 B) --- App/app/action.c | 2 +- App/app/app.c | 4 +- App/app/breakout.c | 304 +++++++++++++++------------------------------ App/app/breakout.h | 1 - App/app/dtmf.c | 4 + App/app/dtmf.h | 1 + App/app/menu.c | 2 +- App/app/spectrum.c | 17 +-- App/functions.c | 2 +- App/ui/helper.c | 5 + App/ui/helper.h | 1 + App/ui/lock.c | 13 +- App/ui/status.c | 2 +- App/ui/welcome.c | 8 +- 14 files changed, 131 insertions(+), 235 deletions(-) diff --git a/App/app/action.c b/App/app/action.c index dd59cfaa..df33e903 100644 --- a/App/app/action.c +++ b/App/app/action.c @@ -212,7 +212,7 @@ void ACTION_Scan(bool bRestart) DTMF_clear_RX(); #endif gDTMF_RX_live_timeout = 0; - memset(gDTMF_RX_live, 0, sizeof(gDTMF_RX_live)); + DTMF_clear_input_box_memory(); RADIO_SelectVfos(); diff --git a/App/app/app.c b/App/app/app.c index 45243570..555340e3 100644 --- a/App/app/app.c +++ b/App/app/app.c @@ -1560,7 +1560,7 @@ void APP_TimeSlice500ms(void) { if (gDTMF_RX_live[0] != 0) { - memset(gDTMF_RX_live, 0, sizeof(gDTMF_RX_live)); + DTMF_clear_input_box_memory(); gUpdateDisplay = true; } } @@ -1908,7 +1908,7 @@ static void ProcessKey(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) if (Key == KEY_EXIT && bKeyHeld) { // exit key held pressed // clear the live DTMF decoder if (gDTMF_RX_live[0] != 0) { - memset(gDTMF_RX_live, 0, sizeof(gDTMF_RX_live)); + DTMF_clear_input_box_memory(); gDTMF_RX_live_timeout = 0; gUpdateDisplay = true; } diff --git a/App/app/breakout.c b/App/app/breakout.c index cbbf4c3f..560fb090 100644 --- a/App/app/breakout.c +++ b/App/app/breakout.c @@ -25,9 +25,8 @@ static uint8_t blockAnim = 0; static bool isInitialized = false; bool isPaused = false; -bool isBeep = false; -uint8_t levelCountBreackout = 1; +uint8_t levelCountBreakout = 1; uint16_t tone = 0; uint16_t score = 0; @@ -42,115 +41,7 @@ Brick brick[BRICK_NUMBER]; Racket racket; Ball ball; -const uint8_t BITMAP_block[4][15] = -{ - { - 0b00011110, - 0b00110001, - 0b00101001, - 0b00100101, - 0b00100011, - 0b00110001, - 0b00101001, - 0b00100101, - 0b00100011, - 0b00110001, - 0b00101001, - 0b00100101, - 0b00100011, - 0b00110001, - 0b00011110, - }, - { - 0b00011110, - 0b00101001, - 0b00100101, - 0b00100011, - 0b00110001, - 0b00101001, - 0b00100101, - 0b00100011, - 0b00110001, - 0b00101001, - 0b00100101, - 0b00100011, - 0b00110001, - 0b00101001, - 0b00011110, - }, - { - 0b00011110, - 0b00100101, - 0b00100011, - 0b00110001, - 0b00101001, - 0b00100101, - 0b00100011, - 0b00110001, - 0b00101001, - 0b00100101, - 0b00100011, - 0b00110001, - 0b00101001, - 0b00100101, - 0b00011110, - }, - { - 0b00011110, - 0b00100011, - 0b00110001, - 0b00101001, - 0b00100101, - 0b00100011, - 0b00110001, - 0b00101001, - 0b00100101, - 0b00100011, - 0b00110001, - 0b00101001, - 0b00100101, - 0b00100011, - 0b00011110, - } -}; - -const uint8_t BITMAP_blockOn[15] = -{ - 0b00011110, - 0b00111111, - 0b00111111, - 0b00111111, - 0b00111111, - 0b00111111, - 0b00111111, - 0b00111111, - 0b00111111, - 0b00111111, - 0b00111111, - 0b00111111, - 0b00111111, - 0b00111111, - 0b00011110, -}; - -const uint8_t BITMAP_blockEmpty[15] = -{ - 0b00000000, - 0b00000000, - 0b00000000, - 0b00000000, - 0b00000000, - 0b00000000, - 0b00000000, - 0b00000000, - 0b00000000, - 0b00000000, - 0b00000000, - 0b00000000, - 0b00000000, - 0b00000000, - 0b00000000, -}; +static const uint8_t BRICK_ANIM_PATTERNS[4] = {0b00110001, 0b00101001, 0b00100101, 0b00100011}; // Initialise seed void srand_custom(uint32_t seed) { @@ -172,7 +63,7 @@ int randInt(int min, int max) { void reset(void) { ballCount = BALL_NUMBER; - levelCountBreackout = 1; + levelCountBreakout = 1; score = 0; } @@ -191,10 +82,10 @@ void playBeep(uint16_t tone) void drawScore() { // Clean status line - memset(gStatusLine, 0, sizeof(gStatusLine)); + UI_StatusClear(); // Level - sprintf(str, "Level %02u", levelCountBreackout); + sprintf(str, "Level %02u", levelCountBreakout); GUI_DisplaySmallest(str, 0, 1, true, true); // Ball @@ -206,6 +97,12 @@ void drawScore() GUI_DisplaySmallest(str, 88, 1, true, true); } +// Render the ball +void renderBall(bool state) { + UI_DrawRectangleBuffer(gFrameBuffer, ball.x, ball.y, ball.x + ball.w - 1, ball.y + ball.h - 1, state); + UI_DrawLineBuffer(gFrameBuffer, ball.x - 1, ball.y + 1, ball.x + ball.w, ball.y + 1, state); +} + // Init ball void initBall() { ball.x = 62; @@ -215,21 +112,25 @@ void initBall() { ball.dx = 0; ball.dy = 1; - UI_DrawRectangleBuffer(gFrameBuffer, ball.x, ball.y, ball.x + ball.w -1, ball.y + ball.h - 1, true); - UI_DrawLineBuffer(gFrameBuffer, ball.x - 1, ball.y + 1, ball.x + ball.w, ball.y + 1, true); + renderBall(true); +} + +// Calculate the direction of the bounced ball +void directionBall(int16_t x, uint8_t w, int8_t num) { + ball.dx = map(x + w - ball.x, 0, w, num, -num); + ball.dy *= -1; } // Draw ball void drawBall() { - UI_DrawRectangleBuffer(gFrameBuffer, ball.x, ball.y, ball.x + ball.w -1, ball.y + ball.h -1, false); - UI_DrawLineBuffer(gFrameBuffer, ball.x - 1, ball.y + 1, ball.x + ball.w, ball.y + 1, false); + renderBall(false); ball.x += ball.dx; ball.y += ball.dy; if (ball.y <= 0) // Up { - ball.dx = map(randInt(0, 7), 0, 7, 3, -3); + ball.dx = randInt(-3, 3); ball.dy = 1; } else if (ball.x <= 2) // Left @@ -243,9 +144,7 @@ void drawBall() { // And now Down... if (ball.y == 47) { if (ball.x + 1 >= racket.x && ball.x - 1 <= racket.x + racket.w) { - ball.dx = map(racket.x + racket.w - ball.x, 0, racket.w, 3, -3); - ball.dy *= -1; - isBeep = true; + directionBall(racket.x, racket.w, 3); tone = 400; } } @@ -253,7 +152,6 @@ void drawBall() { ballCount--; UI_DisplayClear(); drawScore(); - isBeep = true; tone = 800; if (ballCount < 0) { reset(); @@ -261,15 +159,13 @@ void drawBall() { drawWall(); isPaused = true; - UI_PrintStringSmallBold("GAME", 32, 0, 4); - UI_PrintStringSmallBold("OVER", 66, 0, 4); + UI_PrintStringSmallBold("GAME OVER", 0, LCD_WIDTH - 1, 4); } initRacket(); initBall(); } - UI_DrawRectangleBuffer(gFrameBuffer, ball.x, ball.y, ball.x + ball.w -1, ball.y + ball.h - 1, true); - UI_DrawLineBuffer(gFrameBuffer, ball.x - 1, ball.y + 1, ball.x + ball.w, ball.y + 1, true); + renderBall(true); } // Init wall @@ -289,7 +185,6 @@ void initWall() { brick[i].y = -8 + 8 * k; brick[i].w = 14; brick[i].h = 5; - brick[i].s = randInt(0, 3); brick[i].destroy = false; j++; @@ -302,6 +197,11 @@ void drawWall() { for (i = 0; i < BRICK_NUMBER; i++) { if (brick[i].destroy == false) { + uint8_t *fb_ptr = gFrameBuffer[brick[i].y / 8] + brick[i].x; + + fb_ptr[0] = 0b00011110; + fb_ptr[14] = 0b00011110; + if ((ball.x + 1 >= brick[i].x && ball.x - 1 <= brick[i].x + brick[i].w) && ((ball.y + 1 >= brick[i].y && @@ -309,32 +209,37 @@ void drawWall() { brick[i].destroy = true; score++; - ball.dx = map(brick[i].x + brick[i].w - ball.x, 0, brick[i].w, 2, -2); - ball.dy *= -1; + directionBall(brick[i].x, brick[i].w, 2); BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, true); - memcpy(gFrameBuffer[brick[i].y / 8] + brick[i].x, BITMAP_blockOn, sizeof(BITMAP_blockOn)); + memset(fb_ptr + 1, 0b00111111, 13); ST7565_BlitLine(brick[i].y / 8); playBeep(600); - memcpy(gFrameBuffer[brick[i].y / 8] + brick[i].x, BITMAP_blockEmpty, sizeof(BITMAP_blockEmpty)); + memset(fb_ptr + 0, 0b00000000, 15); ST7565_BlitLine(brick[i].y / 8); BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, false); if (score % BRICK_NUMBER == 0) { - levelCountBreackout++; + levelCountBreakout++; ballCount++; initWall(); return; } - } - - if (brick[i].destroy == false) { - memcpy(gFrameBuffer[brick[i].y / 8] + brick[i].x, BITMAP_block[(brick[i].s + blockAnim) % 4], sizeof(BITMAP_block[(brick[i].s + blockAnim) % 4])); + } else { + for(uint8_t k = 0; k < 13; k++) { + fb_ptr[k + 1] = BRICK_ANIM_PATTERNS[(blockAnim + k) % 4]; + } } } } } +// Render the racket shape +void renderRacket(int x, bool state) { + UI_DrawRectangleBuffer(gFrameBuffer, x + 1, racket.y, x + racket.w - 2, racket.y + racket.h, state); + UI_DrawLineBuffer(gFrameBuffer, x, racket.y + 1, x + racket.w - 1, racket.y + 1, state); +} + // Init racket void initRacket() { racket.w = 24; @@ -343,18 +248,15 @@ void initRacket() { racket.h = 2; racket.p = racket.x; - UI_DrawRectangleBuffer(gFrameBuffer, racket.x + 1, racket.y, racket.x + racket.w - 2, racket.y + racket.h, true); - UI_DrawLineBuffer(gFrameBuffer, racket.x, racket.y + 1, racket.x + racket.w - 1, racket.y + 1, true); + renderRacket(racket.x, true); } // Draw racket void drawRacket() { if (racket.p != racket.x) { - UI_DrawRectangleBuffer(gFrameBuffer, racket.p + 1, racket.y, racket.p + racket.w - 2, racket.y + racket.h, false); - UI_DrawLineBuffer(gFrameBuffer, racket.p, racket.y + 1, racket.p + racket.w - 1, racket.y + 1, false); + renderRacket(racket.p, false); racket.p = racket.x; - UI_DrawRectangleBuffer(gFrameBuffer, racket.x + 1, racket.y, racket.x + racket.w - 2, racket.y + racket.h, true); - UI_DrawLineBuffer(gFrameBuffer, racket.x, racket.y + 1, racket.x + racket.w - 1, racket.y + 1, true); + renderRacket(racket.x, true); } } @@ -382,7 +284,7 @@ static void OnKeyDown(uint8_t key) kbd.counter = 0; if(isPaused) { - UI_PrintStringSmallBold("PAUSE", 0, 128, 4); + UI_PrintStringSmallBold("PAUSE", 0, LCD_WIDTH - 1, 4); } break; case KEY_EXIT: @@ -416,17 +318,13 @@ static bool HandleUserInput() 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) - { - OnKeyDown(kbd.current); - return true; - } - - // Action keys (MENU, EXIT): dispatch only on rising edge - if (kbd.current != kbd.prev) - { + if ( + // Movement keys: dispatch on every tick while held + (kbd.current == KEY_UP || kbd.current == KEY_DOWN || + kbd.current == KEY_4 || kbd.current == KEY_0) || + // Action keys (MENU, EXIT): dispatch only on rising edge + (kbd.current != kbd.prev) + ){ OnKeyDown(kbd.current); } @@ -441,62 +339,62 @@ static void Tick() // APP_RunBreakout void APP_RunBreakout(void) { - static uint8_t swap = 0; + static uint8_t swap = 0; - // Init seed - srand_custom(BK4819_ReadRegister(BK4819_REG_67) & 0x01FF * gBatteryVoltageAverage * gEeprom.VfoInfo[0].pRX->Frequency); + // Init seed + srand_custom(BK4819_ReadRegister(BK4819_REG_67) & 0x01FF * gBatteryVoltageAverage * gEeprom.VfoInfo[0].pRX->Frequency); - // Init led - BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, false); + // Init led + BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, false); - // Finish the brightness fade if it is in progress - BACKLIGHT_UpdateTickless(); + // Finish the brightness fade if it is in progress + BACKLIGHT_UpdateTickless(); - // Init game - UI_DisplayClear(); - reset(); - initWall(); - initRacket(); - initBall(); - memset(gStatusLine, 0, sizeof(gStatusLine)); - isInitialized = true; + // Init game + UI_DisplayClear(); + reset(); + initWall(); + initRacket(); + initBall(); + UI_StatusClear(); + isInitialized = true; - while(isInitialized) + while(isInitialized) + { + Tick(); + if(!isPaused) { - Tick(); - if(!isPaused) + if(swap == 0) { - if(swap == 0) - { - blockAnim = (blockAnim + 1) % 4; - } - - swap = (swap + 1) % 4; - - drawScore(); - drawWall(); - drawRacket(); - drawBall(); - - if(isBeep) - { - playBeep(tone); - isBeep = false; - } - else - { - SYSTEM_DelayMs(40 - MIN(levelCountBreackout - 1, 20)); // Add more fun... - } + blockAnim = (blockAnim + 1) % 4; } + + swap = (swap + 1) % 4; - ST7565_BlitStatusLine(); // Blank status line - ST7565_BlitFullScreen(); - - #ifdef ENABLE_FEAT_F4HWN_SCREENSHOT - if(isPaused || swap == 0) - { - SCREENSHOT_Update(false); - } - #endif + drawScore(); + drawWall(); + drawRacket(); + drawBall(); + + if(tone != 0) + { + playBeep(tone); + tone = 0; + } + else + { + SYSTEM_DelayMs(40 - MIN(levelCountBreakout - 1, 20)); // Add more fun... + } } + + 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 diff --git a/App/app/breakout.h b/App/app/breakout.h index afea94c1..3f71b194 100644 --- a/App/app/breakout.h +++ b/App/app/breakout.h @@ -49,7 +49,6 @@ typedef struct { uint8_t y; // y uint8_t w; // width uint8_t h; // height - uint8_t s; // style bool destroy; // active, if true, check this button, else bypass } Brick; diff --git a/App/app/dtmf.c b/App/app/dtmf.c index 70d5fe6f..bfa88ea9 100644 --- a/App/app/dtmf.c +++ b/App/app/dtmf.c @@ -212,6 +212,10 @@ DTMF_CallMode_t DTMF_CheckGroupCall(const char *pMsg, const unsigned int size) } #endif +void DTMF_clear_input_box_memory() { + memset(gDTMF_RX_live, 0, sizeof(gDTMF_RX_live)); +} + void DTMF_clear_input_box(void) { memset(gDTMF_InputBox, 0, sizeof(gDTMF_InputBox)); diff --git a/App/app/dtmf.h b/App/app/dtmf.h index 0e513838..dba3235e 100644 --- a/App/app/dtmf.h +++ b/App/app/dtmf.h @@ -82,6 +82,7 @@ extern DTMF_ReplyState_t gDTMF_ReplyState; bool DTMF_ValidateCodes(char *pCode, const unsigned int size); char DTMF_GetCharacter(const unsigned int code); +void DTMF_clear_input_box_memory(void); void DTMF_clear_input_box(void); void DTMF_Append(const char code); void DTMF_Reply(void); diff --git a/App/app/menu.c b/App/app/menu.c index 6f2f5b36..4de5b74d 100644 --- a/App/app/menu.c +++ b/App/app/menu.c @@ -800,7 +800,7 @@ void MENU_AcceptSetting(void) case MENU_D_LIVE_DEC: gSetting_live_DTMF_decoder = gSubMenuSelection; gDTMF_RX_live_timeout = 0; - memset(gDTMF_RX_live, 0, sizeof(gDTMF_RX_live)); + DTMF_clear_input_box_memory(); if (!gSetting_live_DTMF_decoder) BK4819_DisableDTMF(); gFlagReconfigureVfos = true; diff --git a/App/app/spectrum.c b/App/app/spectrum.c index bc150502..2bc82673 100644 --- a/App/app/spectrum.c +++ b/App/app/spectrum.c @@ -1574,6 +1574,11 @@ static void ShowChannelName(uint32_t f) static uint32_t channelF = 0; static char channelName[12]; + // Channel name starts at x=43 (fixed), leaving room for the dBm + // string on the left (max ~40 px) and battery indicator at x=116. + // Clear first so a shorter name doesn't leave stale pixels. + memset(&gStatusLine[43], 0, 116 - 43); + if (isListening) { if (f != channelF) { @@ -1593,19 +1598,9 @@ static void ShowChannelName(uint32_t f) } } if (channelName[0] != 0) { - // Channel name starts at x=43 (fixed), leaving room for the dBm - // string on the left (max ~40 px) and battery indicator at x=116. - // Clear first so a shorter name doesn't leave stale pixels. - memset(&gStatusLine[43], 0, 116 - 43); UI_PrintStringSmallBufferNormal(channelName, gStatusLine + 43); - } else { - memset(&gStatusLine[43], 0, 116 - 43); } } - else - { - memset(&gStatusLine[43], 0, 116 - 43); - } ST7565_BlitStatusLine(); } @@ -1948,7 +1943,7 @@ static void RenderFreqInput() { UI_PrintString(freqInputString, 2, 127, 0, 8); } static void RenderStatus() { - memset(gStatusLine, 0, sizeof(gStatusLine)); + UI_StatusClear(); DrawStatus(); ST7565_BlitStatusLine(); } diff --git a/App/functions.c b/App/functions.c index e68e6723..f4794f91 100644 --- a/App/functions.c +++ b/App/functions.c @@ -151,7 +151,7 @@ void FUNCTION_Transmit() // clear the DTMF RX live decoder buffer gDTMF_RX_live_timeout = 0; - memset(gDTMF_RX_live, 0, sizeof(gDTMF_RX_live)); + DTMF_clear_input_box_memory(); #if defined(ENABLE_FMRADIO) if (gFmRadioMode) diff --git a/App/ui/helper.c b/App/ui/helper.c index a0edd501..885ab769 100644 --- a/App/ui/helper.c +++ b/App/ui/helper.c @@ -406,3 +406,8 @@ void UI_DisplayClear() { memset(gFrameBuffer, 0, sizeof(gFrameBuffer)); } + +void UI_StatusClear() +{ + memset(gStatusLine, 0, sizeof(gStatusLine)); +} diff --git a/App/ui/helper.h b/App/ui/helper.h index 0002f2cb..c50653d0 100644 --- a/App/ui/helper.h +++ b/App/ui/helper.h @@ -45,5 +45,6 @@ void UI_DrawLineBuffer(uint8_t (*buffer)[128], int16_t x1, int16_t y1, int16_t x void UI_DrawRectangleBuffer(uint8_t (*buffer)[128], int16_t x1, int16_t y1, int16_t x2, int16_t y2, bool black); void UI_DisplayClear(); +void UI_StatusClear(); #endif diff --git a/App/ui/lock.c b/App/ui/lock.c index c787b236..20f3189b 100644 --- a/App/ui/lock.c +++ b/App/ui/lock.c @@ -34,7 +34,7 @@ static void Render(void) unsigned int i; char String[7]; - memset(gStatusLine, 0, sizeof(gStatusLine)); + UI_StatusClear(); UI_DisplayClear(); UI_PrintString("PASSWORD", 0, 127, 1, 10); @@ -80,16 +80,7 @@ void UI_DisplayLock(void) switch (Key) { - case KEY_0: - case KEY_1: - case KEY_2: - case KEY_3: - case KEY_4: - case KEY_5: - case KEY_6: - case KEY_7: - case KEY_8: - case KEY_9: + case KEY_0...KEY_9: INPUTBOX_Append(Key - KEY_0); if (gInputBoxIndex < 6) // 6 frequency digits diff --git a/App/ui/status.c b/App/ui/status.c index ca146871..a9518d8c 100644 --- a/App/ui/status.c +++ b/App/ui/status.c @@ -59,7 +59,7 @@ void UI_DisplayStatus() char str[8] = ""; gUpdateStatus = false; - memset(gStatusLine, 0, sizeof(gStatusLine)); + UI_StatusClear(); uint8_t *line = gStatusLine; unsigned int x = 0; diff --git a/App/ui/welcome.c b/App/ui/welcome.c index 3188484c..69520590 100644 --- a/App/ui/welcome.c +++ b/App/ui/welcome.c @@ -189,7 +189,7 @@ void UI_DrawQRCode(bool wiki, uint8_t origin_x, uint8_t origin_y) void UI_DisplayReleaseKeys(void) { - memset(gStatusLine, 0, sizeof(gStatusLine)); + UI_StatusClear(); #if defined(ENABLE_FEAT_F4HWN_CTR) || defined(ENABLE_FEAT_F4HWN_INV) ST7565_ContrastAndInv(); #endif @@ -209,7 +209,7 @@ void UI_DisplayWelcome(void) char WelcomeString2[16]; char WelcomeString3[32]; - memset(gStatusLine, 0, sizeof(gStatusLine)); + UI_StatusClear(); #if defined(ENABLE_FEAT_F4HWN_CTR) || defined(ENABLE_FEAT_F4HWN_INV) ST7565_ContrastAndInv(); @@ -222,11 +222,13 @@ void UI_DisplayWelcome(void) if (gEeprom.POWER_ON_DISPLAY_MODE == POWER_ON_DISPLAY_MODE_NONE || gEeprom.POWER_ON_DISPLAY_MODE == POWER_ON_DISPLAY_MODE_SOUND) { ST7565_FillScreen(0x00); + } #else if (gEeprom.POWER_ON_DISPLAY_MODE == POWER_ON_DISPLAY_MODE_NONE || gEeprom.POWER_ON_DISPLAY_MODE == POWER_ON_DISPLAY_MODE_FULL_SCREEN) { ST7565_FillScreen(0xFF); + } #endif - } else { + else { memset(WelcomeString0, 0, sizeof(WelcomeString0)); memset(WelcomeString1, 0, sizeof(WelcomeString1)); From c1f8e945c9425851e2754f937d746ebb1f0b91ef Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Wed, 29 Apr 2026 19:27:41 +0200 Subject: [PATCH 05/33] Show homologated DCS index in Rx/Tx DCS menus --- App/dcs.c | 34 ++++++++++++++++++++++++++++++++++ App/dcs.h | 1 + App/ui/menu.c | 15 ++++++++++++++- 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/App/dcs.c b/App/dcs.c index a54f48b2..f7177bdf 100644 --- a/App/dcs.c +++ b/App/dcs.c @@ -48,6 +48,13 @@ const uint16_t DCS_Options[104] = { 0x01C3, 0x01CA, 0x01D3, 0x01D9, 0x01DA, 0x01DC, 0x01E3, 0x01EC, }; +// Indices in DCS_Options for the 21 non-PMR446 DCS codes. +static const uint8_t DCS_ExtraIdx[21] = { + 5, 9, 19, 25, 34, 36, 41, 43, 44, 48, + 50, 54, 56, 60, 71, 72, 73, 74, 75, 82, + 83 +}; + static uint32_t DCS_CalculateGolay(uint32_t CodeWord) { unsigned int i; @@ -141,3 +148,30 @@ uint8_t DCS_GetCtcssApprovedIndex(uint8_t Option) return 0xFF; } + +uint8_t DCS_GetDcsApprovedIndex(uint8_t Option) +{ + unsigned int i; + unsigned int extra_pos = 0; + uint8_t approved_index = 0; + + if (Option >= ARRAY_SIZE(DCS_Options)) + return 0xFF; + + for (i = 0; i < ARRAY_SIZE(DCS_Options); i++) { + const bool is_extra = extra_pos < ARRAY_SIZE(DCS_ExtraIdx) && + i == DCS_ExtraIdx[extra_pos]; + + if (is_extra) { + extra_pos++; + continue; + } + + if (i == Option) + return approved_index; + + approved_index++; + } + + return 0xFF; +} diff --git a/App/dcs.h b/App/dcs.h index c05ecff2..73fa1159 100644 --- a/App/dcs.h +++ b/App/dcs.h @@ -41,5 +41,6 @@ uint32_t DCS_GetGolayCodeWord(DCS_CodeType_t CodeType, uint8_t Option); uint8_t DCS_GetCdcssCode(uint32_t Code); uint8_t DCS_GetCtcssCode(int Code); uint8_t DCS_GetCtcssApprovedIndex(uint8_t Option); +uint8_t DCS_GetDcsApprovedIndex(uint8_t Option); #endif diff --git a/App/ui/menu.c b/App/ui/menu.c index dbf076c8..a343a153 100644 --- a/App/ui/menu.c +++ b/App/ui/menu.c @@ -1499,9 +1499,22 @@ void UI_DisplayMenu(void) if (gSubMenuSelection == 0) sprintf(top_right_badge, "00/00"); else if (approved_index != 0xFF) - sprintf(top_right_badge, "%02u/%02u", (unsigned)gSubMenuSelection, approved_index + 1); + sprintf(top_right_badge, "%02u/%02u", (unsigned)gSubMenuSelection, (unsigned)approved_index + 1); else sprintf(top_right_badge, "%02u/--", (unsigned)gSubMenuSelection); + } else if (UI_MENU_GetCurrentMenuId() == MENU_R_DCS || + UI_MENU_GetCurrentMenuId() == MENU_T_DCS) { + const bool is_normal_dcs = + gSubMenuSelection > 0 && gSubMenuSelection <= ARRAY_SIZE(DCS_Options); + const uint8_t approved_index = + is_normal_dcs ? DCS_GetDcsApprovedIndex(gSubMenuSelection - 1) : 0xFF; + + if (gSubMenuSelection == 0) + sprintf(top_right_badge, "000/000"); + else if (approved_index != 0xFF) + sprintf(top_right_badge, "%03u/%03u", (unsigned)gSubMenuSelection, (unsigned)approved_index + 1); + else + sprintf(top_right_badge, "%03u/---", (unsigned)gSubMenuSelection); } else { sprintf(top_right_badge, "%03d", gSubMenuSelection); } From b83e4c249193880914907264a9f6370488ceeb3b Mon Sep 17 00:00:00 2001 From: mrkusypl Date: Wed, 29 Apr 2026 20:54:00 +0200 Subject: [PATCH 06/33] Show DCS codes from 39 to 121; Code refactoring (16 B) --- App/dcs.c | 40 ++++++++----------------- App/ui/menu.c | 83 +++++++++++++++++++++++++-------------------------- 2 files changed, 52 insertions(+), 71 deletions(-) diff --git a/App/dcs.c b/App/dcs.c index f7177bdf..24f8b522 100644 --- a/App/dcs.c +++ b/App/dcs.c @@ -14,6 +14,7 @@ * limitations under the License. */ +#include #include "dcs.h" #include "misc.h" @@ -122,18 +123,18 @@ uint8_t DCS_GetCtcssCode(int Code) return Result; } -uint8_t DCS_GetCtcssApprovedIndex(uint8_t Option) +static uint8_t DCS_GetApprovedIndex(uint8_t Option, size_t options_size, size_t extraidx_size, const uint8_t *extraidx) { unsigned int i; unsigned int extra_pos = 0; uint8_t approved_index = 0; - if (Option >= ARRAY_SIZE(CTCSS_Options)) + if (Option >= options_size) return 0xFF; - for (i = 0; i < ARRAY_SIZE(CTCSS_Options); i++) { - const bool is_extra = extra_pos < ARRAY_SIZE(CTCSS_ExtraIdx) && - i == CTCSS_ExtraIdx[extra_pos]; + for (i = 0; i < options_size; i++) { + const bool is_extra = extra_pos < extraidx_size && + i == extraidx[extra_pos]; if (is_extra) { extra_pos++; @@ -149,29 +150,12 @@ uint8_t DCS_GetCtcssApprovedIndex(uint8_t Option) return 0xFF; } +uint8_t DCS_GetCtcssApprovedIndex(uint8_t Option) +{ + return DCS_GetApprovedIndex(Option, ARRAY_SIZE(CTCSS_Options), ARRAY_SIZE(CTCSS_ExtraIdx), CTCSS_ExtraIdx); +} + uint8_t DCS_GetDcsApprovedIndex(uint8_t Option) { - unsigned int i; - unsigned int extra_pos = 0; - uint8_t approved_index = 0; - - if (Option >= ARRAY_SIZE(DCS_Options)) - return 0xFF; - - for (i = 0; i < ARRAY_SIZE(DCS_Options); i++) { - const bool is_extra = extra_pos < ARRAY_SIZE(DCS_ExtraIdx) && - i == DCS_ExtraIdx[extra_pos]; - - if (is_extra) { - extra_pos++; - continue; - } - - if (i == Option) - return approved_index; - - approved_index++; - } - - return 0xFF; + return DCS_GetApprovedIndex(Option, ARRAY_SIZE(DCS_Options), ARRAY_SIZE(DCS_ExtraIdx), DCS_ExtraIdx); } diff --git a/App/ui/menu.c b/App/ui/menu.c index a343a153..583a1c6f 100644 --- a/App/ui/menu.c +++ b/App/ui/menu.c @@ -593,6 +593,8 @@ void UI_DisplayMenu(void) char String[64]; // bigger cuz we can now do multi-line in one string (use '\n' char) char top_right_badge[16]; + const int m = UI_MENU_GetCurrentMenuId(); + #ifdef ENABLE_DTMF_CALLING char Contact[16]; #endif @@ -706,7 +708,7 @@ void UI_DisplayMenu(void) uint8_t gaugeMax = 0; //#endif - switch (UI_MENU_GetCurrentMenuId()) + switch (m) { case MENU_SQL: sprintf(String, "%d", gSubMenuSelection); @@ -1059,7 +1061,7 @@ void UI_DisplayMenu(void) case MENU_S_LIST: if (gSubMenuSelection == MR_CHANNELS_LIST + 1) strcpy(String, "ALL"); - else if (gSubMenuSelection == 0 && UI_MENU_GetCurrentMenuId() == MENU_LIST_CH) + else if (gSubMenuSelection == 0 && m == MENU_LIST_CH) strcpy(String, "OFF"); else { const char *name = gListName[gSubMenuSelection - 1]; @@ -1466,16 +1468,16 @@ void UI_DisplayMenu(void) } } - 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) { } - if ((UI_MENU_GetCurrentMenuId() == MENU_R_CTCS || UI_MENU_GetCurrentMenuId() == MENU_R_DCS) && gCssBackgroundScan) + if ((m == MENU_R_CTCS || m == MENU_R_DCS) && gCssBackgroundScan) UI_PrintString("SCAN", menu_item_x1, menu_item_x2, 4, 8); #ifdef ENABLE_DTMF_CALLING - if (UI_MENU_GetCurrentMenuId() == MENU_D_LIST && gIsDtmfContactValid) { + if (m == MENU_D_LIST && gIsDtmfContactValid) { Contact[11] = 0; memcpy(&gDTMF_ID, Contact + 8, 4); sprintf(String, "ID:%4s", gDTMF_ID); @@ -1483,51 +1485,46 @@ void UI_DisplayMenu(void) } #endif - if (UI_MENU_GetCurrentMenuId() == MENU_R_CTCS || - UI_MENU_GetCurrentMenuId() == MENU_T_CTCS || - UI_MENU_GetCurrentMenuId() == MENU_R_DCS || - UI_MENU_GetCurrentMenuId() == MENU_T_DCS -#ifdef ENABLE_DTMF_CALLING - || UI_MENU_GetCurrentMenuId() == MENU_D_LIST -#endif - ) { - if (UI_MENU_GetCurrentMenuId() == MENU_R_CTCS || - UI_MENU_GetCurrentMenuId() == MENU_T_CTCS) { - const uint8_t approved_index = - (gSubMenuSelection > 0) ? DCS_GetCtcssApprovedIndex(gSubMenuSelection - 1) : 0xFF; + if (m == MENU_R_CTCS || + m == MENU_T_CTCS) { + const uint8_t approved_index = + (gSubMenuSelection > 0) ? DCS_GetCtcssApprovedIndex(gSubMenuSelection - 1) : 0xFF; - if (gSubMenuSelection == 0) - sprintf(top_right_badge, "00/00"); - else if (approved_index != 0xFF) - sprintf(top_right_badge, "%02u/%02u", (unsigned)gSubMenuSelection, (unsigned)approved_index + 1); - else - sprintf(top_right_badge, "%02u/--", (unsigned)gSubMenuSelection); - } else if (UI_MENU_GetCurrentMenuId() == MENU_R_DCS || - UI_MENU_GetCurrentMenuId() == MENU_T_DCS) { - const bool is_normal_dcs = - gSubMenuSelection > 0 && gSubMenuSelection <= ARRAY_SIZE(DCS_Options); - const uint8_t approved_index = - is_normal_dcs ? DCS_GetDcsApprovedIndex(gSubMenuSelection - 1) : 0xFF; - - if (gSubMenuSelection == 0) - sprintf(top_right_badge, "000/000"); - else if (approved_index != 0xFF) - sprintf(top_right_badge, "%03u/%03u", (unsigned)gSubMenuSelection, (unsigned)approved_index + 1); - else - sprintf(top_right_badge, "%03u/---", (unsigned)gSubMenuSelection); - } else { - sprintf(top_right_badge, "%03d", gSubMenuSelection); - } + if (gSubMenuSelection == 0) + sprintf(top_right_badge, "00/00"); + else if (approved_index != 0xFF) + sprintf(top_right_badge, "%02u/%02u", (unsigned)gSubMenuSelection, (unsigned)approved_index + 1); + else + sprintf(top_right_badge, "%02u/--", (unsigned)gSubMenuSelection); } + + if (m == MENU_R_DCS || + m == MENU_T_DCS) { + const uint8_t approved_index = + (gSubMenuSelection > 0) ? DCS_GetDcsApprovedIndex(gSubMenuSelection - 1) : 0xFF; + + if (gSubMenuSelection == 0) + sprintf(top_right_badge, "000/000"); + else if (approved_index != 0xFF) + sprintf(top_right_badge, "%03u/%03u", (unsigned)gSubMenuSelection, (unsigned)approved_index + 39); + else + sprintf(top_right_badge, "%03u/---", (unsigned)gSubMenuSelection); + } + +#ifdef ENABLE_DTMF_CALLING + if (m == MENU_D_LIST) { + sprintf(top_right_badge, "%03d", gSubMenuSelection); + } +#endif if (top_right_badge[0] != '\0') { UI_MENU_DrawTopRightRoundedBadge(top_right_badge, 1, true, menu_item_x1, menu_item_x2); } - if ((UI_MENU_GetCurrentMenuId() == MENU_RESET || - UI_MENU_GetCurrentMenuId() == MENU_MEM_CH || - UI_MENU_GetCurrentMenuId() == MENU_MEM_NAME || - UI_MENU_GetCurrentMenuId() == MENU_DEL_CH) && gAskForConfirmation) + if ((m == MENU_RESET || + m == MENU_MEM_CH || + m == MENU_MEM_NAME || + m == MENU_DEL_CH) && gAskForConfirmation) { // display confirmation char *pPrintStr = (gAskForConfirmation == 1) ? "SURE?" : "WAIT!"; UI_PrintString(pPrintStr, menu_item_x1, menu_item_x2, 5, 8); From 1ee871cc793a6cb045686810b2b2210be838f5e7 Mon Sep 17 00:00:00 2001 From: mrkusypl Date: Thu, 30 Apr 2026 00:01:50 +0200 Subject: [PATCH 07/33] Revert "Show DCS codes from 39 to 121" --- 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 583a1c6f..83599f08 100644 --- a/App/ui/menu.c +++ b/App/ui/menu.c @@ -1506,7 +1506,7 @@ void UI_DisplayMenu(void) if (gSubMenuSelection == 0) sprintf(top_right_badge, "000/000"); else if (approved_index != 0xFF) - sprintf(top_right_badge, "%03u/%03u", (unsigned)gSubMenuSelection, (unsigned)approved_index + 39); + sprintf(top_right_badge, "%03u/%03u", (unsigned)gSubMenuSelection, (unsigned)approved_index + 1); else sprintf(top_right_badge, "%03u/---", (unsigned)gSubMenuSelection); } From c6e7ac33e2e0f9d6a7c8c6c670a90a125a86a164 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Thu, 30 Apr 2026 00:48:27 +0200 Subject: [PATCH 08/33] Show homologated DCS index on 2 digits (saves 8B) --- App/ui/menu.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/App/ui/menu.c b/App/ui/menu.c index 83599f08..8fe7c088 100644 --- a/App/ui/menu.c +++ b/App/ui/menu.c @@ -1504,11 +1504,11 @@ void UI_DisplayMenu(void) (gSubMenuSelection > 0) ? DCS_GetDcsApprovedIndex(gSubMenuSelection - 1) : 0xFF; if (gSubMenuSelection == 0) - sprintf(top_right_badge, "000/000"); + sprintf(top_right_badge, "000/00"); else if (approved_index != 0xFF) - sprintf(top_right_badge, "%03u/%03u", (unsigned)gSubMenuSelection, (unsigned)approved_index + 1); + sprintf(top_right_badge, "%03u/%02u", (unsigned)gSubMenuSelection, (unsigned)approved_index + 1); else - sprintf(top_right_badge, "%03u/---", (unsigned)gSubMenuSelection); + sprintf(top_right_badge, "%03u/--", (unsigned)gSubMenuSelection); } #ifdef ENABLE_DTMF_CALLING From 915083522d44a2541ca35af0ae0d5c842852ab26 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Thu, 30 Apr 2026 23:12:52 +0200 Subject: [PATCH 09/33] Save additional spectrum analyzer parameters --- App/app/spectrum.c | 44 +++++++++++++++++++++++++++++--------- App/driver/eeprom_compat.c | 5 +++-- App/settings.c | 4 ---- 3 files changed, 37 insertions(+), 16 deletions(-) diff --git a/App/app/spectrum.c b/App/app/spectrum.c index 2bc82673..f758bf01 100644 --- a/App/app/spectrum.c +++ b/App/app/spectrum.c @@ -209,32 +209,56 @@ uint16_t statuslineUpdateTimer = 0; #ifdef ENABLE_FEAT_F4HWN_SPECTRUM static void LoadSettings() { - uint8_t Data[8] = {0}; - PY25Q16_ReadBuffer(0x00A158, Data, sizeof(Data)); + uint8_t Data[8] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + PY25Q16_ReadBuffer(0x00A148, Data, sizeof(Data)); - settings.scanStepIndex = ((Data[3] & 0xF0) >> 4); + // Data[0]: scanStepIndex (7:4), stepsCount (3:2), listenBw (1:0) + settings.scanStepIndex = (Data[0] >> 4) & 0x0F; if (settings.scanStepIndex > 14) settings.scanStepIndex = S_STEP_25_0kHz; - settings.stepsCount = ((Data[3] & 0x0F) & 0b1100) >> 2; + settings.stepsCount = (Data[0] >> 2) & 0x03; if (settings.stepsCount > 3) settings.stepsCount = STEPS_64; - settings.listenBw = ((Data[3] & 0x0F) & 0b0011); + settings.listenBw = Data[0] & 0x03; if (settings.listenBw > 2) settings.listenBw = BK4819_FILTER_BW_WIDE; + // Data[1]: manualSetFlag (0), autoSensitivity (2:1) + manualSetFlag = Data[1] & 0x01; + autoSensitivity = (Data[1] >> 1) & 0x03; + if (autoSensitivity >= AUTO_SENS_N_ELEM) + autoSensitivity = AUTO_SENS_NORMAL; + + // Data[2]: dbMax encoded as (dbMax + 130) / 5 + if (Data[2] <= 28) + settings.dbMax = (int)Data[2] * 5 - 130; + + // Data[3]: rssiTriggerLevel as uint8_t (0xFF = auto) + settings.rssiTriggerLevel = (Data[3] == 0xFF) ? RSSI_MAX_VALUE : Data[3]; + + // Data[4] ~ Data[7] are free (for the moment...) } static void SaveSettings() { uint8_t Data[8] = {0}; - PY25Q16_ReadBuffer(0x00A158, Data, sizeof(Data)); + PY25Q16_ReadBuffer(0x00A148, Data, sizeof(Data)); - Data[3] = (settings.scanStepIndex << 4) | (settings.stepsCount << 2) | settings.listenBw; + // Data[0]: scanStepIndex (7:4), stepsCount (3:2), listenBw (1:0) + Data[0] = (settings.scanStepIndex << 4) | (settings.stepsCount << 2) | settings.listenBw; + // Data[1]: manualSetFlag (0), autoSensitivity (2:1) + Data[1] = (manualSetFlag & 0x01) | ((autoSensitivity & 0x03) << 1); - PY25Q16_WriteBuffer(0x00A158, Data, sizeof(Data), false); + // Data[2]: dbMax encoded as (dbMax + 130) / 5 + Data[2] = (uint8_t)((settings.dbMax + 130) / 5); + + // Data[3]: rssiTriggerLevel as uint8_t (0xFF = auto) + Data[3] = (settings.rssiTriggerLevel == RSSI_MAX_VALUE) ? 0xFF : (uint8_t)settings.rssiTriggerLevel; + + PY25Q16_WriteBuffer(0x00A148, Data, sizeof(Data), false); } #endif @@ -2565,8 +2589,8 @@ void APP_RunSpectrum() // Reset dynamic spectrum state on every entry. // Persisted settings are step/count/listenBW only; trigger and dB window // are runtime values and should not carry over between sessions. - manualSetFlag = false; - settings.rssiTriggerLevel = RSSI_MAX_VALUE; + // manualSetFlag = false; + // settings.rssiTriggerLevel = RSSI_MAX_VALUE; RearmRuntimeState(); diff --git a/App/driver/eeprom_compat.c b/App/driver/eeprom_compat.c index 59b5e874..39dde9aa 100644 --- a/App/driver/eeprom_compat.c +++ b/App/driver/eeprom_compat.c @@ -59,8 +59,9 @@ static const AddrMapping_t ADDR_MAPPINGS[] = { // MR FM * 128 Bytes (0x003000) 0x00A028 -> 0x00A0A8 // Settings * 80 Bytes (0x007000) 0x00A0A8 -> 0x00A0F8 // Settings * 56 Bytes (0x008000) 0x00A0F8 -> 0x00A130 - // Settings Scanlist * 8 Bytes (0x009000) 0x00A130 -> 0x00A140 - // Settings AES * 16 Bytes (0x00A000) 0x00A140 -> 0x00A150 + // Settings Scanlist * 8 Bytes (0x009000) 0x00A130 -> 0x00A138 + // Settings AES * 16 Bytes (0x00A000) 0x00A138 -> 0x00A148 + // Settings Spectrum * 8 Bytes 0x00A148 -> 0x00A150 // Settings * 8 Bytes (0x00B000) 0x00A150 -> 0x00A158 // Settings F4HWN * 8 Bytes (0x00C000) 0x00A158 -> 0x00A160 // Settings Version * 16 Bytes 0x00A160 -> 0x00A170 diff --git a/App/settings.c b/App/settings.c index ec2abeef..66eb4688 100644 --- a/App/settings.c +++ b/App/settings.c @@ -495,10 +495,6 @@ gEeprom.FreqChannel[1] = IS_FREQ_CHANNEL(Data16[5]) ? Data16[5] : (FREQ_CHANNE gSetting_set_off = (Data[4] >> 1) > 120 ? 60 : (Data[4] >> 1); #endif - // Warning - // Be aware, Data[3] is use by Spectrum - // Warning - // And set special session settings for actions gSetting_set_ptt_session = gSetting_set_ptt; gEeprom.KEY_LOCK_PTT = gSetting_set_lck; From e0154de186b3c6acfe9db6c741bf39fdff58e3ce Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Fri, 1 May 2026 00:04:24 +0200 Subject: [PATCH 10/33] Fix stuck squelch on quick PTT by seeding listenPrevRssi with peak.rssi at listen entry --- App/app/spectrum.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/App/app/spectrum.c b/App/app/spectrum.c index f758bf01..b2e13f72 100644 --- a/App/app/spectrum.c +++ b/App/app/spectrum.c @@ -642,7 +642,11 @@ static void ToggleRX(bool on) if (on) { listenLowCount = 0; - listenPrevRssi = RSSI_MAX_VALUE; + // Seed with the RSSI that opened the squelch so the very first measure + // can already detect an abrupt drop (quick-PTT case where the operator + // released before listen was actually engaged). + // listenPrevRssi = RSSI_MAX_VALUE; // previous behavior + listenPrevRssi = peak.rssi; #ifdef ENABLE_FEAT_F4HWN_SPECTRUM listenT = 25; BK4819_WriteRegister(0x43, listenBWRegValues[settings.listenBw]); From d5a29aa241437760f595de705e78c8b20025e6fb Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Fri, 1 May 2026 00:26:08 +0200 Subject: [PATCH 11/33] Show scan range bandwidth instead of unused frequencyChangeStep in scan-range mode --- App/app/spectrum.c | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/App/app/spectrum.c b/App/app/spectrum.c index b2e13f72..6b44bfb0 100644 --- a/App/app/spectrum.c +++ b/App/app/spectrum.c @@ -1683,8 +1683,20 @@ static void DrawNums() FormatFrequency(GetFStart(), String); GUI_DisplaySmallest(String, 0, 49, false, true); - sprintf(String, "\x7F%u.%02uk", settings.frequencyChangeStep / 100, - settings.frequencyChangeStep % 100); +#ifdef ENABLE_SCAN_RANGES + if (gScanRangeStart) + { + // Scan-range mode: UP/DOWN are blocked, frequencyChangeStep is unused. + // Show the visible bandwidth instead, which is meaningful here. + uint32_t bw = gScanRangeStop - gScanRangeStart; + sprintf(String, "%u.%02uk", bw / 100, bw % 100); + } + else +#endif + { + sprintf(String, "\x7F%u.%02uk", settings.frequencyChangeStep / 100, + settings.frequencyChangeStep % 100); + } GUI_DisplaySmallest(String, 48, 49, false, true); FormatFrequency(GetFEnd(), String); From 6ae360d0296d70cab9c3a7ae73afc516d85775b8 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Fri, 1 May 2026 18:35:14 +0200 Subject: [PATCH 12/33] Add faster scanning (range & memory) behind ENABLE_FEAT_F4HWN_SCAN_FASTER flag --- App/CMakeLists.txt | 1 + App/app/chFrScanner.c | 337 +++++++++++++++++++++++++++++++++++++++++- App/scheduler.c | 31 ++++ CMakePresets.json | 4 +- 4 files changed, 368 insertions(+), 5 deletions(-) diff --git a/App/CMakeLists.txt b/App/CMakeLists.txt index 441ec36a..66a5dd0e 100644 --- a/App/CMakeLists.txt +++ b/App/CMakeLists.txt @@ -196,6 +196,7 @@ enable_feature(ENABLE_FEAT_F4HWN_NARROWER) enable_feature(ENABLE_FEAT_F4HWN_INV) enable_feature(ENABLE_FEAT_F4HWN_CTR) enable_feature(ENABLE_FEAT_F4HWN_SCAN_PROGRESS) +enable_feature(ENABLE_FEAT_F4HWN_SCAN_FASTER) enable_feature(ENABLE_FEAT_F4HWN_RESCUE_OPS) enable_feature(ENABLE_FEAT_F4HWN_VOL) enable_feature(ENABLE_FEAT_F4HWN_AUDIO) diff --git a/App/app/chFrScanner.c b/App/app/chFrScanner.c index b64e0dd1..cc9204a0 100644 --- a/App/app/chFrScanner.c +++ b/App/app/chFrScanner.c @@ -1,6 +1,9 @@ #include "app/app.h" #include "app/chFrScanner.h" +#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER +#include "driver/systick.h" +#endif #include "functions.h" #include "misc.h" #include "settings.h" @@ -37,10 +40,260 @@ uint8_t initialCROSS_BAND_RX_TX; static void NextFreqChannel(void); static void NextMemChannel(void); +#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER +#define SCAN_FAST_PRECHECK_STEPS 6 +#define SCAN_FAST_RSSI_MARGIN 16 +#define SCAN_FAST_SQUELCH_MARGIN 8 +#define SCAN_FAST_FINE_STEP_LIMIT 250 +#define SCAN_FAST_FINE_REFINE_SPAN 1000 +#define SCAN_FAST_FINE_REFINE_MAX 80 +#define SCAN_FAST_FINE_RSSI_DROP 8 +#define SCAN_FAST_RSSI_MAX 65535u +// Settle loop guard: at most this many 1us waits while the BK4819 glitch +// indicator stays above SCAN_FAST_GLITCH_THRESHOLD. Caps the worst-case +// settling time per step at ~50us before we read the RSSI anyway. +#define SCAN_FAST_GLITCH_GUARD_MAX 50 +#define SCAN_FAST_GLITCH_THRESHOLD 200 +// HF/VHF boundary in Hz: BK4819_PickRXFilterPathBasedOnFrequency() switches +// the front-end filter path here, so we only re-run that (relatively +// expensive) call when we actually cross the boundary. +#define SCAN_FAST_HF_VHF_BOUNDARY_HZ 28000000u + +typedef enum { + SCAN_FAST_DISABLED, + SCAN_FAST_QUIET_BATCH, + SCAN_FAST_CANDIDATE +} scan_fast_result_t; + +static uint16_t scanFastReg30; +static uint16_t scanFastNoiseFloor = SCAN_FAST_RSSI_MAX; +static uint32_t scanFastPrevFrequency; +static bool scanFastLastFullTuneCandidate; + +static void ScanFastResetState(void) +{ + // Called on every scan (re)start, after a reception, and on each + // wraparound to the start of the channel list / range. The noise + // floor is re-warmed up from current conditions instead of carrying + // stale calibration into a later sweep. + scanFastNoiseFloor = SCAN_FAST_RSSI_MAX; + scanFastPrevFrequency = 0; + scanFastLastFullTuneCandidate = false; +} + +static uint16_t ScanFastReadRssi(void) +{ + uint8_t guard = SCAN_FAST_GLITCH_GUARD_MAX; + while (guard-- && BK4819_GetGlitchIndicator() >= SCAN_FAST_GLITCH_THRESHOLD) + { + SYSTICK_DelayUs(1); + } + + // Discard first read: after fast tuning the RSSI/AGC value may still be stale. + BK4819_GetRSSI(); + return BK4819_GetRSSI(); +} + +static void ScanFastTune(uint32_t frequency) +{ + if (scanFastPrevFrequency == 0 || + ((frequency < SCAN_FAST_HF_VHF_BOUNDARY_HZ) != + (scanFastPrevFrequency < SCAN_FAST_HF_VHF_BOUNDARY_HZ))) + { + BK4819_PickRXFilterPathBasedOnFrequency(frequency); + } + + scanFastPrevFrequency = frequency; + BK4819_SetFrequency(frequency); + BK4819_WriteRegister(BK4819_REG_30, 0); + BK4819_WriteRegister(BK4819_REG_30, scanFastReg30); +} + +static bool ScanFastIsCandidate(uint16_t rssi) +{ + if (scanFastNoiseFloor == SCAN_FAST_RSSI_MAX) + { + scanFastNoiseFloor = rssi; + return false; + } + + const uint16_t noiseTrigger = + (scanFastNoiseFloor > SCAN_FAST_RSSI_MAX - SCAN_FAST_RSSI_MARGIN) + ? SCAN_FAST_RSSI_MAX + : (uint16_t)(scanFastNoiseFloor + SCAN_FAST_RSSI_MARGIN); + const uint16_t squelchTrigger = + (gRxVfo->SquelchOpenRSSIThresh > SCAN_FAST_SQUELCH_MARGIN) + ? (uint16_t)(gRxVfo->SquelchOpenRSSIThresh - SCAN_FAST_SQUELCH_MARGIN) + : 0; + + if (rssi >= noiseTrigger && rssi >= squelchTrigger) + return true; + + if (rssi < scanFastNoiseFloor) + scanFastNoiseFloor = rssi; + else + scanFastNoiseFloor = (uint16_t)((7u * scanFastNoiseFloor + rssi + 4u) >> 3); + + return false; +} +#endif + +#if defined(ENABLE_FEAT_F4HWN_SCAN_FASTER) && defined(ENABLE_SCAN_RANGES) +static void ScanRangeFastRefineCandidate(uint16_t firstRssi) +{ + const uint16_t step = gRxVfo->StepFrequency; + if (step == 0 || step >= SCAN_FAST_FINE_STEP_LIMIT) + return; + + uint16_t maxSteps = SCAN_FAST_FINE_REFINE_SPAN / step; + if (maxSteps == 0) + maxSteps = 1; + if (maxSteps > SCAN_FAST_FINE_REFINE_MAX) + maxSteps = SCAN_FAST_FINE_REFINE_MAX; + + uint16_t bestRssi = firstRssi; + uint32_t bestFrequency = gRxVfo->freq_config_RX.Frequency; + uint8_t fallingSteps = 0; + + for (uint16_t i = 0; i < maxSteps; ++i) + { + const uint32_t prevRxFrequency = gRxVfo->pRX->Frequency; + + gRxVfo->freq_config_RX.Frequency = + APP_SetFreqByStepAndLimits(gRxVfo, gScanStateDir, gScanRangeStart, gScanRangeStop); + RADIO_ApplyOffset(gRxVfo); + + const uint32_t freq = gRxVfo->pRX->Frequency; + if ((gScanStateDir > 0 && freq < prevRxFrequency) || + (gScanStateDir < 0 && freq > prevRxFrequency)) + { + break; + } + + ScanFastTune(freq); + + const uint16_t rssi = ScanFastReadRssi(); + if (rssi > bestRssi) + { + bestRssi = rssi; + bestFrequency = gRxVfo->freq_config_RX.Frequency; + fallingSteps = 0; + } + else if (bestRssi > rssi && bestRssi - rssi >= SCAN_FAST_FINE_RSSI_DROP) + { + if (++fallingSteps >= 3) + break; + } + } + + gRxVfo->freq_config_RX.Frequency = bestFrequency; + RADIO_ApplyOffset(gRxVfo); + ScanFastTune(gRxVfo->pRX->Frequency); +} + +static scan_fast_result_t ScanRangeFastPrecheck(void) +{ + if (!gScanRangeStart) + return SCAN_FAST_DISABLED; + + if (gRxVfo->SquelchOpenRSSIThresh == 0) + return SCAN_FAST_DISABLED; + + // Mute AF DAC during the precheck sweep: avoids audio glitches and + // saves a few uA on each silent step. The bit is restored on the real + // tune by RADIO_SetupRegisters() in NextFreqChannel(). + scanFastReg30 = BK4819_ReadRegister(BK4819_REG_30) & ~BK4819_REG_30_MASK_ENABLE_AF_DAC; + + for (uint8_t i = 0; i < SCAN_FAST_PRECHECK_STEPS; ++i) + { + gRxVfo->freq_config_RX.Frequency = + APP_SetFreqByStepAndLimits(gRxVfo, gScanStateDir, gScanRangeStart, gScanRangeStop); + RADIO_ApplyOffset(gRxVfo); + + const uint32_t freq = gRxVfo->pRX->Frequency; + + // Detect wraparound: scanning forward but the new freq is lower + // than the previous one (or scanning backward but it's higher) + // means the range has wrapped from stop back to start. Reset the + // noise floor so the new pass re-warms up from current conditions. + if (scanFastPrevFrequency != 0 && + ((gScanStateDir > 0 && freq < scanFastPrevFrequency) || + (gScanStateDir < 0 && freq > scanFastPrevFrequency))) + { + ScanFastResetState(); + } + + ScanFastTune(freq); + + const uint16_t rssi = ScanFastReadRssi(); + if (ScanFastIsCandidate(rssi)) + { + ScanRangeFastRefineCandidate(rssi); + return SCAN_FAST_CANDIDATE; + } + } + + return SCAN_FAST_QUIET_BATCH; +} +#endif + +#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER +static bool MemChannelFastPrecheck(uint16_t channel) +{ + if (gRxVfo->SquelchOpenRSSIThresh == 0) + { + scanFastLastFullTuneCandidate = false; + return true; + } + + const uint32_t frequency = SETTINGS_FetchChannelFrequency(channel); + if (frequency == 0 || frequency == 0xFFFFFFFF) + { + scanFastLastFullTuneCandidate = false; + return true; + } + + // Mute AF DAC for the same reason as in ScanRangeFastPrecheck(). + scanFastReg30 = BK4819_ReadRegister(BK4819_REG_30) & ~BK4819_REG_30_MASK_ENABLE_AF_DAC; + ScanFastTune(frequency); + + if (ScanFastIsCandidate(ScanFastReadRssi())) + { + scanFastLastFullTuneCandidate = true; + return true; // signal detected: let the full tune path follow + } + + // No signal here: still mirror the probed frequency in the VFO so the + // status line (channel name + frequency) keeps in sync as we skip. + // RADIO_ConfigureChannel() will overwrite these values cleanly when a + // candidate is eventually retained. + scanFastLastFullTuneCandidate = false; + gRxVfo->freq_config_RX.Frequency = frequency; + return false; +} + +static void AdvanceMemScanList(const bool enabled) +{ + if (enabled) + if (++currentScanList >= SCAN_NEXT_NUM) + currentScanList = SCAN_NEXT_CHAN_SCANLIST1; +} + +static void SetMemScanProgressChannel(uint16_t channel) +{ + gEeprom.MrChannel[ gEeprom.RX_VFO] = channel; + gEeprom.ScreenChannel[gEeprom.RX_VFO] = channel; + gRxVfo->CHANNEL_SAVE = channel; +} +#endif + #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; +#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER + ScanFastResetState(); +#endif if(gScanRangeStart > gScanRangeStop) SWAP(gScanRangeStart, gScanRangeStop); } @@ -59,6 +312,9 @@ void CHFRSCANNER_Start(const bool storeBackupSettings, const int8_t scan_directi gNextMrChannel = gRxVfo->CHANNEL_SAVE; currentScanList = SCAN_NEXT_CHAN_SCANLIST1; gScanStateDir = scan_direction; +#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER + ScanFastResetState(); +#endif if (IS_MR_CHANNEL(gNextMrChannel)) { @@ -119,6 +375,15 @@ void CHFRSCANNER_ContinueScanning(void) void CHFRSCANNER_ContinueScanning(void) { +#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER + if (scanFastLastFullTuneCandidate && + gCurrentFunction != FUNCTION_INCOMING && + !g_SquelchLost) + { + ScanFastResetState(); + } +#endif + if (gCurrentFunction == FUNCTION_INCOMING && (IS_FREQ_CHANNEL(gNextMrChannel) || gCurrentCodeType == CODE_TYPE_OFF)) { @@ -136,6 +401,15 @@ void CHFRSCANNER_ContinueScanning(void) void CHFRSCANNER_Found(void) { +#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER + // After a real reception the BK4819 AGC has shifted, biasing the next + // few RSSI readings high. Reset the precheck state so it warms up from + // the current noise floor instead of carrying stale calibration into + // the post-reception scan, which otherwise turns nearly every channel + // into a CANDIDATE and erases the speed gain. + ScanFastResetState(); +#endif + if (gEeprom.SCAN_RESUME_MODE > 80) { if (!gScanPauseMode) { gScanPauseDelayIn_10ms = scan_pause_delay_in_5_10ms * (gEeprom.SCAN_RESUME_MODE - 80) * 5; @@ -243,11 +517,38 @@ static void NextFreqChannel(void) { #ifdef ENABLE_SCAN_RANGES if(gScanRangeStart) { +#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER + const scan_fast_result_t fastResult = ScanRangeFastPrecheck(); + + if (fastResult == SCAN_FAST_QUIET_BATCH) + { + scanFastLastFullTuneCandidate = false; + gScanPauseDelayIn_10ms = 1; + gUpdateDisplay = true; + return; + } + + if (fastResult == SCAN_FAST_DISABLED) + { + scanFastLastFullTuneCandidate = false; + gRxVfo->freq_config_RX.Frequency = APP_SetFreqByStepAndLimits(gRxVfo, gScanStateDir, gScanRangeStart, gScanRangeStop); + } + else + { + scanFastLastFullTuneCandidate = true; + } +#else gRxVfo->freq_config_RX.Frequency = APP_SetFreqByStepAndLimits(gRxVfo, gScanStateDir, gScanRangeStart, gScanRangeStop); +#endif } else +#endif + { +#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER + scanFastLastFullTuneCandidate = false; #endif gRxVfo->freq_config_RX.Frequency = APP_SetFrequencyByStep(gRxVfo, gScanStateDir); + } RADIO_ApplyOffset(gRxVfo); RADIO_ConfigureSquelchAndOutputPower(gRxVfo); @@ -348,23 +649,47 @@ static void NextMemChannel(void) } if (!enabled || chan == 0xFFFF) - { + { chan = RADIO_FindNextChannel(gNextMrChannel + gScanStateDir, gScanStateDir, true, gEeprom.SCAN_LIST_DEFAULT); if (chan == 0xFFFF) - { // no valid channel found + { // no valid channel found -> wrapping back to the first channel chan = MR_CHANNEL_FIRST; +#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER + // Wraparound: re-warm the precheck noise floor on the new pass + // so it tracks current RF conditions instead of an EMA that + // accumulated drift over the previous full sweep. + ScanFastResetState(); +#endif } - + gNextMrChannel = chan; //sprintf(str, "----> Chan %d\n", chan + 1); //LogUart(str); } - if (gNextMrChannel != prev_chan) +#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER + SetMemScanProgressChannel(gNextMrChannel); + + if (!MemChannelFastPrecheck(gNextMrChannel)) { + gScanPauseDelayIn_10ms = 1; + gUpdateDisplay = true; + AdvanceMemScanList(enabled); + return; + } +#endif + + if (gNextMrChannel != prev_chan +#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER + || scanFastLastFullTuneCandidate +#endif + ) + { +#ifndef ENABLE_FEAT_F4HWN_SCAN_FASTER gEeprom.MrChannel[ gEeprom.RX_VFO] = gNextMrChannel; gEeprom.ScreenChannel[gEeprom.RX_VFO] = gNextMrChannel; +#endif RADIO_ConfigureChannel(gEeprom.RX_VFO, VFO_CONFIGURE_RELOAD); RADIO_SetupRegisters(true); @@ -378,7 +703,11 @@ static void NextMemChannel(void) gScanPauseDelayIn_10ms = scan_pause_delay_in_3_10ms; #endif +#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER + AdvanceMemScanList(enabled); +#else if (enabled) if (++currentScanList >= SCAN_NEXT_NUM) currentScanList = SCAN_NEXT_CHAN_SCANLIST1; // back round we go +#endif } diff --git a/App/scheduler.c b/App/scheduler.c index 3d12fb22..c43e05f9 100644 --- a/App/scheduler.c +++ b/App/scheduler.c @@ -98,6 +98,37 @@ void SysTick_Handler(void) if (gCurrentFunction != FUNCTION_MONITOR && gCurrentFunction != FUNCTION_TRANSMIT) DECREMENT_AND_TRIGGER(gScanPauseDelayIn_10ms, gScheduleScanListen); +#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER + // Scan stall watchdog: if the scan-resume countdown has expired + // (gScanPauseDelayIn_10ms == 0) but no resume was actually scheduled + // (gScheduleScanListen == false) and no real reception is happening + // (squelch closed -> green LED off, not in RX/TX/MONITOR), the scan + // loop is stuck. Force a resume after this many 10ms ticks. The + // gScanPauseDelayIn_10ms == 0 guard ensures we never override the + // user-configured ScnRev (SCAN_RESUME_MODE) pause: while that pause + // is decrementing, the watchdog stays asleep. + #define SCAN_FAST_STALL_WATCHDOG_10ms 25 // 250 ms + static uint16_t scanFastStallCounter; + + if (gScanStateDir != SCAN_OFF + && gScanPauseDelayIn_10ms == 0 + && !gScheduleScanListen + && !g_SquelchLost + && gCurrentFunction != FUNCTION_RECEIVE + && gCurrentFunction != FUNCTION_TRANSMIT + && gCurrentFunction != FUNCTION_MONITOR) + { + if (++scanFastStallCounter >= SCAN_FAST_STALL_WATCHDOG_10ms) { + scanFastStallCounter = 0; + gScheduleScanListen = true; + } + } + else + { + scanFastStallCounter = 0; + } +#endif + DECREMENT_AND_TRIGGER(gTailNoteEliminationCountdown_10ms, gFlagTailNoteEliminationComplete); #ifdef ENABLE_VOICE diff --git a/CMakePresets.json b/CMakePresets.json index 7f3e04e2..1966b83a 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -69,6 +69,7 @@ "ENABLE_FEAT_F4HWN_QRCODE": false, "ENABLE_FEAT_F4HWN_MEM": false, "ENABLE_FEAT_F4HWN_SCAN_PROGRESS": false, + "ENABLE_FEAT_F4HWN_SCAN_FASTER": false, "ENABLE_AGC_SHOW_DATA": false, "ENABLE_UART_RW_BK_REGS": false, "ENABLE_SWD": false, @@ -194,6 +195,7 @@ "ENABLE_FEAT_F4HWN_AUDIO": true, "ENABLE_FEAT_F4HWN_AUDIO_SCOPE": true, "ENABLE_FEAT_F4HWN_SCAN_PROGRESS": true, + "ENABLE_FEAT_F4HWN_SCAN_FASTER": true, "ENABLE_FEAT_F4HWN_QRCODE": true, "ENABLE_FEAT_F4HWN_MEM": true, "ENABLE_SWD": true, @@ -232,4 +234,4 @@ "configurePreset": "Fusion" } ] -} \ No newline at end of file +} From 673d1b57ab1237fb3a87c9c7a598a057ea190666 Mon Sep 17 00:00:00 2001 From: mrkusypl Date: Fri, 1 May 2026 19:39:59 +0200 Subject: [PATCH 13/33] Refactoring of beep handling (94 B) --- App/app/app.c | 2 +- App/app/breakout.c | 6 +- App/audio.c | 124 ++++++++++------------------------------- App/audio.h | 9 ++- App/driver/backlight.c | 4 +- App/driver/bk4819.h | 2 + App/driver/bk4829.c | 34 +++++------ 7 files changed, 60 insertions(+), 121 deletions(-) diff --git a/App/app/app.c b/App/app/app.c index 555340e3..043f23b8 100644 --- a/App/app/app.c +++ b/App/app/app.c @@ -994,7 +994,7 @@ void APP_Update(void) APP_EndTransmission(); - AUDIO_PlayBeep(BEEP_880HZ_60MS_DOUBLE_BEEP); + AUDIO_PlayBeep(BEEP_880HZ_60MS_TRIPLE_BEEP); RADIO_SetVfoState(VFO_STATE_TIMEOUT); diff --git a/App/app/breakout.c b/App/app/breakout.c index 560fb090..6b626ea6 100644 --- a/App/app/breakout.c +++ b/App/app/breakout.c @@ -70,11 +70,9 @@ void reset(void) // PlayBeep void playBeep(uint16_t tone) { - BK4819_PlayTone(tone, true); // 500 Hz ON AUDIO_AudioPathOn(); - BK4819_ExitTxMute(); - SYSTEM_DelayMs(100); - BK4819_EnterTxMute(); + BK4819_PrepareToPlayTone(true); + BK4819_PlayToneRaw(tone, 100); AUDIO_AudioPathOff(); } diff --git a/App/audio.c b/App/audio.c index 2a61b6d3..a1249d64 100644 --- a/App/audio.c +++ b/App/audio.c @@ -33,31 +33,34 @@ #include "settings.h" #include "ui/ui.h" +static const uint16_t BEEP_Classic_array[][3] = { +// Tone Duration Repeats + {0, 0, 0 }, // BEEP_NONE + {1000, 60, 1 }, // BEEP_1KHZ_60MS_OPTIONAL + {500, 60, 2 }, // BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL +#ifdef ENABLE_DTMF_CALLING + {880, 200, 1 }, // BEEP_880HZ_200MS + {880, 500, 1 }, // BEEP_880HZ_500MS +#endif + {500, 60, 2 }, // BEEP_500HZ_60MS_DOUBLE_BEEP +#ifdef ENABLE_FEAT_F4HWN + {400, 30, 1 }, // BEEP_400HZ_30MS + {500, 30, 1 }, // BEEP_500HZ_30MS + {600, 30, 1 }, // BEEP_600HZ_30MS +#endif + {880, 60, 3 } // BEEP_880HZ_60MS_TRIPLE_BEEP +}; BEEP_Type_t gBeepToPlay = BEEP_NONE; -static void AUDIO_PlayBeepPulse(void) { - BK4819_ExitTxMute(); - SYSTEM_DelayMs(60); - BK4819_EnterTxMute(); - SYSTEM_DelayMs(20); -} - void AUDIO_PlayBeep(BEEP_Type_t Beep) { + if (Beep == BEEP_NONE) + return; - if (Beep != BEEP_880HZ_60MS_DOUBLE_BEEP && - Beep != BEEP_500HZ_60MS_DOUBLE_BEEP && -#ifdef ENABLE_DTMF_CALLING - Beep != BEEP_880HZ_200MS && - Beep != BEEP_880HZ_500MS && -#endif -#ifdef ENABLE_FEAT_F4HWN - Beep != BEEP_400HZ_30MS && - Beep != BEEP_500HZ_30MS && - Beep != BEEP_600HZ_30MS && -#endif - !gEeprom.BEEP_CONTROL) + if ((Beep == BEEP_1KHZ_60MS_OPTIONAL || + Beep == BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL) && + !gEeprom.BEEP_CONTROL) return; if (gCurrentFunction == FUNCTION_RECEIVE) @@ -80,40 +83,6 @@ void AUDIO_PlayBeep(BEEP_Type_t Beep) uint16_t ToneConfig = BK4819_ReadRegister(BK4819_REG_71); - uint16_t ToneFrequency; - switch (Beep) - { - default: - case BEEP_NONE: - ToneFrequency = 220; - break; - case BEEP_1KHZ_60MS_OPTIONAL: - ToneFrequency = 1000; - break; - case BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL: - case BEEP_500HZ_60MS_DOUBLE_BEEP: - ToneFrequency = 500; - break; - case BEEP_880HZ_60MS_DOUBLE_BEEP: -#ifndef ENABLE_FEAT_F4HWN - case BEEP_880HZ_200MS: - case BEEP_880HZ_500MS: -#endif - ToneFrequency = 880; - break; -#ifdef ENABLE_FEAT_F4HWN - case BEEP_400HZ_30MS: - ToneFrequency = 400; - break; - case BEEP_500HZ_30MS: - ToneFrequency = 500; - break; - case BEEP_600HZ_30MS: - ToneFrequency = 600; - break; -#endif - } - #ifdef ENABLE_FEAT_F4HWN if(Beep == BEEP_400HZ_30MS || Beep == BEEP_500HZ_30MS || Beep == BEEP_600HZ_30MS) { @@ -121,52 +90,17 @@ void AUDIO_PlayBeep(BEEP_Type_t Beep) } #endif - BK4819_PlayTone(ToneFrequency, true); - - SYSTEM_DelayMs(2); - AUDIO_AudioPathOn(); SYSTEM_DelayMs(60); - uint16_t Duration; - switch (Beep) - { - case BEEP_880HZ_60MS_DOUBLE_BEEP: - AUDIO_PlayBeepPulse(); - [[fallthrough]]; - case BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL: - case BEEP_500HZ_60MS_DOUBLE_BEEP: - AUDIO_PlayBeepPulse(); - [[fallthrough]]; - case BEEP_1KHZ_60MS_OPTIONAL: - BK4819_ExitTxMute(); - Duration = 60; - break; -#ifdef ENABLE_FEAT_F4HWN - case BEEP_400HZ_30MS: - case BEEP_500HZ_30MS: - case BEEP_600HZ_30MS: - BK4819_ExitTxMute(); - Duration = 30; - break; -#endif -#ifndef ENABLE_FEAT_F4HWN - case BEEP_880HZ_200MS: - BK4819_ExitTxMute(); - Duration = 200; - break; - case BEEP_880HZ_500MS: -#endif - default: - BK4819_ExitTxMute(); - Duration = 500; - break; - } + BK4819_PrepareToPlayTone(true); - SYSTEM_DelayMs(Duration); - BK4819_EnterTxMute(); - SYSTEM_DelayMs(20); + for (uint8_t i = 0; i < BEEP_Classic_array[Beep][BEEP_REPEATS]; i++) { + BK4819_PlayToneRaw( BEEP_Classic_array[Beep][BEEP_TONE], + BEEP_Classic_array[Beep][BEEP_DURATION]); + SYSTEM_DelayMs(20); + } AUDIO_AudioPathOff(); @@ -182,7 +116,6 @@ void AUDIO_PlayBeep(BEEP_Type_t Beep) SYSTEM_DelayMs(10); #endif - if (gEnableSpeaker) AUDIO_AudioPathOn(); @@ -197,7 +130,6 @@ void AUDIO_PlayBeep(BEEP_Type_t Beep) #ifdef ENABLE_VOX gVoxResumeCountdown = 80; #endif - } #ifdef ENABLE_VOICE diff --git a/App/audio.h b/App/audio.h index 210fd956..22790dbb 100644 --- a/App/audio.h +++ b/App/audio.h @@ -22,6 +22,13 @@ #include "driver/gpio.h" +enum { + BEEP_TONE = 0, + BEEP_DURATION, + BEEP_REPEATS +}; + + enum BEEP_Type_t { BEEP_NONE = 0, @@ -37,7 +44,7 @@ enum BEEP_Type_t BEEP_500HZ_30MS, BEEP_600HZ_30MS, #endif - BEEP_880HZ_60MS_DOUBLE_BEEP + BEEP_880HZ_60MS_TRIPLE_BEEP }; typedef enum BEEP_Type_t BEEP_Type_t; diff --git a/App/driver/backlight.c b/App/driver/backlight.c index dc925803..31e08f33 100644 --- a/App/driver/backlight.c +++ b/App/driver/backlight.c @@ -109,8 +109,8 @@ static void BACKLIGHT_Sound(void) { if (gEeprom.POWER_ON_DISPLAY_MODE == POWER_ON_DISPLAY_MODE_SOUND || gEeprom.POWER_ON_DISPLAY_MODE == POWER_ON_DISPLAY_MODE_ALL) { - AUDIO_PlayBeep(BEEP_880HZ_60MS_DOUBLE_BEEP); - AUDIO_PlayBeep(BEEP_880HZ_60MS_DOUBLE_BEEP); + AUDIO_PlayBeep(BEEP_880HZ_60MS_TRIPLE_BEEP); + AUDIO_PlayBeep(BEEP_880HZ_60MS_TRIPLE_BEEP); } gK5startup = false; diff --git a/App/driver/bk4819.h b/App/driver/bk4819.h index 94f40748..0800f2c5 100644 --- a/App/driver/bk4819.h +++ b/App/driver/bk4819.h @@ -105,7 +105,9 @@ void BK4819_SetCompander(const unsigned int mode); void BK4819_DisableVox(void); void BK4819_DisableDTMF(void); void BK4819_EnableDTMF(void); +void BK4819_PrepareToPlayTone(bool bTuningGainSwitch); void BK4819_PlayTone(uint16_t Frequency, bool bTuningGainSwitch); +void BK4819_PlayToneRaw(const unsigned int tone_Hz, const unsigned int delay); void BK4819_PlaySingleTone(const unsigned int tone_Hz, const unsigned int delay, const unsigned int level, const bool play_speaker); void BK4819_EnterTxMute(void); void BK4819_ExitTxMute(void); diff --git a/App/driver/bk4829.c b/App/driver/bk4829.c index ada22059..d79d6576 100644 --- a/App/driver/bk4829.c +++ b/App/driver/bk4829.c @@ -1031,7 +1031,7 @@ void BK4819_EnableDTMF(void) (15u << BK4819_REG_24_SHIFT_MAX_SYMBOLS)); // 0 ~ 15 } -void BK4819_PlayTone(uint16_t Frequency, bool bTuningGainSwitch) +void BK4819_PrepareToPlayTone(bool bTuningGainSwitch) { uint16_t ToneConfig = BK4819_REG_70_ENABLE_TONE1; @@ -1046,10 +1046,23 @@ void BK4819_PlayTone(uint16_t Frequency, bool bTuningGainSwitch) BK4819_WriteRegister(BK4819_REG_30, 0); BK4819_WriteRegister(BK4819_REG_30, BK4819_REG_30_ENABLE_AF_DAC | BK4819_REG_30_ENABLE_DISC_MODE | BK4819_REG_30_ENABLE_TX_DSP); +} + +void BK4819_PlayTone(uint16_t Frequency, bool bTuningGainSwitch) +{ + BK4819_PrepareToPlayTone(bTuningGainSwitch); BK4819_WriteRegister(BK4819_REG_71, scale_freq(Frequency)); } +void BK4819_PlayToneRaw(const unsigned int tone_Hz, const unsigned int delay) { + BK4819_WriteRegister(BK4819_REG_71, scale_freq(tone_Hz)); + + BK4819_ExitTxMute(); + SYSTEM_DelayMs(delay); + BK4819_EnterTxMute(); +} + // level 0 ~ 127 void BK4819_PlaySingleTone(const unsigned int tone_Hz, const unsigned int delay, const unsigned int level, const bool play_speaker) { @@ -1069,11 +1082,7 @@ void BK4819_PlaySingleTone(const unsigned int tone_Hz, const unsigned int delay, BK4819_EnableTXLink(); SYSTEM_DelayMs(50); - BK4819_WriteRegister(BK4819_REG_71, scale_freq(tone_Hz)); - - BK4819_ExitTxMute(); - SYSTEM_DelayMs(delay); - BK4819_EnterTxMute(); + BK4819_PlayToneRaw(tone_Hz, delay); if (play_speaker) { @@ -1767,17 +1776,8 @@ static void BK4819_PlayRogerNormal(void) BK4819_EnableTXLink(); SYSTEM_DelayMs(50); - BK4819_WriteRegister(BK4819_REG_71, scale_freq(tone1_Hz)); - - BK4819_ExitTxMute(); - SYSTEM_DelayMs(80); - BK4819_EnterTxMute(); - - BK4819_WriteRegister(BK4819_REG_71, scale_freq(tone2_Hz)); - - BK4819_ExitTxMute(); - SYSTEM_DelayMs(80); - BK4819_EnterTxMute(); + BK4819_PlayToneRaw(tone1_Hz, 80); + BK4819_PlayToneRaw(tone2_Hz, 80); BK4819_WriteRegister(BK4819_REG_70, 0x0000); BK4819_WriteRegister(BK4819_REG_30, 0xC1FE); // 1 1 0000 0 1 1111 1 1 1 0 From 1e03dc4af038105a4b6b0440313a111576c0175c Mon Sep 17 00:00:00 2001 From: mrkusypl Date: Sat, 2 May 2026 00:35:43 +0200 Subject: [PATCH 14/33] Minor changes to the refactoring of beep handling --- App/audio.c | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/App/audio.c b/App/audio.c index a1249d64..7b09141b 100644 --- a/App/audio.c +++ b/App/audio.c @@ -33,22 +33,21 @@ #include "settings.h" #include "ui/ui.h" -static const uint16_t BEEP_Classic_array[][3] = { -// Tone Duration Repeats - {0, 0, 0 }, // BEEP_NONE - {1000, 60, 1 }, // BEEP_1KHZ_60MS_OPTIONAL - {500, 60, 2 }, // BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL +static const uint16_t BEEP_Classic_array[][3] = { /* Tone Duration Repeats */ + [BEEP_NONE] = {0, 0, 0 }, + [BEEP_1KHZ_60MS_OPTIONAL] = {1000, 60, 1 }, + [BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL] = {500, 60, 2 }, #ifdef ENABLE_DTMF_CALLING - {880, 200, 1 }, // BEEP_880HZ_200MS - {880, 500, 1 }, // BEEP_880HZ_500MS + [BEEP_880HZ_200MS] = {880, 200, 1 }, + [BEEP_880HZ_500MS] = {880, 500, 1 }, #endif - {500, 60, 2 }, // BEEP_500HZ_60MS_DOUBLE_BEEP + [BEEP_500HZ_60MS_DOUBLE_BEEP] = {500, 60, 2 }, #ifdef ENABLE_FEAT_F4HWN - {400, 30, 1 }, // BEEP_400HZ_30MS - {500, 30, 1 }, // BEEP_500HZ_30MS - {600, 30, 1 }, // BEEP_600HZ_30MS + [BEEP_400HZ_30MS] = {400, 30, 1 }, + [BEEP_500HZ_30MS] = {500, 30, 1 }, + [BEEP_600HZ_30MS] = {600, 30, 1 }, #endif - {880, 60, 3 } // BEEP_880HZ_60MS_TRIPLE_BEEP + [BEEP_880HZ_60MS_TRIPLE_BEEP] = {880, 60, 3 } }; BEEP_Type_t gBeepToPlay = BEEP_NONE; @@ -69,6 +68,9 @@ void AUDIO_PlayBeep(BEEP_Type_t Beep) if (gCurrentFunction == FUNCTION_MONITOR) return; + if (Beep >= ARRAY_SIZE(BEEP_Classic_array)) + return; + #ifdef ENABLE_FMRADIO if (gFmRadioMode) BK1080_Mute(true); @@ -90,12 +92,14 @@ void AUDIO_PlayBeep(BEEP_Type_t Beep) } #endif + BK4819_PrepareToPlayTone(true); + + SYSTEM_DelayMs(2); + AUDIO_AudioPathOn(); SYSTEM_DelayMs(60); - BK4819_PrepareToPlayTone(true); - for (uint8_t i = 0; i < BEEP_Classic_array[Beep][BEEP_REPEATS]; i++) { BK4819_PlayToneRaw( BEEP_Classic_array[Beep][BEEP_TONE], BEEP_Classic_array[Beep][BEEP_DURATION]); From 46cf7d7d579829e764559b070f81f1632e22a80b Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Sat, 2 May 2026 00:43:59 +0200 Subject: [PATCH 15/33] Fix Breakout beep audio setup order --- App/app/breakout.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/App/app/breakout.c b/App/app/breakout.c index 6b626ea6..e9867a25 100644 --- a/App/app/breakout.c +++ b/App/app/breakout.c @@ -70,8 +70,8 @@ void reset(void) // PlayBeep void playBeep(uint16_t tone) { - AUDIO_AudioPathOn(); BK4819_PrepareToPlayTone(true); + AUDIO_AudioPathOn(); BK4819_PlayToneRaw(tone, 100); AUDIO_AudioPathOff(); } @@ -395,4 +395,4 @@ void APP_RunBreakout(void) { } #endif } -} \ No newline at end of file +} From a0aefae01bff5705cfd7d502161895d2be8636d9 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Sun, 3 May 2026 02:07:32 +0200 Subject: [PATCH 16/33] Fix manual scan resume from active RX --- App/app/action.c | 4 +--- App/app/chFrScanner.c | 25 ++++++++++++++++++++++++- App/app/chFrScanner.h | 3 ++- App/app/main.c | 4 +--- 4 files changed, 28 insertions(+), 8 deletions(-) diff --git a/App/app/action.c b/App/app/action.c index df33e903..f441940d 100644 --- a/App/app/action.c +++ b/App/app/action.c @@ -243,9 +243,7 @@ void ACTION_Scan(bool bRestart) #endif // jump to the next channel - CHFRSCANNER_Start(false, gScanStateDir); - gScanPauseDelayIn_10ms = 1; - gScheduleScanListen = false; + CHFRSCANNER_ManualResume(gScanStateDir); } else { #ifdef ENABLE_FEAT_F4HWN_RESUME_STATE if(gScanRangeStart == 0) // No ScanRange diff --git a/App/app/chFrScanner.c b/App/app/chFrScanner.c index cc9204a0..7efd2c9b 100644 --- a/App/app/chFrScanner.c +++ b/App/app/chFrScanner.c @@ -349,6 +349,26 @@ void CHFRSCANNER_Start(const bool storeBackupSettings, const int8_t scan_directi gScanPauseMode = false; } +void CHFRSCANNER_ManualResume(const int8_t scan_direction) +{ + if (FUNCTION_IsRx()) + { + // Abort the current reception before a user-forced scan step, otherwise + // HandleReceive() can re-apply the carrier resume delay on the next tick. + gMonitor = false; + gRxReceptionMode = RX_MODE_NONE; + gScanPauseMode = false; + + FUNCTION_Init(); + FUNCTION_Select(FUNCTION_FOREGROUND); + } + + CHFRSCANNER_Start(false, scan_direction); + + gScanPauseDelayIn_10ms = 1; + gScheduleScanListen = false; +} + /* void CHFRSCANNER_ContinueScanning(void) { @@ -380,7 +400,10 @@ void CHFRSCANNER_ContinueScanning(void) gCurrentFunction != FUNCTION_INCOMING && !g_SquelchLost) { - ScanFastResetState(); + // A rejected full-tune candidate is just a false RSSI hit. Keep the + // learned floor; resetting here can make the next channel blind when + // it is the real signal, especially while scanning down. + scanFastLastFullTuneCandidate = false; } #endif diff --git a/App/app/chFrScanner.h b/App/app/chFrScanner.h index 51b2c9a6..84e36348 100644 --- a/App/app/chFrScanner.h +++ b/App/app/chFrScanner.h @@ -18,6 +18,7 @@ extern uint32_t gScanRangeStop; void CHFRSCANNER_Found(void); void CHFRSCANNER_Stop(void); void CHFRSCANNER_Start(const bool storeBackupSettings, const int8_t scan_direction); +void CHFRSCANNER_ManualResume(const int8_t scan_direction); void CHFRSCANNER_ContinueScanning(void); #if defined(ENABLE_FEAT_F4HWN_RESUME_STATE) || defined(ENABLE_SCAN_RANGES) @@ -29,4 +30,4 @@ void CHFRSCANNER_ContinueScanning(void); extern uint32_t lastFoundFrqOrChanOld; #endif -#endif \ No newline at end of file +#endif diff --git a/App/app/main.c b/App/app/main.c index 35e58e03..1708a477 100644 --- a/App/app/main.c +++ b/App/app/main.c @@ -1010,9 +1010,7 @@ static void MAIN_Key_UP_DOWN(bool bKeyPressed, bool bKeyHeld, int8_t Direction) } // jump to the next channel - CHFRSCANNER_Start(false, Direction); - gScanPauseDelayIn_10ms = 1; - gScheduleScanListen = false; + CHFRSCANNER_ManualResume(Direction); gPttWasReleased = true; } From 5157dc573b825e603019b4d581d502a2668ce7ef Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Sun, 3 May 2026 14:39:13 +0200 Subject: [PATCH 17/33] Fix scan resume, AM/FM precheck and list refresh --- App/app/action.c | 3 ++ App/app/chFrScanner.c | 79 +++++++++++++++++++++++++++++++++++-------- App/app/main.c | 6 ++++ App/settings.c | 25 ++++++++++++++ App/settings.h | 1 + 5 files changed, 100 insertions(+), 14 deletions(-) diff --git a/App/app/action.c b/App/app/action.c index f441940d..1b925bf9 100644 --- a/App/app/action.c +++ b/App/app/action.c @@ -40,6 +40,7 @@ #include "misc.h" #include "settings.h" #include "ui/inputbox.h" +#include "ui/main.h" #include "ui/ui.h" #ifdef ENABLE_REGA #include "app/rega.h" @@ -237,6 +238,8 @@ void ACTION_Scan(bool bRestart) // channel mode. Keep scanning but toggle between scan lists RADIO_NextValidList(1); + UI_MAIN_NotifyScanProgressDataChanged(); + gUpdateStatus = true; #ifdef ENABLE_FEAT_F4HWN_RESUME_STATE SETTINGS_WriteCurrentState(); diff --git a/App/app/chFrScanner.c b/App/app/chFrScanner.c index 7efd2c9b..3c755fc1 100644 --- a/App/app/chFrScanner.c +++ b/App/app/chFrScanner.c @@ -1,12 +1,14 @@ #include "app/app.h" #include "app/chFrScanner.h" +#include "audio.h" #ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER #include "driver/systick.h" #endif #include "functions.h" #include "misc.h" #include "settings.h" +#include "ui/main.h" //#include "debugging.h" int8_t gScanStateDir; @@ -40,6 +42,21 @@ uint8_t initialCROSS_BAND_RX_TX; static void NextFreqChannel(void); static void NextMemChannel(void); +static void CHFRSCANNER_AbortActiveReception(void) +{ + if (!FUNCTION_IsRx()) + return; + + AUDIO_AudioPathOff(); + gEnableSpeaker = false; + gMonitor = false; + gRxReceptionMode = RX_MODE_NONE; + gScanPauseMode = false; + + FUNCTION_Init(); + FUNCTION_Select(FUNCTION_FOREGROUND); +} + #ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER #define SCAN_FAST_PRECHECK_STEPS 6 #define SCAN_FAST_RSSI_MARGIN 16 @@ -136,6 +153,34 @@ static bool ScanFastIsCandidate(uint16_t rssi) return false; } + +static void ScanFastApplyChannelShape(ModulationMode_t modulation) +{ + const bool modulationChanged = gRxVfo->Modulation != modulation; + const bool bandwidthChanged = gRxVfo->CHANNEL_BANDWIDTH != BANDWIDTH_WIDE; + + if (!modulationChanged && !bandwidthChanged) + return; + + gRxVfo->Modulation = modulation; + gRxVfo->CHANNEL_BANDWIDTH = BANDWIDTH_WIDE; + + if (modulationChanged) + RADIO_SetModulation(modulation); + + if (modulation == MODULATION_AM) + { + BK4819_SetFilterBandwidth(RADIO_GetAMFilterBandwidth(gRxVfo), true); + } + else + { +#ifdef ENABLE_AM_FIX + BK4819_SetFilterBandwidth(BK4819_FILTER_BW_WIDE, true); +#else + BK4819_SetFilterBandwidth(BK4819_FILTER_BW_WIDE, false); +#endif + } +} #endif #if defined(ENABLE_FEAT_F4HWN_SCAN_FASTER) && defined(ENABLE_SCAN_RANGES) @@ -240,19 +285,23 @@ static scan_fast_result_t ScanRangeFastPrecheck(void) #ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER static bool MemChannelFastPrecheck(uint16_t channel) { + uint32_t frequency; + ModulationMode_t modulation; + if (gRxVfo->SquelchOpenRSSIThresh == 0) { scanFastLastFullTuneCandidate = false; return true; } - const uint32_t frequency = SETTINGS_FetchChannelFrequency(channel); - if (frequency == 0 || frequency == 0xFFFFFFFF) + if (!SETTINGS_FetchChannelScanInfo(channel, &frequency, &modulation)) { scanFastLastFullTuneCandidate = false; return true; } + ScanFastApplyChannelShape(modulation); + // Mute AF DAC for the same reason as in ScanRangeFastPrecheck(). scanFastReg30 = BK4819_ReadRegister(BK4819_REG_30) & ~BK4819_REG_30_MASK_ENABLE_AF_DAC; ScanFastTune(frequency); @@ -308,6 +357,7 @@ void CHFRSCANNER_Start(const bool storeBackupSettings, const int8_t scan_directi } RADIO_SelectVfos(); + CHFRSCANNER_AbortActiveReception(); gNextMrChannel = gRxVfo->CHANNEL_SAVE; currentScanList = SCAN_NEXT_CHAN_SCANLIST1; @@ -321,6 +371,8 @@ void CHFRSCANNER_Start(const bool storeBackupSettings, const int8_t scan_directi if(!RADIO_CheckValidList(gEeprom.SCAN_LIST_DEFAULT)) { RADIO_NextValidList(1); + UI_MAIN_NotifyScanProgressDataChanged(); + gUpdateStatus = true; } // channel mode @@ -351,18 +403,6 @@ void CHFRSCANNER_Start(const bool storeBackupSettings, const int8_t scan_directi void CHFRSCANNER_ManualResume(const int8_t scan_direction) { - if (FUNCTION_IsRx()) - { - // Abort the current reception before a user-forced scan step, otherwise - // HandleReceive() can re-apply the carrier resume delay on the next tick. - gMonitor = false; - gRxReceptionMode = RX_MODE_NONE; - gScanPauseMode = false; - - FUNCTION_Init(); - FUNCTION_Select(FUNCTION_FOREGROUND); - } - CHFRSCANNER_Start(false, scan_direction); gScanPauseDelayIn_10ms = 1; @@ -673,6 +713,7 @@ static void NextMemChannel(void) if (!enabled || chan == 0xFFFF) { + const uint16_t searchStart = gNextMrChannel; chan = RADIO_FindNextChannel(gNextMrChannel + gScanStateDir, gScanStateDir, true, gEeprom.SCAN_LIST_DEFAULT); if (chan == 0xFFFF) { // no valid channel found -> wrapping back to the first channel @@ -684,6 +725,16 @@ static void NextMemChannel(void) ScanFastResetState(); #endif } +#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER + else if ((gScanStateDir > 0 && chan < searchStart) || + (gScanStateDir < 0 && chan > searchStart)) + { + // RADIO_FindNextChannel() wraps internally, so 0xFFFF is not + // returned on a normal full-sweep wrap. Detect that transition + // here and restart the RSSI floor learning for the new pass. + ScanFastResetState(); + } +#endif gNextMrChannel = chan; diff --git a/App/app/main.c b/App/app/main.c index 1708a477..525112d9 100644 --- a/App/app/main.c +++ b/App/app/main.c @@ -300,6 +300,8 @@ static void processFKeyFunction(const KEY_Code_t Key, const bool beep) if (gScanStateDir != SCAN_OFF) { RADIO_NextValidList(isKeyUp ? 1 : -1); + UI_MAIN_NotifyScanProgressDataChanged(); + gUpdateStatus = true; } else { // Adjust squelch: UP increments, DOWN decrements if (gSquelchLevelOriginal == 10) @@ -482,6 +484,8 @@ static void MAIN_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) if (value == 0) { gEeprom.SCAN_LIST_DEFAULT = MR_CHANNELS_LIST + 1; + UI_MAIN_NotifyScanProgressDataChanged(); + gUpdateStatus = true; #ifdef ENABLE_FEAT_F4HWN_RESUME_STATE SETTINGS_WriteCurrentState(); #endif @@ -500,6 +504,8 @@ static void MAIN_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL; RADIO_NextValidList(1); } + UI_MAIN_NotifyScanProgressDataChanged(); + gUpdateStatus = true; #ifdef ENABLE_FEAT_F4HWN_RESUME_STATE SETTINGS_WriteCurrentState(); diff --git a/App/settings.c b/App/settings.c index 66eb4688..ecbf9e9e 100644 --- a/App/settings.c +++ b/App/settings.c @@ -579,6 +579,31 @@ uint32_t SETTINGS_FetchChannelFrequency(const uint16_t channel) return info.frequency; } +bool SETTINGS_FetchChannelScanInfo(const uint16_t channel, uint32_t *frequency, ModulationMode_t *modulation) +{ + struct + { + uint32_t frequency; + uint32_t offset; + uint8_t settings[4]; + } __attribute__((packed)) info; + + PY25Q16_ReadBuffer(channel * 16, &info, sizeof(info)); + + if (frequency) + *frequency = info.frequency; + + if (modulation) + { + uint8_t mode = info.settings[3] >> 4; + if (mode >= MODULATION_UKNOWN) + mode = MODULATION_FM; + *modulation = (ModulationMode_t)mode; + } + + return info.frequency != 0 && info.frequency != 0xFFFFFFFF; +} + void SETTINGS_FetchChannelName(char *s, const uint16_t channel) { if (s == NULL) diff --git a/App/settings.h b/App/settings.h index 77e22463..5ea7efcb 100644 --- a/App/settings.h +++ b/App/settings.h @@ -310,6 +310,7 @@ extern EEPROM_Config_t gEeprom; void SETTINGS_InitEEPROM(void); void SETTINGS_LoadCalibration(void); uint32_t SETTINGS_FetchChannelFrequency(const uint16_t channel); +bool SETTINGS_FetchChannelScanInfo(const uint16_t channel, uint32_t *frequency, ModulationMode_t *modulation); void SETTINGS_FetchChannelName(char *s, const uint16_t channel); void SETTINGS_FactoryReset(bool bIsAll); #ifdef ENABLE_FMRADIO From ec4ea28325bea912c4254146b1846ffcb31005d7 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Sun, 3 May 2026 16:49:47 +0200 Subject: [PATCH 18/33] Reset fast scan RSSI floor on AM/FM changes --- App/app/chFrScanner.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/App/app/chFrScanner.c b/App/app/chFrScanner.c index 3c755fc1..e03b28d1 100644 --- a/App/app/chFrScanner.c +++ b/App/app/chFrScanner.c @@ -98,6 +98,11 @@ static void ScanFastResetState(void) scanFastLastFullTuneCandidate = false; } +static void ScanFastResetNoiseFloor(void) +{ + scanFastNoiseFloor = SCAN_FAST_RSSI_MAX; +} + static uint16_t ScanFastReadRssi(void) { uint8_t guard = SCAN_FAST_GLITCH_GUARD_MAX; @@ -180,6 +185,15 @@ static void ScanFastApplyChannelShape(ModulationMode_t modulation) BK4819_SetFilterBandwidth(BK4819_FILTER_BW_WIDE, false); #endif } + + if (modulationChanged) + { + // AM and FM use different demod/AGC profiles, so their RSSI + // baselines are not directly comparable. Relearn the floor after + // crossing that boundary instead of treating the next FM block as + // a wall of candidates. + ScanFastResetNoiseFloor(); + } } #endif From 3e6b169b7b50b3aaafca9b9800a639e72d618ebb Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Sun, 3 May 2026 22:12:54 +0200 Subject: [PATCH 19/33] Improve fast scan detection and display metadata --- App/app/chFrScanner.c | 149 +++++++++++++++++++++++++++++++++++++----- App/app/chFrScanner.h | 5 ++ App/radio.h | 1 + App/settings.c | 93 ++++++++++++++++++++++++++ App/settings.h | 20 ++++++ App/ui/main.c | 17 +++-- 6 files changed, 264 insertions(+), 21 deletions(-) diff --git a/App/app/chFrScanner.c b/App/app/chFrScanner.c index e03b28d1..8518466a 100644 --- a/App/app/chFrScanner.c +++ b/App/app/chFrScanner.c @@ -1,4 +1,6 @@ +#include + #include "app/app.h" #include "app/chFrScanner.h" #include "audio.h" @@ -61,6 +63,8 @@ static void CHFRSCANNER_AbortActiveReception(void) #define SCAN_FAST_PRECHECK_STEPS 6 #define SCAN_FAST_RSSI_MARGIN 16 #define SCAN_FAST_SQUELCH_MARGIN 8 +#define SCAN_FAST_WEAK_MARGIN 8 +#define SCAN_FAST_RECHECK_DELAY_US 350 #define SCAN_FAST_FINE_STEP_LIMIT 250 #define SCAN_FAST_FINE_REFINE_SPAN 1000 #define SCAN_FAST_FINE_REFINE_MAX 80 @@ -86,6 +90,8 @@ static uint16_t scanFastReg30; static uint16_t scanFastNoiseFloor = SCAN_FAST_RSSI_MAX; static uint32_t scanFastPrevFrequency; static bool scanFastLastFullTuneCandidate; +static VFO_Info_t scanFastDisplayVfo; +static bool scanFastDisplayVfoValid; static void ScanFastResetState(void) { @@ -96,6 +102,7 @@ static void ScanFastResetState(void) scanFastNoiseFloor = SCAN_FAST_RSSI_MAX; scanFastPrevFrequency = 0; scanFastLastFullTuneCandidate = false; + scanFastDisplayVfoValid = false; } static void ScanFastResetNoiseFloor(void) @@ -131,25 +138,133 @@ static void ScanFastTune(uint32_t frequency) BK4819_WriteRegister(BK4819_REG_30, scanFastReg30); } -static bool ScanFastIsCandidate(uint16_t rssi) +const VFO_Info_t *CHFRSCANNER_GetScanDisplayVfo(void) { - if (scanFastNoiseFloor == SCAN_FAST_RSSI_MAX) + if (!scanFastDisplayVfoValid || gScanStateDir == SCAN_OFF || FUNCTION_IsRx()) + return NULL; + + return &scanFastDisplayVfo; +} + +static bool ScanFastUpdateDisplayVfo(uint16_t channel, uint32_t *frequency, ModulationMode_t *modulation) +{ + ChannelScanDisplayInfo_t info; + + if (!SETTINGS_FetchChannelScanDisplayInfo(channel, &info)) { - scanFastNoiseFloor = rssi; + scanFastDisplayVfoValid = false; return false; } - const uint16_t noiseTrigger = - (scanFastNoiseFloor > SCAN_FAST_RSSI_MAX - SCAN_FAST_RSSI_MARGIN) - ? SCAN_FAST_RSSI_MAX - : (uint16_t)(scanFastNoiseFloor + SCAN_FAST_RSSI_MARGIN); - const uint16_t squelchTrigger = - (gRxVfo->SquelchOpenRSSIThresh > SCAN_FAST_SQUELCH_MARGIN) - ? (uint16_t)(gRxVfo->SquelchOpenRSSIThresh - SCAN_FAST_SQUELCH_MARGIN) - : 0; + scanFastDisplayVfo = gEeprom.VfoInfo[gEeprom.RX_VFO]; - if (rssi >= noiseTrigger && rssi >= squelchTrigger) + scanFastDisplayVfo.CHANNEL_SAVE = channel; + scanFastDisplayVfo.freq_config_RX = info.rx; + scanFastDisplayVfo.freq_config_TX = info.tx; + scanFastDisplayVfo.TX_OFFSET_FREQUENCY = info.offset; + scanFastDisplayVfo.StepFrequency = info.stepFrequency; + scanFastDisplayVfo.STEP_SETTING = info.stepSetting; + scanFastDisplayVfo.Modulation = info.modulation; + scanFastDisplayVfo.TX_OFFSET_FREQUENCY_DIRECTION = info.txOffsetFrequencyDirection; + scanFastDisplayVfo.OUTPUT_POWER = info.outputPower; + scanFastDisplayVfo.FrequencyReverse = info.frequencyReverse; + scanFastDisplayVfo.CHANNEL_BANDWIDTH = info.channelBandwidth; + scanFastDisplayVfo.BUSY_CHANNEL_LOCK = info.busyChannelLock; + scanFastDisplayVfo.TX_LOCK = info.txLock; +#ifdef ENABLE_DTMF_CALLING + scanFastDisplayVfo.DTMF_DECODING_ENABLE = info.dtmfDecodingEnable; +#endif + scanFastDisplayVfo.DTMF_PTT_ID_TX_MODE = info.dtmfPttIdTxMode; + + if (!scanFastDisplayVfo.FrequencyReverse) + { + scanFastDisplayVfo.pRX = &scanFastDisplayVfo.freq_config_RX; + scanFastDisplayVfo.pTX = &scanFastDisplayVfo.freq_config_TX; + } + else + { + scanFastDisplayVfo.pRX = &scanFastDisplayVfo.freq_config_TX; + scanFastDisplayVfo.pTX = &scanFastDisplayVfo.freq_config_RX; + } + + scanFastDisplayVfoValid = true; + + if (frequency) + *frequency = info.rx.Frequency; + if (modulation) + *modulation = info.modulation; + + return true; +} + +static uint16_t ScanFastSaturatingAdd(uint16_t value, uint16_t add) +{ + return (value > SCAN_FAST_RSSI_MAX - add) ? SCAN_FAST_RSSI_MAX : (uint16_t)(value + add); +} + +static uint16_t ScanFastSaturatingSub(uint16_t value, uint16_t sub) +{ + return (value > sub) ? (uint16_t)(value - sub) : 0; +} + +static uint16_t ScanFastGetNoiseTrigger(void) +{ + return ScanFastSaturatingAdd(scanFastNoiseFloor, SCAN_FAST_RSSI_MARGIN); +} + +static uint16_t ScanFastGetSquelchTrigger(void) +{ + return ScanFastSaturatingSub(gRxVfo->SquelchOpenRSSIThresh, SCAN_FAST_SQUELCH_MARGIN); +} + +static bool ScanFastIsNearCandidate(uint16_t rssi) +{ + const uint16_t rssiWithMargin = ScanFastSaturatingAdd(rssi, SCAN_FAST_WEAK_MARGIN); + const uint16_t squelchTrigger = ScanFastGetSquelchTrigger(); + + if (scanFastNoiseFloor == SCAN_FAST_RSSI_MAX) + return gRxVfo->SquelchOpenRSSIThresh > 0 && + rssiWithMargin >= gRxVfo->SquelchOpenRSSIThresh; + + return rssiWithMargin >= ScanFastGetNoiseTrigger() && + rssiWithMargin >= squelchTrigger; +} + +static uint16_t ScanFastReadCandidateRssi(void) +{ + uint16_t rssi = ScanFastReadRssi(); + + if (ScanFastIsNearCandidate(rssi)) + { + SYSTICK_DelayUs(SCAN_FAST_RECHECK_DELAY_US); + + const uint16_t retryRssi = ScanFastReadRssi(); + if (retryRssi > rssi) + rssi = retryRssi; + } + + return rssi; +} + +static bool ScanFastIsCandidate(uint16_t rssi) +{ + const uint16_t squelchTrigger = ScanFastGetSquelchTrigger(); + + if (scanFastNoiseFloor == SCAN_FAST_RSSI_MAX) + { + scanFastNoiseFloor = rssi; + return gRxVfo->SquelchOpenRSSIThresh > 0 && + rssi >= gRxVfo->SquelchOpenRSSIThresh; + } + + const uint16_t noiseTrigger = ScanFastGetNoiseTrigger(); + const uint16_t rssiWithMargin = ScanFastSaturatingAdd(rssi, SCAN_FAST_WEAK_MARGIN); + + if ((rssi >= noiseTrigger && rssi >= squelchTrigger) || + (rssiWithMargin >= noiseTrigger && rssiWithMargin >= squelchTrigger)) + { return true; + } if (rssi < scanFastNoiseFloor) scanFastNoiseFloor = rssi; @@ -284,7 +399,7 @@ static scan_fast_result_t ScanRangeFastPrecheck(void) ScanFastTune(freq); - const uint16_t rssi = ScanFastReadRssi(); + const uint16_t rssi = ScanFastReadCandidateRssi(); if (ScanFastIsCandidate(rssi)) { ScanRangeFastRefineCandidate(rssi); @@ -308,7 +423,7 @@ static bool MemChannelFastPrecheck(uint16_t channel) return true; } - if (!SETTINGS_FetchChannelScanInfo(channel, &frequency, &modulation)) + if (!ScanFastUpdateDisplayVfo(channel, &frequency, &modulation)) { scanFastLastFullTuneCandidate = false; return true; @@ -320,7 +435,7 @@ static bool MemChannelFastPrecheck(uint16_t channel) scanFastReg30 = BK4819_ReadRegister(BK4819_REG_30) & ~BK4819_REG_30_MASK_ENABLE_AF_DAC; ScanFastTune(frequency); - if (ScanFastIsCandidate(ScanFastReadRssi())) + if (ScanFastIsCandidate(ScanFastReadCandidateRssi())) { scanFastLastFullTuneCandidate = true; return true; // signal detected: let the full tune path follow @@ -419,7 +534,9 @@ void CHFRSCANNER_ManualResume(const int8_t scan_direction) { CHFRSCANNER_Start(false, scan_direction); - gScanPauseDelayIn_10ms = 1; + gScanPauseDelayIn_10ms = (gRxVfo->SquelchOpenRSSIThresh == 0) + ? scan_pause_delay_in_3_10ms + : 1; gScheduleScanListen = false; } diff --git a/App/app/chFrScanner.h b/App/app/chFrScanner.h index 84e36348..26ca3ef9 100644 --- a/App/app/chFrScanner.h +++ b/App/app/chFrScanner.h @@ -4,6 +4,8 @@ #include #include +#include "radio.h" + // scan direction, if not equal SCAN_OFF indicates // that we are in a process of scanning channels/frequencies extern int8_t gScanStateDir; @@ -20,6 +22,9 @@ void CHFRSCANNER_Stop(void); void CHFRSCANNER_Start(const bool storeBackupSettings, const int8_t scan_direction); void CHFRSCANNER_ManualResume(const int8_t scan_direction); void CHFRSCANNER_ContinueScanning(void); +#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER +const VFO_Info_t *CHFRSCANNER_GetScanDisplayVfo(void); +#endif #if defined(ENABLE_FEAT_F4HWN_RESUME_STATE) || defined(ENABLE_SCAN_RANGES) void CHFRSCANNER_ScanRange(void); diff --git a/App/radio.h b/App/radio.h index 8fa28fa3..e6ae7f01 100644 --- a/App/radio.h +++ b/App/radio.h @@ -156,6 +156,7 @@ uint16_t RADIO_FindNextChannel(uint16_t ChNum, int8_t Direction, bool bCheckScan void RADIO_InitInfo(VFO_Info_t *pInfo, const uint16_t ChannelSave, const uint32_t Frequency); void RADIO_ConfigureChannel(const unsigned int VFO, const unsigned int configure); void RADIO_ConfigureSquelchAndOutputPower(VFO_Info_t *pInfo); +void RADIO_ValidateAndSetCode(FREQ_Config_t *pFreq_Config, uint8_t tmp); void RADIO_ApplyOffset(VFO_Info_t *pInfo); void RADIO_SelectVfos(void); void RADIO_SetupRegisters(bool switchToForeground); diff --git a/App/settings.c b/App/settings.c index ecbf9e9e..6e993032 100644 --- a/App/settings.c +++ b/App/settings.c @@ -604,6 +604,99 @@ bool SETTINGS_FetchChannelScanInfo(const uint16_t channel, uint32_t *frequency, return info.frequency != 0 && info.frequency != 0xFFFFFFFF; } +bool SETTINGS_FetchChannelScanDisplayInfo(const uint16_t channel, ChannelScanDisplayInfo_t *info) +{ + if (info == NULL) + return false; + + struct + { + uint32_t frequency; + uint32_t offset; + uint8_t data[8]; + } __attribute__((packed)) raw; + + PY25Q16_ReadBuffer(channel * 16, &raw, sizeof(raw)); + + if (raw.frequency == 0 || raw.frequency == 0xFFFFFFFF) + return false; + + memset(info, 0, sizeof(*info)); + + info->rx.Frequency = raw.frequency; + info->tx.Frequency = raw.frequency; + info->offset = (raw.offset >= _1GHz_in_KHz) ? (_1GHz_in_KHz / 100) : raw.offset; + + info->rx.CodeType = (raw.data[2] >> 0) & 0x0F; + info->tx.CodeType = (raw.data[2] >> 4) & 0x0F; + RADIO_ValidateAndSetCode(&info->rx, raw.data[0]); + RADIO_ValidateAndSetCode(&info->tx, raw.data[1]); + + uint8_t tmp = raw.data[3] & 0x0F; + if (tmp > TX_OFFSET_FREQUENCY_DIRECTION_SUB) + tmp = TX_OFFSET_FREQUENCY_DIRECTION_OFF; + info->txOffsetFrequencyDirection = tmp; + + tmp = raw.data[3] >> 4; + if (tmp >= MODULATION_UKNOWN) + tmp = MODULATION_FM; + info->modulation = (ModulationMode_t)tmp; + + tmp = raw.data[6]; + if (tmp >= STEP_N_ELEM) + tmp = STEP_12_5kHz; + info->stepSetting = (STEP_Setting_t)tmp; + info->stepFrequency = gStepFrequencyTable[tmp]; + + if (raw.data[4] == 0xFF) + { + info->frequencyReverse = false; + info->channelBandwidth = BANDWIDTH_WIDE; + info->outputPower = OUTPUT_POWER_LOW1; + info->busyChannelLock = false; + info->txLock = true; + } + else + { + const uint8_t d4 = raw.data[4]; + info->frequencyReverse = !!((d4 >> 0) & 1u); + info->channelBandwidth = !!((d4 >> 1) & 1u); + info->outputPower = ((d4 >> 2) & 7u); + info->busyChannelLock = !!((d4 >> 5) & 1u); + info->txLock = !!((d4 >> 6) & 1u); + } + + switch (info->txOffsetFrequencyDirection) + { + case TX_OFFSET_FREQUENCY_DIRECTION_ADD: + info->tx.Frequency = raw.frequency + info->offset; + break; + case TX_OFFSET_FREQUENCY_DIRECTION_SUB: + info->tx.Frequency = raw.frequency - info->offset; + break; + default: + break; + } + + if (raw.data[5] == 0xFF) + { +#ifdef ENABLE_DTMF_CALLING + info->dtmfDecodingEnable = false; +#endif + info->dtmfPttIdTxMode = PTT_ID_OFF; + } + else + { +#ifdef ENABLE_DTMF_CALLING + info->dtmfDecodingEnable = (raw.data[5] >> 0) & 1u; +#endif + const uint8_t pttId = (raw.data[5] >> 1) & 7u; + info->dtmfPttIdTxMode = pttId < ARRAY_SIZE(gSubMenu_PTT_ID) ? pttId : PTT_ID_OFF; + } + + return true; +} + void SETTINGS_FetchChannelName(char *s, const uint16_t channel) { if (s == NULL) diff --git a/App/settings.h b/App/settings.h index 5ea7efcb..b833c61d 100644 --- a/App/settings.h +++ b/App/settings.h @@ -307,10 +307,30 @@ typedef struct { extern EEPROM_Config_t gEeprom; +typedef struct { + FREQ_Config_t rx; + FREQ_Config_t tx; + uint32_t offset; + uint16_t stepFrequency; + STEP_Setting_t stepSetting; + ModulationMode_t modulation; + uint8_t txOffsetFrequencyDirection; + uint8_t outputPower; + bool frequencyReverse; + uint8_t channelBandwidth; + uint8_t busyChannelLock; + uint8_t txLock; +#ifdef ENABLE_DTMF_CALLING + uint8_t dtmfDecodingEnable; +#endif + PTT_ID_t dtmfPttIdTxMode; +} ChannelScanDisplayInfo_t; + void SETTINGS_InitEEPROM(void); void SETTINGS_LoadCalibration(void); uint32_t SETTINGS_FetchChannelFrequency(const uint16_t channel); bool SETTINGS_FetchChannelScanInfo(const uint16_t channel, uint32_t *frequency, ModulationMode_t *modulation); +bool SETTINGS_FetchChannelScanDisplayInfo(const uint16_t channel, ChannelScanDisplayInfo_t *info); void SETTINGS_FetchChannelName(char *s, const uint16_t channel); void SETTINGS_FactoryReset(bool bIsAll); #ifdef ENABLE_FMRADIO diff --git a/App/ui/main.c b/App/ui/main.c index d650db40..7df0ccc4 100644 --- a/App/ui/main.c +++ b/App/ui/main.c @@ -1748,6 +1748,11 @@ void UI_DisplayMain(void) String[0] = '\0'; const VFO_Info_t *vfoInfo = &gEeprom.VfoInfo[vfo_num]; +#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER + const VFO_Info_t *scanDisplayVfo = CHFRSCANNER_GetScanDisplayVfo(); + if (vfo_num == gEeprom.RX_VFO && scanDisplayVfo != NULL) + vfoInfo = scanDisplayVfo; +#endif // show the modulation symbol const char * s = ""; @@ -1937,10 +1942,12 @@ void UI_DisplayMain(void) #endif #if ENABLE_FEAT_F4HWN + const uint8_t displayBandwidth = vfoInfo->CHANNEL_BANDWIDTH; + #ifdef ENABLE_FEAT_F4HWN_NARROWER bool narrower = 0; - if(vfoInfo->CHANNEL_BANDWIDTH == BANDWIDTH_NARROW && gSetting_set_nfm == 1) + if(displayBandwidth == BANDWIDTH_NARROW && gSetting_set_nfm == 1) { narrower = 1; } @@ -1948,23 +1955,23 @@ void UI_DisplayMain(void) if (gSetting_set_gui) { const char *bandWidthNames[] = {"W", "N", "N+"}; - UI_PrintStringSmallNormal(bandWidthNames[vfoInfo->CHANNEL_BANDWIDTH + narrower], LCD_WIDTH + 80, 0, line + 1); + UI_PrintStringSmallNormal(bandWidthNames[displayBandwidth + narrower], LCD_WIDTH + 80, 0, line + 1); } else { const char *bandWidthNames[] = {"WIDE", "NAR", "NAR+"}; - GUI_DisplaySmallest(bandWidthNames[vfoInfo->CHANNEL_BANDWIDTH + narrower], 91, line == 0 ? 17 : 49, false, true); + GUI_DisplaySmallest(bandWidthNames[displayBandwidth + narrower], 91, line == 0 ? 17 : 49, false, true); } #else if (gSetting_set_gui) { const char *bandWidthNames[] = {"W", "N"}; - UI_PrintStringSmallNormal(bandWidthNames[vfoInfo->CHANNEL_BANDWIDTH], LCD_WIDTH + 80, 0, line + 1); + UI_PrintStringSmallNormal(bandWidthNames[displayBandwidth], LCD_WIDTH + 80, 0, line + 1); } else { const char *bandWidthNames[] = {"WIDE", "NAR"}; - GUI_DisplaySmallest(bandWidthNames[vfoInfo->CHANNEL_BANDWIDTH], 91, line == 0 ? 17 : 49, false, true); + GUI_DisplaySmallest(bandWidthNames[displayBandwidth], 91, line == 0 ? 17 : 49, false, true); } #endif #else From 932d6e954a56f7d697eb1429d2ac70763c8c2921 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Mon, 4 May 2026 03:36:40 +0200 Subject: [PATCH 20/33] Add volatile scanrange exclusions --- App/app/chFrScanner.c | 152 ++++++++++++++++++++++++++++++++++++++++-- App/app/chFrScanner.h | 2 + App/app/main.c | 16 ++++- App/ui/main.c | 8 ++- 4 files changed, 168 insertions(+), 10 deletions(-) diff --git a/App/app/chFrScanner.c b/App/app/chFrScanner.c index 8518466a..35261248 100644 --- a/App/app/chFrScanner.c +++ b/App/app/chFrScanner.c @@ -20,6 +20,22 @@ bool gScanPauseMode; #ifdef ENABLE_SCAN_RANGES uint32_t gScanRangeStart; uint32_t gScanRangeStop; + +#define SCAN_RANGE_SKIP_MAX 32 +#if (SCAN_RANGE_SKIP_MAX & (SCAN_RANGE_SKIP_MAX - 1)) != 0 + #error SCAN_RANGE_SKIP_MAX must be a power of two +#endif + +typedef struct { + uint16_t sample[SCAN_RANGE_SKIP_MAX]; + uint32_t start; + uint32_t stop; + uint16_t step; + uint8_t count; + uint8_t next; +} ScanRangeSkipList_t; + +static ScanRangeSkipList_t scanRangeSkip; #endif typedef enum { @@ -43,6 +59,123 @@ uint8_t initialCROSS_BAND_RX_TX; static void NextFreqChannel(void); static void NextMemChannel(void); +#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER +static void ScanFastResetState(void); +#endif + +#ifdef ENABLE_SCAN_RANGES +static void ScanRangeSkipClear(void) +{ + scanRangeSkip.count = 0; + scanRangeSkip.next = 0; +} + +static void ScanRangeSkipSync(void) +{ + const uint16_t step = gRxVfo->StepFrequency; + + if (scanRangeSkip.start == gScanRangeStart && + scanRangeSkip.stop == gScanRangeStop && + scanRangeSkip.step == step) + { + return; + } + + ScanRangeSkipClear(); + scanRangeSkip.start = gScanRangeStart; + scanRangeSkip.stop = gScanRangeStop; + scanRangeSkip.step = step; +} + +static uint16_t ScanRangeSkipSampleForFrequency(uint32_t frequency) +{ + const uint16_t step = gRxVfo->StepFrequency; + if (!gScanRangeStart || step == 0 || frequency < gScanRangeStart || frequency > gScanRangeStop) + return 0; + + const uint32_t sample = (frequency - gScanRangeStart) / step; + if (sample >= 0xFFFFu) + return 0; + + return (uint16_t)(sample + 1); +} + +static bool ScanRangeSkipContainsFrequency(uint32_t frequency) +{ + const uint16_t sample = ScanRangeSkipSampleForFrequency(frequency); + if (sample == 0) + return false; + + for (uint8_t i = 0; i < scanRangeSkip.count; ++i) + if (scanRangeSkip.sample[i] == sample) + return true; + + return false; +} + +static uint32_t ScanRangeNextFrequency(void) +{ + uint32_t frequency; + uint8_t guard = SCAN_RANGE_SKIP_MAX + 1; + + ScanRangeSkipSync(); + + do + { + frequency = APP_SetFreqByStepAndLimits(gRxVfo, gScanStateDir, gScanRangeStart, gScanRangeStop); + gRxVfo->freq_config_RX.Frequency = frequency; + } while (--guard && ScanRangeSkipContainsFrequency(frequency)); + + return frequency; +} + +bool CHFRSCANNER_ExcludeCurrentScanRange(void) +{ + ScanRangeSkipSync(); + + uint16_t sample = ScanRangeSkipSampleForFrequency(lastFoundFrqOrChan); + if (sample == 0) + sample = ScanRangeSkipSampleForFrequency(gRxVfo->freq_config_RX.Frequency); + + if (sample == 0) + return false; + + for (uint8_t i = 0; i < scanRangeSkip.count; ++i) + if (scanRangeSkip.sample[i] == sample) + return true; + + scanRangeSkip.sample[scanRangeSkip.next] = sample; + scanRangeSkip.next = (scanRangeSkip.next + 1) & (SCAN_RANGE_SKIP_MAX - 1); + if (scanRangeSkip.count < SCAN_RANGE_SKIP_MAX) + scanRangeSkip.count++; + +#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER + ScanFastResetState(); +#endif + + return true; +} + +bool CHFRSCANNER_HasScanRangeExcludedOrdinal(uint32_t first_ordinal, uint32_t last_ordinal) +{ + if (first_ordinal == 0) + first_ordinal = 1; + if (last_ordinal < first_ordinal) + return false; + if (first_ordinal > 0xFFFFu) + return false; + if (last_ordinal > 0xFFFFu) + last_ordinal = 0xFFFFu; + + for (uint8_t i = 0; i < scanRangeSkip.count; ++i) { + const uint16_t sample = scanRangeSkip.sample[i]; + if (sample >= first_ordinal && sample <= last_ordinal) + return true; + } + + return false; +} +#endif static void CHFRSCANNER_AbortActiveReception(void) { @@ -333,8 +466,7 @@ static void ScanRangeFastRefineCandidate(uint16_t firstRssi) { const uint32_t prevRxFrequency = gRxVfo->pRX->Frequency; - gRxVfo->freq_config_RX.Frequency = - APP_SetFreqByStepAndLimits(gRxVfo, gScanStateDir, gScanRangeStart, gScanRangeStop); + gRxVfo->freq_config_RX.Frequency = ScanRangeNextFrequency(); RADIO_ApplyOffset(gRxVfo); const uint32_t freq = gRxVfo->pRX->Frequency; @@ -380,8 +512,7 @@ static scan_fast_result_t ScanRangeFastPrecheck(void) for (uint8_t i = 0; i < SCAN_FAST_PRECHECK_STEPS; ++i) { - gRxVfo->freq_config_RX.Frequency = - APP_SetFreqByStepAndLimits(gRxVfo, gScanStateDir, gScanRangeStart, gScanRangeStop); + gRxVfo->freq_config_RX.Frequency = ScanRangeNextFrequency(); RADIO_ApplyOffset(gRxVfo); const uint32_t freq = gRxVfo->pRX->Frequency; @@ -467,13 +598,20 @@ static void SetMemScanProgressChannel(uint16_t channel) #if defined(ENABLE_FEAT_F4HWN_RESUME_STATE) || defined(ENABLE_SCAN_RANGES) void CHFRSCANNER_ScanRange(void) { - gScanRangeStart = gScanRangeStart ? 0 : gTxVfo->pRX->Frequency; + if (gScanRangeStart) { + gScanRangeStart = 0; + return; + } + + gScanRangeStart = gTxVfo->pRX->Frequency; gScanRangeStop = gEeprom.VfoInfo[!gEeprom.TX_VFO].freq_config_RX.Frequency; #ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER ScanFastResetState(); #endif if(gScanRangeStart > gScanRangeStop) SWAP(gScanRangeStart, gScanRangeStop); + + ScanRangeSkipSync(); } #endif @@ -725,14 +863,14 @@ static void NextFreqChannel(void) if (fastResult == SCAN_FAST_DISABLED) { scanFastLastFullTuneCandidate = false; - gRxVfo->freq_config_RX.Frequency = APP_SetFreqByStepAndLimits(gRxVfo, gScanStateDir, gScanRangeStart, gScanRangeStop); + gRxVfo->freq_config_RX.Frequency = ScanRangeNextFrequency(); } else { scanFastLastFullTuneCandidate = true; } #else - gRxVfo->freq_config_RX.Frequency = APP_SetFreqByStepAndLimits(gRxVfo, gScanStateDir, gScanRangeStart, gScanRangeStop); + gRxVfo->freq_config_RX.Frequency = ScanRangeNextFrequency(); #endif } else diff --git a/App/app/chFrScanner.h b/App/app/chFrScanner.h index 26ca3ef9..264ccc73 100644 --- a/App/app/chFrScanner.h +++ b/App/app/chFrScanner.h @@ -15,6 +15,8 @@ extern bool gScanPauseMode; #ifdef ENABLE_SCAN_RANGES extern uint32_t gScanRangeStart; extern uint32_t gScanRangeStop; +bool CHFRSCANNER_ExcludeCurrentScanRange(void); +bool CHFRSCANNER_HasScanRangeExcludedOrdinal(uint32_t first_ordinal, uint32_t last_ordinal); #endif void CHFRSCANNER_Found(void); diff --git a/App/app/main.c b/App/app/main.c index 525112d9..a8823271 100644 --- a/App/app/main.c +++ b/App/app/main.c @@ -762,11 +762,25 @@ static void MAIN_Key_MENU(bool bKeyPressed, bool bKeyHeld) if (bKeyPressed) { // long press MENU key #ifdef ENABLE_FEAT_F4HWN - // Exclude channel + // Exclude current scan entry if(gScanStateDir != SCAN_OFF) { if(FUNCTION_IsRx() || gScanPauseDelayIn_10ms > 9) { +#ifdef ENABLE_SCAN_RANGES + if(gScanRangeStart && !IS_MR_CHANNEL(gNextMrChannel)) + { + if(CHFRSCANNER_ExcludeCurrentScanRange()) + { + UI_MAIN_NotifyScanProgressDataChanged(); + lastFoundFrqOrChan = lastFoundFrqOrChanOld; + CHFRSCANNER_ContinueScanning(); + } + + return; + } +#endif + ChannelAttributes_t *att = MR_GetChannelAttributes(lastFoundFrqOrChan); att->exclude = true; diff --git a/App/ui/main.c b/App/ui/main.c index 7df0ccc4..f759e4b5 100644 --- a/App/ui/main.c +++ b/App/ui/main.c @@ -221,7 +221,7 @@ static bool ScanProgress_BucketHasExcludedOrdinal(uint32_t first_ordinal, uint32 return false; } -static void ScanProgress_DrawGaugeLine(uint8_t line, uint32_t current_index, uint32_t total, uint8_t width, bool memory_mode, uint8_t extra_left_offset) +static void ScanProgress_DrawGaugeLine(uint8_t line, uint32_t current_index, uint32_t total, uint8_t width, bool memory_mode, bool range_mode, uint8_t extra_left_offset) { const bool forward = ScanProgress_IsForward(); @@ -260,6 +260,10 @@ static void ScanProgress_DrawGaugeLine(uint8_t line, uint32_t current_index, uin if (memory_mode) excluded = ScanProgress_BucketHasExcludedOrdinal(first_ordinal, last_ordinal); +#ifdef ENABLE_SCAN_RANGES + else if (range_mode) + excluded = CHFRSCANNER_HasScanRangeExcludedOrdinal(first_ordinal, last_ordinal); +#endif if (processed && !excluded) { pixel = 0x2d; @@ -502,7 +506,7 @@ static bool UI_DrawScanProgress(void) UI_PrintStringSmallNormal(text, 2, 0, line); #endif - ScanProgress_DrawGaugeLine(line, current_index, total, width, show_memory, extra_offset); + ScanProgress_DrawGaugeLine(line, current_index, total, width, show_memory, show_range, extra_offset); return true; } From c1d5b7bd556735f6c562309331802e40e7bb5974 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Mon, 4 May 2026 04:56:15 +0200 Subject: [PATCH 21/33] Add scan mode menu --- App/app/chFrScanner.c | 45 ++++++++++++++++++++++++++++--------------- App/app/menu.c | 16 +++++++++++++++ App/misc.c | 6 +++++- App/misc.h | 4 ++++ App/scheduler.c | 3 ++- App/settings.c | 14 ++++++++++++-- App/ui/menu.c | 17 ++++++++++++++++ App/ui/menu.h | 6 ++++++ 8 files changed, 92 insertions(+), 19 deletions(-) diff --git a/App/app/chFrScanner.c b/App/app/chFrScanner.c index 35261248..dc4442b8 100644 --- a/App/app/chFrScanner.c +++ b/App/app/chFrScanner.c @@ -63,6 +63,13 @@ static void NextMemChannel(void); static void ScanFastResetState(void); #endif +#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER +static bool ScanFastEnabled(void) +{ + return gSetting_set_scn; +} +#endif + #ifdef ENABLE_SCAN_RANGES static void ScanRangeSkipClear(void) { @@ -273,7 +280,7 @@ static void ScanFastTune(uint32_t frequency) const VFO_Info_t *CHFRSCANNER_GetScanDisplayVfo(void) { - if (!scanFastDisplayVfoValid || gScanStateDir == SCAN_OFF || FUNCTION_IsRx()) + if (!ScanFastEnabled() || !scanFastDisplayVfoValid || gScanStateDir == SCAN_OFF || FUNCTION_IsRx()) return NULL; return &scanFastDisplayVfo; @@ -850,24 +857,32 @@ static void NextFreqChannel(void) #ifdef ENABLE_SCAN_RANGES if(gScanRangeStart) { #ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER - const scan_fast_result_t fastResult = ScanRangeFastPrecheck(); - - if (fastResult == SCAN_FAST_QUIET_BATCH) + if (ScanFastEnabled()) { - scanFastLastFullTuneCandidate = false; - gScanPauseDelayIn_10ms = 1; - gUpdateDisplay = true; - return; - } + const scan_fast_result_t fastResult = ScanRangeFastPrecheck(); - if (fastResult == SCAN_FAST_DISABLED) - { - scanFastLastFullTuneCandidate = false; - gRxVfo->freq_config_RX.Frequency = ScanRangeNextFrequency(); + if (fastResult == SCAN_FAST_QUIET_BATCH) + { + scanFastLastFullTuneCandidate = false; + gScanPauseDelayIn_10ms = 1; + gUpdateDisplay = true; + return; + } + + if (fastResult == SCAN_FAST_DISABLED) + { + scanFastLastFullTuneCandidate = false; + gRxVfo->freq_config_RX.Frequency = ScanRangeNextFrequency(); + } + else + { + scanFastLastFullTuneCandidate = true; + } } else { - scanFastLastFullTuneCandidate = true; + scanFastLastFullTuneCandidate = false; + gRxVfo->freq_config_RX.Frequency = ScanRangeNextFrequency(); } #else gRxVfo->freq_config_RX.Frequency = ScanRangeNextFrequency(); @@ -1014,7 +1029,7 @@ static void NextMemChannel(void) #ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER SetMemScanProgressChannel(gNextMrChannel); - if (!MemChannelFastPrecheck(gNextMrChannel)) + if (ScanFastEnabled() && !MemChannelFastPrecheck(gNextMrChannel)) { gScanPauseDelayIn_10ms = 1; gUpdateDisplay = true; diff --git a/App/app/menu.c b/App/app/menu.c index 4de5b74d..55c1f7a2 100644 --- a/App/app/menu.c +++ b/App/app/menu.c @@ -426,6 +426,12 @@ int MENU_GetLimits(uint8_t menu_id, int32_t *pMin, int32_t *pMax) //*pMin = 0; *pMax = ARRAY_SIZE(gSubMenu_SET_MET) - 1; break; + #ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER + case MENU_SET_SCN: + //*pMin = 0; + *pMax = ARRAY_SIZE(gSubMenu_SET_SCN) - 1; + break; + #endif #ifdef ENABLE_FEAT_F4HWN_AUDIO case MENU_SET_AUD: //*pMin = 0; @@ -989,6 +995,11 @@ void MENU_AcceptSetting(void) case MENU_SET_GUI: gSetting_set_gui = gSubMenuSelection; break; + #ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER + case MENU_SET_SCN: + gSetting_set_scn = gSubMenuSelection; + break; + #endif #ifdef ENABLE_FEAT_F4HWN_AUDIO case MENU_SET_AUD: if(gTxVfo->Modulation == MODULATION_AM) @@ -1453,6 +1464,11 @@ void MENU_ShowCurrentSetting(void) case MENU_SET_GUI: gSubMenuSelection = gSetting_set_gui; break; + #ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER + case MENU_SET_SCN: + gSubMenuSelection = gSetting_set_scn; + break; + #endif #ifdef ENABLE_FEAT_F4HWN_AUDIO case MENU_SET_AUD: if(gTxVfo->Modulation == MODULATION_AM) diff --git a/App/misc.c b/App/misc.c index ee830a6a..de250535 100644 --- a/App/misc.c +++ b/App/misc.c @@ -116,6 +116,10 @@ enum BacklightOnRxTx_t gSetting_backlight_on_tx_rx; bool gWakeUp = false; #endif +#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER + bool gSetting_set_scn = 1; +#endif + #ifdef ENABLE_FEAT_F4HWN uint8_t gSetting_set_pwr = 1; bool gSetting_set_ptt = 0; @@ -649,4 +653,4 @@ void MR_PrintCacheStats(void) return false; } -#endif \ No newline at end of file +#endif diff --git a/App/misc.h b/App/misc.h index 52ee3336..688d6e0a 100644 --- a/App/misc.h +++ b/App/misc.h @@ -176,6 +176,10 @@ extern enum BacklightOnRxTx_t gSetting_backlight_on_tx_rx; extern bool gWakeUp; #endif +#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER + extern bool gSetting_set_scn; +#endif + #ifdef ENABLE_FEAT_F4HWN extern uint8_t gSetting_set_pwr; extern bool gSetting_set_ptt; diff --git a/App/scheduler.c b/App/scheduler.c index c43e05f9..0d2031a1 100644 --- a/App/scheduler.c +++ b/App/scheduler.c @@ -110,7 +110,8 @@ void SysTick_Handler(void) #define SCAN_FAST_STALL_WATCHDOG_10ms 25 // 250 ms static uint16_t scanFastStallCounter; - if (gScanStateDir != SCAN_OFF + if (gSetting_set_scn + && gScanStateDir != SCAN_OFF && gScanPauseDelayIn_10ms == 0 && !gScheduleScanListen && !g_SquelchLost diff --git a/App/settings.c b/App/settings.c index 6e993032..1782f92b 100644 --- a/App/settings.c +++ b/App/settings.c @@ -454,8 +454,12 @@ gEeprom.FreqChannel[1] = IS_FREQ_CHANNEL(Data16[5]) ? Data16[5] : (FREQ_CHANNE // 1FF0..0x1FF7 // TODO: address TBD PY25Q16_ReadBuffer(0x00A158, Data, 8); + const uint8_t set_ptt_scn = Data[7] & 0x0F; gSetting_set_pwr = (((Data[7] & 0xF0) >> 4) < 7) ? ((Data[7] & 0xF0) >> 4) : 0; - gSetting_set_ptt = (((Data[7] & 0x0F)) < 2) ? ((Data[7] & 0x0F)) : 0; + gSetting_set_ptt = (set_ptt_scn < 4) ? (set_ptt_scn & 0x01) : 0; +#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER + gSetting_set_scn = (set_ptt_scn < 4) ? ((set_ptt_scn & 0x02) == 0) : 1; +#endif gSetting_set_tot = (((Data[6] & 0xF0) >> 4) < 4) ? ((Data[6] & 0xF0) >> 4) : 0; gSetting_set_eot = (((Data[6] & 0x0F)) < 4) ? ((Data[6] & 0x0F)) : 0; @@ -1127,7 +1131,13 @@ void SETTINGS_SaveSettings(void) State[5] = ((tmp << 4) | (gSetting_set_ctr & 0x0F)); State[6] = ((gSetting_set_tot << 4) | (gSetting_set_eot & 0x0F)); - State[7] = ((gSetting_set_pwr << 4) | (gSetting_set_ptt & 0x0F)); + uint8_t set_ptt_scn = gSetting_set_ptt & 0x01; +#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER + if (!gSetting_set_scn) + set_ptt_scn |= 0x02; +#endif + + State[7] = ((gSetting_set_pwr << 4) | set_ptt_scn); gEeprom.KEY_LOCK_PTT = gSetting_set_lck; diff --git a/App/ui/menu.c b/App/ui/menu.c index 8fe7c088..38d74b1e 100644 --- a/App/ui/menu.c +++ b/App/ui/menu.c @@ -168,6 +168,9 @@ const t_menu_item MenuList[] = #ifdef ENABLE_NOAA {"SetNWR", MENU_NOAA_S }, #endif +#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER + {"SetScn", MENU_SET_SCN }, +#endif #endif // hidden menu items from here on // enabled if pressing both the PTT and upper side button at power-on @@ -414,6 +417,14 @@ const char gSubMenu_SCRAMBLER[][7] = "CLASSIC" }; + #ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER + const char gSubMenu_SET_SCN[][7] = + { + "NORMAL", + "FAST" + }; + #endif + #ifdef ENABLE_FEAT_F4HWN_AUDIO const char gSubMenu_SET_AUD_FM[][6] = { @@ -1350,6 +1361,12 @@ void UI_DisplayMenu(void) strcpy(String, gSubMenu_SET_MET[gSubMenuSelection]); // Same as SET_MET break; + #ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER + case MENU_SET_SCN: + strcpy(String, gSubMenu_SET_SCN[gSubMenuSelection]); + break; + #endif + #ifdef ENABLE_FEAT_F4HWN_AUDIO case MENU_SET_AUD: if(gTxVfo->Modulation == MODULATION_AM) { diff --git a/App/ui/menu.h b/App/ui/menu.h index 18adae4f..0ae007af 100644 --- a/App/ui/menu.h +++ b/App/ui/menu.h @@ -137,6 +137,9 @@ enum MENU_SET_MET, MENU_SET_GUI, MENU_SET_TMR, + #ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER + MENU_SET_SCN, + #endif #ifdef ENABLE_FEAT_F4HWN_NARROWER MENU_SET_NFM, #endif @@ -191,6 +194,9 @@ extern const char gSubMenu_D_RSP[4][11]; extern const char gSubMenu_SET_TOT[4][7]; extern const char gSubMenu_SET_LCK[2][9]; extern const char gSubMenu_SET_MET[2][8]; + #ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER + extern const char gSubMenu_SET_SCN[2][7]; + #endif #ifdef ENABLE_FEAT_F4HWN_NARROWER extern const char gSubMenu_SET_NFM[2][9]; #endif From c0c1d9881c7b01f4646ae0429d4bf0c33ca58688 Mon Sep 17 00:00:00 2001 From: mrkusypl Date: Mon, 4 May 2026 12:38:43 +0200 Subject: [PATCH 22/33] Displaying VFO_Lock on the second channel while receiving on the first; Code refactoring (42 B) --- App/app/action.c | 1 - App/app/chFrScanner.c | 1 - App/app/main.c | 3 --- App/ui/main.c | 15 +++++++-------- 4 files changed, 7 insertions(+), 13 deletions(-) diff --git a/App/app/action.c b/App/app/action.c index 1b925bf9..680fe4a1 100644 --- a/App/app/action.c +++ b/App/app/action.c @@ -239,7 +239,6 @@ void ACTION_Scan(bool bRestart) // channel mode. Keep scanning but toggle between scan lists RADIO_NextValidList(1); UI_MAIN_NotifyScanProgressDataChanged(); - gUpdateStatus = true; #ifdef ENABLE_FEAT_F4HWN_RESUME_STATE SETTINGS_WriteCurrentState(); diff --git a/App/app/chFrScanner.c b/App/app/chFrScanner.c index dc4442b8..7206ee7e 100644 --- a/App/app/chFrScanner.c +++ b/App/app/chFrScanner.c @@ -646,7 +646,6 @@ void CHFRSCANNER_Start(const bool storeBackupSettings, const int8_t scan_directi if(!RADIO_CheckValidList(gEeprom.SCAN_LIST_DEFAULT)) { RADIO_NextValidList(1); UI_MAIN_NotifyScanProgressDataChanged(); - gUpdateStatus = true; } // channel mode diff --git a/App/app/main.c b/App/app/main.c index a8823271..a67bee90 100644 --- a/App/app/main.c +++ b/App/app/main.c @@ -301,7 +301,6 @@ static void processFKeyFunction(const KEY_Code_t Key, const bool beep) if (gScanStateDir != SCAN_OFF) { RADIO_NextValidList(isKeyUp ? 1 : -1); UI_MAIN_NotifyScanProgressDataChanged(); - gUpdateStatus = true; } else { // Adjust squelch: UP increments, DOWN decrements if (gSquelchLevelOriginal == 10) @@ -485,7 +484,6 @@ static void MAIN_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) { gEeprom.SCAN_LIST_DEFAULT = MR_CHANNELS_LIST + 1; UI_MAIN_NotifyScanProgressDataChanged(); - gUpdateStatus = true; #ifdef ENABLE_FEAT_F4HWN_RESUME_STATE SETTINGS_WriteCurrentState(); #endif @@ -505,7 +503,6 @@ static void MAIN_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) RADIO_NextValidList(1); } UI_MAIN_NotifyScanProgressDataChanged(); - gUpdateStatus = true; #ifdef ENABLE_FEAT_F4HWN_RESUME_STATE SETTINGS_WriteCurrentState(); diff --git a/App/ui/main.c b/App/ui/main.c index f759e4b5..a6660886 100644 --- a/App/ui/main.c +++ b/App/ui/main.c @@ -108,6 +108,7 @@ static void ScanProgress_ResetSession(void) void UI_MAIN_NotifyScanProgressDataChanged(void) { gScanProgressForceRebuild = true; + gUpdateStatus = true; } static inline void ScanProgress_SetBit(uint8_t *map, uint16_t ch) @@ -1302,14 +1303,6 @@ void UI_DisplayMain(void) uint32_t frequency = gEeprom.VfoInfo[vfo_num].pRX->Frequency; - if(TX_freq_check(frequency) != 0 && gEeprom.VfoInfo[vfo_num].TX_LOCK == true && !FUNCTION_IsRx()) - { - if(isMainOnly()) - memcpy(p_line0 + 25, BITMAP_VFO_Lock, sizeof(BITMAP_VFO_Lock)); - else - memcpy(p_line0 + 25, BITMAP_VFO_Lock, sizeof(BITMAP_VFO_Lock)); - } - if (gCurrentFunction == FUNCTION_TRANSMIT) { // transmitting @@ -1393,6 +1386,12 @@ void UI_DisplayMain(void) #endif } + if(TX_freq_check(frequency) != 0 && gEeprom.VfoInfo[vfo_num].TX_LOCK == true) + { + if (!FUNCTION_IsRx() || RxOnVfofrequency != frequency) + memcpy(p_line0 + 25, BITMAP_VFO_Lock, sizeof(BITMAP_VFO_Lock)); + } + if (IS_MR_CHANNEL(gEeprom.ScreenChannel[vfo_num])) { // channel mode const unsigned int x = 1; From 59ed9cd8279b5b9a778bc44b756b32823bdca214 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Mon, 4 May 2026 22:03:53 +0200 Subject: [PATCH 23/33] Fix display bug (overlapping) --- App/ui/main.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/App/ui/main.c b/App/ui/main.c index a6660886..8c2c42a5 100644 --- a/App/ui/main.c +++ b/App/ui/main.c @@ -1204,7 +1204,7 @@ void UI_DisplayMain(void) shift = 3; } - UI_PrintString("ScnRng", 5, 0, line + shift, 8); + UI_PrintString("ScnRng", 7, 0, line + shift, 8); UI_FormatFrequency(gScanRangeStart, String); UI_PrintStringSmallNormal(String, 56, 0, line + shift); UI_FormatFrequency(gScanRangeStop, String); @@ -1218,7 +1218,7 @@ void UI_DisplayMain(void) gScanRangeStart = 0; } #else - UI_PrintString("ScnRng", 5, 0, line, 8); + UI_PrintString("ScnRng", 7, 0, line, 8); UI_FormatFrequency(gScanRangeStart, String); UI_PrintStringSmallNormal(String, 56, 0, line); UI_FormatFrequency(gScanRangeStop, String); From b8936ace0e86aaa32bbba6da1c600fb0be1b337a Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Tue, 5 May 2026 04:35:03 +0200 Subject: [PATCH 24/33] Add beam feature behind ENABLE_FEAT_F4HWN_BEAM flag --- App/CMakeLists.txt | 9 +- App/app/action.c | 6 + App/app/aircopy.c | 8 +- App/app/aircopy.h | 4 + App/app/app.c | 68 ++++++++- App/app/beam.c | 363 +++++++++++++++++++++++++++++++++++++++++++++ App/app/beam.h | 53 +++++++ App/settings.h | 3 + App/ui/main.c | 52 +++++++ App/ui/main.h | 5 +- App/ui/menu.c | 3 + CMakePresets.json | 2 + 12 files changed, 562 insertions(+), 14 deletions(-) create mode 100644 App/app/beam.c create mode 100644 App/app/beam.h diff --git a/App/CMakeLists.txt b/App/CMakeLists.txt index 66a5dd0e..44fd71ed 100644 --- a/App/CMakeLists.txt +++ b/App/CMakeLists.txt @@ -123,7 +123,7 @@ enable_feature(ENABLE_FMRADIO ui/fmradio.c ) enable_feature(ENABLE_AIRCOPY - app/aircopy.c + app/aircopy.c ui/aircopy.c ) enable_feature(ENABLE_NOAA) @@ -206,6 +206,13 @@ enable_feature(ENABLE_FEAT_F4HWN_GMRS_FRS_MURS) enable_feature(ENABLE_FEAT_F4HWN_CA) enable_feature(ENABLE_FEAT_F4HWN_DEBUG) enable_feature(ENABLE_FEAT_F4HWN_MEM) +enable_feature(ENABLE_FEAT_F4HWN_BEAM + app/beam.c +) + +if(ENABLE_FEAT_F4HWN_BEAM AND NOT ENABLE_AIRCOPY) + message(FATAL_ERROR "ENABLE_FEAT_F4HWN_BEAM requires ENABLE_AIRCOPY (it reuses g_FSK_Buffer, AIRCOPY_Obfuscate and the FSK packet plumbing).") +endif() enable_feature(ENABLE_FEAT_F4HWN_QRCODE) # ---- DEBUGGING ---- diff --git a/App/app/action.c b/App/app/action.c index 680fe4a1..d399023b 100644 --- a/App/app/action.c +++ b/App/app/action.c @@ -45,6 +45,9 @@ #ifdef ENABLE_REGA #include "app/rega.h" #endif +#ifdef ENABLE_FEAT_F4HWN_BEAM + #include "app/beam.h" +#endif #if defined(ENABLE_FMRADIO) static void ACTION_Scan_FM(bool bRestart); @@ -132,6 +135,9 @@ void (*action_opt_table[])(void) = { [ACTION_OPT_REGA_ALARM] = &ACTION_RegaAlarm, [ACTION_OPT_REGA_TEST] = &ACTION_RegaTest, #endif +#ifdef ENABLE_FEAT_F4HWN_BEAM + [ACTION_OPT_BEAM] = &ACTION_Beam, +#endif }; static_assert(ARRAY_SIZE(action_opt_table) == ACTION_OPT_LEN); diff --git a/App/app/aircopy.c b/App/app/aircopy.c index 97d85839..ea8973d9 100644 --- a/App/app/aircopy.c +++ b/App/app/aircopy.c @@ -172,9 +172,9 @@ static inline void AIRCOPY_CheckComplete(void) } } -static inline void AIRCOPY_Obfuscation(void) +void AIRCOPY_Obfuscate(unsigned int count) { - for (unsigned int i = 0; i < 34; i++) { + for (unsigned int i = 0; i < count; i++) { g_FSK_Buffer[i + 1] ^= Obfuscation[i % 8]; } } @@ -230,7 +230,7 @@ bool AIRCOPY_SendMessage(void) g_FSK_Buffer[34] = CRC_Calculate(&g_FSK_Buffer[1], 2 + 64); - AIRCOPY_Obfuscation(); + AIRCOPY_Obfuscate(34); RADIO_SetTxParameters(); @@ -266,7 +266,7 @@ void AIRCOPY_StorePacket(void) return; } - AIRCOPY_Obfuscation(); + AIRCOPY_Obfuscate(34); uint16_t Crc = CRC_Calculate(&g_FSK_Buffer[1], 2 + 64); if (g_FSK_Buffer[34] != Crc) { diff --git a/App/app/aircopy.h b/App/app/aircopy.h index ce2a4032..f84f22d0 100644 --- a/App/app/aircopy.h +++ b/App/app/aircopy.h @@ -106,5 +106,9 @@ void AIRCOPY_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld); const AIRCOPY_TransferMap_t* AIRCOPY_GetCurrentMap(void); +// XOR-obfuscate `count` words of g_FSK_Buffer starting at index 1. +// Self-inverse: applying twice restores the original buffer. +void AIRCOPY_Obfuscate(unsigned int count); + #endif // ENABLE_AIRCOPY #endif // APP_AIRCOPY_H diff --git a/App/app/app.c b/App/app/app.c index 043f23b8..f404782e 100644 --- a/App/app/app.c +++ b/App/app/app.c @@ -24,6 +24,9 @@ #ifdef ENABLE_AIRCOPY #include "app/aircopy.h" #endif +#ifdef ENABLE_FEAT_F4HWN_BEAM + #include "app/beam.h" +#endif #include "app/app.h" #include "app/chFrScanner.h" #include "app/dtmf.h" @@ -796,6 +799,25 @@ static void CheckRadioInterrupts(void) AIRCOPY_StorePacket(); } #endif + +#ifdef ENABLE_FEAT_F4HWN_BEAM + if ((interrupts.fskFifoAlmostFull || interrupts.fskRxFinied) && + gBeamActive && + gBeamMode == BEAM_MODE_RX && + (gBeamStatus == BEAM_STATUS_RX_WAIT || gBeamStatus == BEAM_STATUS_ERROR)) + { + const unsigned int wordsToRead = interrupts.fskRxFinied ? (36 - gFSKWriteIndex) : 4; + for (unsigned int i = 0; i < wordsToRead; i++) { + const uint16_t word = BK4819_ReadRegister(BK4819_REG_5F); + if (gFSKWriteIndex < 36) + g_FSK_Buffer[gFSKWriteIndex++] = word; + } + + gBeamRxWordCount = gFSKWriteIndex; + gUpdateDisplay = true; + BEAM_StorePacket(); + } +#endif } } @@ -1044,6 +1066,9 @@ void APP_Update(void) && gScanStateDir == SCAN_OFF && !gPttIsPressed && gCurrentFunction != FUNCTION_POWER_SAVE +#ifdef ENABLE_FEAT_F4HWN_BEAM + && !gBeamActive +#endif #ifdef ENABLE_VOICE && gVoiceWriteIndex == 0 #endif @@ -1122,7 +1147,11 @@ void APP_Update(void) if (gEeprom.DUAL_WATCH != DUAL_WATCH_OFF && gScanStateDir == SCAN_OFF && - !gCssBackgroundScan) + !gCssBackgroundScan +#ifdef ENABLE_FEAT_F4HWN_BEAM + && !gBeamActive +#endif + ) { // dual watch mode, toggle between the two VFO's DualwatchAlternate(); goToSleep = false; @@ -1133,7 +1162,11 @@ void APP_Update(void) gPowerSave_10ms = power_save1_10ms; // come back here in a bit gRxIdleMode = false; // RX is awake } - else if (gEeprom.DUAL_WATCH == DUAL_WATCH_OFF || gScanStateDir != SCAN_OFF || gCssBackgroundScan || goToSleep) + else if ( +#ifdef ENABLE_FEAT_F4HWN_BEAM + !gBeamActive && +#endif + (gEeprom.DUAL_WATCH == DUAL_WATCH_OFF || gScanStateDir != SCAN_OFF || gCssBackgroundScan || goToSleep)) { // dual watch mode off or scanning or rssi update request // go back to sleep @@ -1152,7 +1185,11 @@ void APP_Update(void) // Authentic device checked removed } - else { + else +#ifdef ENABLE_FEAT_F4HWN_BEAM + if (!gBeamActive) +#endif + { // toggle between the two VFO's DualwatchAlternate(); gPowerSave_10ms = power_save1_10ms; @@ -1608,11 +1645,14 @@ void APP_TimeSlice500ms(void) gWakeUp = false; } + if(gCurrentFunction != FUNCTION_TRANSMIT && !FUNCTION_IsRx() #ifdef ENABLE_AIRCOPY - if(gCurrentFunction != FUNCTION_TRANSMIT && !FUNCTION_IsRx() && gScreenToDisplay != DISPLAY_AIRCOPY) - #else - if(gCurrentFunction != FUNCTION_TRANSMIT && !FUNCTION_IsRx()) + && gScreenToDisplay != DISPLAY_AIRCOPY #endif + #ifdef ENABLE_FEAT_F4HWN_BEAM + && !gBeamActive + #endif + ) { if (gSleepModeCountdown_500ms > 0 && --gSleepModeCountdown_500ms == 0) { gBacklightCountdown_500ms = 0; @@ -1698,6 +1738,9 @@ void APP_TimeSlice500ms(void) #endif #ifdef ENABLE_AIRCOPY && gScreenToDisplay != DISPLAY_AIRCOPY +#endif +#ifdef ENABLE_FEAT_F4HWN_BEAM + && !gBeamActive #endif ) { if (gEeprom.AUTO_KEYPAD_LOCK && gKeyLockCountdown > 0 && !gDTMF_InputMode @@ -2104,12 +2147,21 @@ static void ProcessKey(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) } #endif } - else if (gScreenToDisplay != DISPLAY_INVALID && ( + else if ( +#ifdef ENABLE_FEAT_F4HWN_BEAM + gBeamActive || +#endif + (gScreenToDisplay != DISPLAY_INVALID && ( (Key != KEY_SIDE1 && Key != KEY_SIDE2) #ifdef ENABLE_FEAT_F4HWN // For F + SIDE1 or F + SIDE2 || (gWasFKeyPressed && (Key == KEY_SIDE1 || Key == KEY_SIDE2)) #endif - )) { + ))) { +#ifdef ENABLE_FEAT_F4HWN_BEAM + if (gBeamActive) + BEAM_ProcessKeys(Key, bKeyPressed, bKeyHeld); + else +#endif ProcessKeysFunctions[gScreenToDisplay](Key, bKeyPressed, bKeyHeld); } else if (!SCANNER_IsScanning() diff --git a/App/app/beam.c b/App/app/beam.c new file mode 100644 index 00000000..08b9d169 --- /dev/null +++ b/App/app/beam.c @@ -0,0 +1,363 @@ +/* Copyright 2026 + * + * Licensed under the Apache License, Version 2.0. + */ + +#ifdef ENABLE_FEAT_F4HWN_BEAM + +#include +#include + +#include "app/aircopy.h" +#include "app/beam.h" +#include "audio.h" +#include "driver/bk4819.h" +#include "driver/crc.h" +#include "driver/st7565.h" +#include "frequencies.h" +#include "misc.h" +#include "radio.h" +#include "settings.h" +#include "ui/main.h" +#include "ui/ui.h" + +#define BEAM_PACKET_MAGIC 0xBEA5u +#define BEAM_PACKET_VERSION 2u + +// One byte per field instead of bit-packing — payload still fits in 64 bytes, +// and avoids costly shift/mask/store sequences on Cortex-M. +typedef struct { + uint16_t magic; + uint8_t version; + uint8_t _pad; + uint32_t rx_frequency; + uint32_t tx_offset_frequency; + uint8_t rx_code; + uint8_t tx_code; + uint8_t rx_codetype; + uint8_t tx_codetype; + uint8_t modulation; + uint8_t tx_offset_direction; + uint8_t tx_lock; + uint8_t busy_channel_lock; + uint8_t output_power; + uint8_t channel_bandwidth; + uint8_t frequency_reverse; + uint8_t dtmf_ptt_id_mode; + uint8_t dtmf_decoding_enable; + uint8_t step_setting; + uint8_t scrambling_type; + uint8_t band; + uint8_t scanlist; + uint8_t compander; + char name[16]; +} BEAM_Payload_t; + +static_assert(sizeof(BEAM_Payload_t) <= 64); + +BEAM_Mode_t gBeamMode = BEAM_MODE_TX; +BEAM_Status_t gBeamStatus = BEAM_STATUS_READY; +uint16_t gBeamCopiedChannel = 0xFFFFu; +uint8_t gBeamRxWordCount; +bool gBeamActive; + +static VFO_Info_t gBeamBackupVfo; +static VFO_Info_t gBeamRadioVfo; +static uint16_t gBeamBackupScreenChannel; +static uint8_t gBeamVfoIndex; +static uint8_t gBeamBackupRxVfoIndex; +static bool gBeamHasBackup; + +static void BEAM_SaveBackup(void) +{ + gBeamVfoIndex = gEeprom.TX_VFO; + memcpy(&gBeamBackupVfo, &gEeprom.VfoInfo[gBeamVfoIndex], sizeof(gBeamBackupVfo)); + gBeamBackupScreenChannel = gEeprom.ScreenChannel[gBeamVfoIndex]; + gBeamBackupRxVfoIndex = gEeprom.RX_VFO; + gBeamHasBackup = true; +} + +static void BEAM_RestoreBackup(void) +{ + if (!gBeamHasBackup) + return; + + memcpy(&gEeprom.VfoInfo[gBeamVfoIndex], &gBeamBackupVfo, sizeof(gBeamBackupVfo)); + gEeprom.ScreenChannel[gBeamVfoIndex] = gBeamBackupScreenChannel; + gEeprom.RX_VFO = gBeamBackupRxVfoIndex; + RADIO_SelectVfos(); + RADIO_SetupRegisters(true); + gBeamHasBackup = false; +} + +static void BEAM_SetRadioToBeamFrequency(void) +{ + const uint16_t channel = FREQ_CHANNEL_FIRST + BAND6_400MHz; + + RADIO_InitInfo(&gBeamRadioVfo, channel, DEFAULT_FREQ); + gBeamRadioVfo.CHANNEL_BANDWIDTH = BANDWIDTH_NARROW; + gBeamRadioVfo.OUTPUT_POWER = OUTPUT_POWER_LOW1; + RADIO_ConfigureSquelchAndOutputPower(&gBeamRadioVfo); + + gRxVfo = &gBeamRadioVfo; + gTxVfo = &gBeamRadioVfo; + gCurrentVfo = gRxVfo; + RADIO_SetupRegisters(true); + BK4819_SetupAircopy(); + BK4819_ResetFSK(); +} + +static void BEAM_CopyNameFromActiveVfo(char *name) +{ + memset(name, 0, 16); + + if (IS_MR_CHANNEL(gBeamBackupVfo.CHANNEL_SAVE)) { + SETTINGS_FetchChannelName(name, gBeamBackupVfo.CHANNEL_SAVE); + } else { + memcpy(name, gBeamBackupVfo.Name, sizeof(gBeamBackupVfo.Name)); + } +} + +static void BEAM_FillPayload(BEAM_Payload_t *payload) +{ + memset(payload, 0, sizeof(*payload)); + + payload->magic = BEAM_PACKET_MAGIC; + payload->version = BEAM_PACKET_VERSION; + payload->rx_frequency = gBeamBackupVfo.freq_config_RX.Frequency; + payload->tx_offset_frequency = gBeamBackupVfo.TX_OFFSET_FREQUENCY; + payload->rx_code = gBeamBackupVfo.freq_config_RX.Code; + payload->tx_code = gBeamBackupVfo.freq_config_TX.Code; + payload->rx_codetype = gBeamBackupVfo.freq_config_RX.CodeType; + payload->tx_codetype = gBeamBackupVfo.freq_config_TX.CodeType; + payload->modulation = gBeamBackupVfo.Modulation; + payload->tx_offset_direction = gBeamBackupVfo.TX_OFFSET_FREQUENCY_DIRECTION; + payload->tx_lock = gBeamBackupVfo.TX_LOCK; + payload->busy_channel_lock = gBeamBackupVfo.BUSY_CHANNEL_LOCK; + payload->output_power = gBeamBackupVfo.OUTPUT_POWER; + payload->channel_bandwidth = gBeamBackupVfo.CHANNEL_BANDWIDTH; + payload->frequency_reverse = gBeamBackupVfo.FrequencyReverse; + payload->dtmf_ptt_id_mode = gBeamBackupVfo.DTMF_PTT_ID_TX_MODE; +#ifdef ENABLE_DTMF_CALLING + payload->dtmf_decoding_enable = gBeamBackupVfo.DTMF_DECODING_ENABLE; +#endif + payload->step_setting = gBeamBackupVfo.STEP_SETTING; + payload->scrambling_type = gBeamBackupVfo.SCRAMBLING_TYPE; + payload->band = gBeamBackupVfo.Band; + payload->scanlist = gBeamBackupVfo.SCANLIST_PARTICIPATION; + payload->compander = gBeamBackupVfo.Compander; + + BEAM_CopyNameFromActiveVfo(payload->name); +} + +static void BEAM_SendPacket(void) +{ + BEAM_Payload_t payload; + + BEAM_FillPayload(&payload); + + memset(g_FSK_Buffer, 0, sizeof(g_FSK_Buffer)); + g_FSK_Buffer[0] = 0xABCDu; + memcpy(&g_FSK_Buffer[2], &payload, sizeof(payload)); + g_FSK_Buffer[34] = CRC_Calculate(&g_FSK_Buffer[1], 2 + 64); + g_FSK_Buffer[35] = 0xDCBAu; + + AIRCOPY_Obfuscate(32); + + // Show "SENDING" before the blocking FSK transmission so the user gets + // immediate visual feedback instead of a frozen "BEAM TX" screen. + gBeamStatus = BEAM_STATUS_TX_WAIT; + UI_DisplayMain(); + ST7565_BlitFullScreen(); + + RADIO_SetTxParameters(); + BK4819_SendFSKData(g_FSK_Buffer); + BK4819_SetupPowerAmplifier(0, 0); + BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, false); + + RADIO_SelectVfos(); + RADIO_SetupRegisters(true); + gBeamStatus = BEAM_STATUS_TX_DONE; + gBeepToPlay = BEEP_880HZ_60MS_TRIPLE_BEEP; + gUpdateDisplay = true; +} + +static uint16_t BEAM_FindFirstFreeChannel(void) +{ + for (uint16_t channel = MR_CHANNEL_FIRST; IS_MR_CHANNEL(channel); channel++) { + if (!RADIO_CheckValidChannel(channel, false, 0)) + return channel; + } + + return 0xFFFFu; +} + +static bool BEAM_SavePayloadToFirstFreeChannel(const BEAM_Payload_t *payload) +{ + uint16_t channel = BEAM_FindFirstFreeChannel(); + char name[16]; + + if (channel == 0xFFFFu) { + gBeamStatus = BEAM_STATUS_RX_FULL; + gUpdateDisplay = true; + return false; + } + + memcpy(name, payload->name, sizeof(name)); + name[sizeof(name) - 1] = 0; + + VFO_Info_t vfo; + RADIO_InitInfo(&vfo, channel, payload->rx_frequency); + + // CRC + magic + version already validate the payload — no need to clamp fields. + vfo.TX_OFFSET_FREQUENCY = payload->tx_offset_frequency; + vfo.freq_config_RX.Code = payload->rx_code; + vfo.freq_config_TX.Code = payload->tx_code; + vfo.freq_config_RX.CodeType = payload->rx_codetype; + vfo.freq_config_TX.CodeType = payload->tx_codetype; + vfo.TX_OFFSET_FREQUENCY_DIRECTION = payload->tx_offset_direction; + vfo.Modulation = payload->modulation; + vfo.TX_LOCK = payload->tx_lock; + vfo.BUSY_CHANNEL_LOCK = payload->busy_channel_lock; + vfo.OUTPUT_POWER = payload->output_power; + vfo.CHANNEL_BANDWIDTH = payload->channel_bandwidth; + vfo.FrequencyReverse = payload->frequency_reverse; + vfo.DTMF_PTT_ID_TX_MODE = payload->dtmf_ptt_id_mode; +#ifdef ENABLE_DTMF_CALLING + vfo.DTMF_DECODING_ENABLE = payload->dtmf_decoding_enable; +#endif + vfo.STEP_SETTING = payload->step_setting; + vfo.StepFrequency = gStepFrequencyTable[vfo.STEP_SETTING]; + vfo.SCRAMBLING_TYPE = payload->scrambling_type; + vfo.Band = payload->band; + vfo.SCANLIST_PARTICIPATION = payload->scanlist; + vfo.Compander = payload->compander; + memcpy(vfo.Name, name, sizeof(vfo.Name)); + RADIO_ApplyOffset(&vfo); + RADIO_ConfigureSquelchAndOutputPower(&vfo); + + SETTINGS_SaveChannel(channel, gBeamVfoIndex, &vfo, 3); + SETTINGS_SaveChannelName(channel, name); + + gBeamCopiedChannel = channel; + gBeamStatus = BEAM_STATUS_RX_SAVED; + gUpdateDisplay = true; + return true; +} + +static void BEAM_KeyMenu(void) +{ + BEAM_SetRadioToBeamFrequency(); + + if (gBeamMode == BEAM_MODE_TX) { + BEAM_SendPacket(); + } else { + gBeamStatus = BEAM_STATUS_RX_WAIT; + gBeamCopiedChannel = 0xFFFFu; + gBeamRxWordCount = 0; + gFSKWriteIndex = 0; + BK4819_PrepareFSKReceive(); + } +} + +static void BEAM_KeyExit(void) +{ + BK4819_ResetFSK(); + + if (gBeamMode == BEAM_MODE_RX && gBeamCopiedChannel != 0xFFFFu) { + gEeprom.MrChannel[gBeamVfoIndex] = gBeamCopiedChannel; + gEeprom.ScreenChannel[gBeamVfoIndex] = gBeamCopiedChannel; + RADIO_ConfigureChannel(gBeamVfoIndex, VFO_CONFIGURE_RELOAD); + RADIO_SelectVfos(); + RADIO_SetupRegisters(true); + gBeamHasBackup = false; + } else { + BEAM_RestoreBackup(); + } + + GUI_SelectNextDisplay(DISPLAY_MAIN); + gRequestDisplayScreen = DISPLAY_MAIN; + gBeamActive = false; + gUpdateDisplay = true; +} + +void ACTION_Beam(void) +{ + BEAM_SaveBackup(); + gBeamMode = BEAM_MODE_TX; + gBeamStatus = BEAM_STATUS_READY; + gBeamCopiedChannel = 0xFFFFu; + gBeamRxWordCount = 0; + gBeamActive = true; + GUI_SelectNextDisplay(DISPLAY_MAIN); + gRequestDisplayScreen = DISPLAY_MAIN; + gUpdateDisplay = true; +} + +void BEAM_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_UP: + case KEY_DOWN: + gBeamMode = (gBeamMode == BEAM_MODE_TX) ? BEAM_MODE_RX : BEAM_MODE_TX; + gBeamStatus = BEAM_STATUS_READY; + gBeamRxWordCount = 0; + break; + case KEY_MENU: + BEAM_KeyMenu(); + break; + case KEY_EXIT: + BEAM_KeyExit(); + return; + case KEY_PTT: + break; + default: + gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL; + break; + } + + gUpdateDisplay = true; +} + +void BEAM_StorePacket(void) +{ + if (gFSKWriteIndex < 36) + return; + + gBeamRxWordCount = gFSKWriteIndex; + gFSKWriteIndex = 0; + + const uint16_t Status = BK4819_ReadRegister(BK4819_REG_0B); + BK4819_PrepareFSKReceive(); + + if ((Status & 0x0010U) != 0 || g_FSK_Buffer[0] != 0xABCDu || g_FSK_Buffer[35] != 0xDCBAu) + goto error; + + AIRCOPY_Obfuscate(32); + + if (g_FSK_Buffer[34] != CRC_Calculate(&g_FSK_Buffer[1], 2 + 64)) + goto error; + + BEAM_Payload_t payload; + memcpy(&payload, &g_FSK_Buffer[2], sizeof(payload)); + + if (payload.magic != BEAM_PACKET_MAGIC || payload.version != BEAM_PACKET_VERSION) + goto error; + + BEAM_SavePayloadToFirstFreeChannel(&payload); + BK4819_ResetFSK(); + return; + +error: + gBeamStatus = BEAM_STATUS_ERROR; + gBeamRxWordCount = 0; + gUpdateDisplay = true; +} + +#endif // ENABLE_FEAT_F4HWN_BEAM diff --git a/App/app/beam.h b/App/app/beam.h new file mode 100644 index 00000000..6571119e --- /dev/null +++ b/App/app/beam.h @@ -0,0 +1,53 @@ +/* Copyright 2026 Armel F4HWN + * https://github.com/armel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef APP_BEAM_H +#define APP_BEAM_H + +#ifdef ENABLE_FEAT_F4HWN_BEAM + +#include +#include + +#include "driver/keyboard.h" + +typedef enum { + BEAM_MODE_TX = 0, + BEAM_MODE_RX +} BEAM_Mode_t; + +typedef enum { + BEAM_STATUS_READY = 0, + BEAM_STATUS_TX_DONE, + BEAM_STATUS_RX_WAIT, + BEAM_STATUS_RX_SAVED, + BEAM_STATUS_RX_FULL, + BEAM_STATUS_ERROR, + BEAM_STATUS_TX_WAIT +} BEAM_Status_t; + +extern BEAM_Mode_t gBeamMode; +extern BEAM_Status_t gBeamStatus; +extern uint16_t gBeamCopiedChannel; +extern uint8_t gBeamRxWordCount; +extern bool gBeamActive; + +void ACTION_Beam(void); +void BEAM_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld); +void BEAM_StorePacket(void); + +#endif // ENABLE_FEAT_F4HWN_BEAM +#endif // APP_BEAM_H diff --git a/App/settings.h b/App/settings.h index b833c61d..81ffa637 100644 --- a/App/settings.h +++ b/App/settings.h @@ -127,6 +127,9 @@ enum ACTION_OPT_t { #ifdef ENABLE_REGA ACTION_OPT_REGA_ALARM, ACTION_OPT_REGA_TEST, +#endif +#ifdef ENABLE_FEAT_F4HWN_BEAM + ACTION_OPT_BEAM, #endif ACTION_OPT_LEN }; diff --git a/App/ui/main.c b/App/ui/main.c index 8c2c42a5..6353a856 100644 --- a/App/ui/main.c +++ b/App/ui/main.c @@ -19,6 +19,11 @@ #include "app/chFrScanner.h" #include "app/dtmf.h" + +#ifdef ENABLE_FEAT_F4HWN_BEAM + #include "app/beam.h" +#endif + #ifdef ENABLE_AM_FIX #include "am_fix.h" #endif @@ -84,6 +89,46 @@ static uint8_t gScanProgressPriorityState; #define SCAN_PROGRESS_PRIORITY_HOLD_FRAMES 6 #endif +#ifdef ENABLE_FEAT_F4HWN_BEAM +static void UI_MAIN_DrawBeamLine(void) +{ + const char *text; +#ifdef ENABLE_FEAT_F4HWN + const unsigned int line = isMainOnly() ? 5 : 3; +#else + const unsigned int line = 3; +#endif + + switch (gBeamStatus) { + case BEAM_STATUS_TX_WAIT: + text = "SENDING"; + break; + case BEAM_STATUS_TX_DONE: + text = "SENT"; + break; + case BEAM_STATUS_RX_WAIT: + text = "WAITING"; + break; + case BEAM_STATUS_RX_SAVED: + text = "RECEIVED"; + break; + case BEAM_STATUS_RX_FULL: + text = "MEM FULL"; + break; + case BEAM_STATUS_ERROR: + text = "ERROR"; + break; + case BEAM_STATUS_READY: + default: + text = (gBeamMode == BEAM_MODE_TX) ? "BEAM TX" : "BEAM RX"; + break; + } + + memset(gFrameBuffer[line], 0, LCD_WIDTH); + UI_PrintStringSmallBold(text, 2, 127, line); +} +#endif + const char *VfoStateStr[] = { [VFO_STATE_NORMAL]="", [VFO_STATE_BUSY]="BUSY", @@ -2047,6 +2092,13 @@ void UI_DisplayMain(void) const bool rx = FUNCTION_IsRx(); +#ifdef ENABLE_FEAT_F4HWN_BEAM + if (gBeamActive) { + center_line = CENTER_LINE_BEAM; + UI_MAIN_DrawBeamLine(); + } + else +#endif #ifdef ENABLE_FEAT_F4HWN_SCAN_PROGRESS if (!rx && gScanStateDir != SCAN_OFF && UI_DrawScanProgress()) { diff --git a/App/ui/main.h b/App/ui/main.h index 072612bd..43abb3aa 100644 --- a/App/ui/main.h +++ b/App/ui/main.h @@ -28,7 +28,10 @@ enum center_line_t { CENTER_LINE_RSSI, CENTER_LINE_AM_FIX_DATA, CENTER_LINE_DTMF_DEC, - CENTER_LINE_CHARGE_DATA + CENTER_LINE_CHARGE_DATA, +#ifdef ENABLE_FEAT_F4HWN_BEAM + CENTER_LINE_BEAM +#endif }; enum Vfo_txtr_mode{ diff --git a/App/ui/menu.c b/App/ui/menu.c index 38d74b1e..774afce0 100644 --- a/App/ui/menu.c +++ b/App/ui/menu.c @@ -487,6 +487,9 @@ const t_sidefunction gSubMenu_SIDEFUNCTIONS[] = #ifdef ENABLE_REGA {"REGA\nALARM", ACTION_OPT_REGA_ALARM}, {"REGA\nTEST", ACTION_OPT_REGA_TEST}, +#endif +#ifdef ENABLE_FEAT_F4HWN_BEAM + {"BEAM", ACTION_OPT_BEAM}, #endif {"LOCK\nKEYPAD", ACTION_OPT_KEYLOCK}, {"VFO A\nVFO B", ACTION_OPT_A_B}, diff --git a/CMakePresets.json b/CMakePresets.json index 1966b83a..444f422d 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -70,6 +70,7 @@ "ENABLE_FEAT_F4HWN_MEM": false, "ENABLE_FEAT_F4HWN_SCAN_PROGRESS": false, "ENABLE_FEAT_F4HWN_SCAN_FASTER": false, + "ENABLE_FEAT_F4HWN_BEAM": false, "ENABLE_AGC_SHOW_DATA": false, "ENABLE_UART_RW_BK_REGS": false, "ENABLE_SWD": false, @@ -196,6 +197,7 @@ "ENABLE_FEAT_F4HWN_AUDIO_SCOPE": true, "ENABLE_FEAT_F4HWN_SCAN_PROGRESS": true, "ENABLE_FEAT_F4HWN_SCAN_FASTER": true, + "ENABLE_FEAT_F4HWN_BEAM": true, "ENABLE_FEAT_F4HWN_QRCODE": true, "ENABLE_FEAT_F4HWN_MEM": true, "ENABLE_SWD": true, From a9ff58e4a7b31388ab9668bf1568d94dbb806af2 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Tue, 5 May 2026 04:39:49 +0200 Subject: [PATCH 25/33] Beam: full Apache 2.0 header in beam.c --- App/app/beam.c | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/App/app/beam.c b/App/app/beam.c index 08b9d169..96149938 100644 --- a/App/app/beam.c +++ b/App/app/beam.c @@ -1,6 +1,17 @@ -/* Copyright 2026 +/* Copyright 2026 Armel F4HWN + * https://github.com/armel * - * Licensed under the Apache License, Version 2.0. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #ifdef ENABLE_FEAT_F4HWN_BEAM From 33d949a4beb3290a230812e7399c8b3cfcb37fe2 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Tue, 5 May 2026 16:51:53 +0200 Subject: [PATCH 26/33] Move BEAM Action --- App/ui/menu.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/App/ui/menu.c b/App/ui/menu.c index 774afce0..2b5156b6 100644 --- a/App/ui/menu.c +++ b/App/ui/menu.c @@ -487,9 +487,6 @@ const t_sidefunction gSubMenu_SIDEFUNCTIONS[] = #ifdef ENABLE_REGA {"REGA\nALARM", ACTION_OPT_REGA_ALARM}, {"REGA\nTEST", ACTION_OPT_REGA_TEST}, -#endif -#ifdef ENABLE_FEAT_F4HWN_BEAM - {"BEAM", ACTION_OPT_BEAM}, #endif {"LOCK\nKEYPAD", ACTION_OPT_KEYLOCK}, {"VFO A\nVFO B", ACTION_OPT_A_B}, @@ -511,6 +508,9 @@ const t_sidefunction gSubMenu_SIDEFUNCTIONS[] = {"POWER\nHIGH", ACTION_OPT_POWER_HIGH}, {"REMOVE\nOFFSET", ACTION_OPT_REMOVE_OFFSET}, #endif + #ifdef ENABLE_FEAT_F4HWN_BEAM + {"BEAM", ACTION_OPT_BEAM}, + #endif #endif }; From c92e63da0d776c72cfe9f3cdf74b5dc8822bb1a3 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Tue, 5 May 2026 21:51:21 +0200 Subject: [PATCH 27/33] Restore spectrum predictable 1 dB step on manual squelch line (issue #333) --- App/app/spectrum.c | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/App/app/spectrum.c b/App/app/spectrum.c index 6b44bfb0..86e35c03 100644 --- a/App/app/spectrum.c +++ b/App/app/spectrum.c @@ -462,8 +462,6 @@ static void SetFScan(uint32_t f) // Spectrum related -bool IsPeakOverLevel() { return peak.rssi >= settings.rssiTriggerLevel; } - static bool IsPeakOverOpenLevel() { uint16_t openLevel = settings.rssiTriggerLevel; @@ -994,25 +992,15 @@ static void UpdateAutoSensitivity(bool inc) static void UpdateRssiTriggerLevel(bool inc) { - if (inc && isListening && IsPeakOverLevel()) - { - // One-press escape: jump threshold above the active carrier without - // clamping — the interferer may exceed dbMax so ClampRssiTriggerLevel - // would silently prevent the escape. - settings.rssiTriggerLevel = peak.rssi + 8; - } + if (inc) + settings.rssiTriggerLevel += 2; else - { - if (inc) - settings.rssiTriggerLevel += 2; - else - settings.rssiTriggerLevel -= 2; + settings.rssiTriggerLevel -= 2; - if (settings.rssiTriggerLevel > dbm2rssi(settings.dbMax)) - UpdateDbMax(true); - else - ClampRssiTriggerLevel(); - } + if (settings.rssiTriggerLevel > dbm2rssi(settings.dbMax)) + UpdateDbMax(true); + else + ClampRssiTriggerLevel(); redrawScreen = true; redrawStatus = true; From 4523108293a909afb111436d8af41b679f1ba8b2 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Tue, 5 May 2026 22:00:10 +0200 Subject: [PATCH 28/33] Skip spectrum curve smoothing in manual mode (issue #333) --- App/app/spectrum.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/App/app/spectrum.c b/App/app/spectrum.c index 86e35c03..5cc0982b 100644 --- a/App/app/spectrum.c +++ b/App/app/spectrum.c @@ -1525,7 +1525,11 @@ static void BuildCurrentSpectrumTopY(uint8_t *topY) #endif BuildSpectrumTopY(topY, bars); - SmoothTopY(topY); + // Skip cosmetic smoothing in manual mode so the rendered curve matches + // the raw RSSI used by the squelch detector — narrow peaks must visibly + // cross the trigger line when the radio opens the squelch. + if (!manualSetFlag) + SmoothTopY(topY); } static void DrawStatus() From 7a4226f48329d1f1c57a2f132cb1faf5a5df1eb3 Mon Sep 17 00:00:00 2001 From: mrkusypl Date: Wed, 6 May 2026 00:03:26 +0200 Subject: [PATCH 29/33] Code refactoring (224 B) --- App/app/action.c | 3 + App/app/aircopy.c | 39 +++------- App/app/beam.c | 169 +++++++++++++++---------------------------- App/app/main.c | 75 +++++++++---------- App/audio.c | 1 - App/audio.h | 1 - App/helper/battery.c | 4 +- App/ui/main.c | 3 +- 8 files changed, 111 insertions(+), 184 deletions(-) diff --git a/App/app/action.c b/App/app/action.c index d399023b..af2fcab2 100644 --- a/App/app/action.c +++ b/App/app/action.c @@ -380,6 +380,9 @@ void ACTION_Handle(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) case ACTION_OPT_POWER_HIGH: case ACTION_OPT_REMOVE_OFFSET: #endif + #endif + #ifdef ENABLE_FEAT_F4HWN_BEAM + case ACTION_OPT_BEAM: #endif gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL; return; diff --git a/App/app/aircopy.c b/App/app/aircopy.c index ea8973d9..895392d3 100644 --- a/App/app/aircopy.c +++ b/App/app/aircopy.c @@ -158,8 +158,10 @@ static inline const AIRCOPY_Segment_t *AIRCOPY_FindSegmentForOffset(uint16_t off return NULL; } -static inline void AIRCOPY_CheckComplete(void) +static inline void AIRCOPY_CheckComplete(uint16_t *num) { + *num = *num + 1; + const AIRCOPY_TransferMap_t *map = AIRCOPY_GetCurrentMap(); uint16_t done = gAirCopyBlockNumber + gErrorsDuringAirCopy; @@ -257,12 +259,10 @@ void AIRCOPY_StorePacket(void) BK4819_PrepareFSKReceive(); if ((Status & 0x0010U) != 0 || g_FSK_Buffer[0] != 0xABCD || g_FSK_Buffer[35] != 0xDCBA) { - gErrorsDuringAirCopy++; - BK4819_ResetFSK(); // <- important BK4819_PrepareFSKReceive(); // <- re-arm proprement - AIRCOPY_CheckComplete(); + AIRCOPY_CheckComplete(&gErrorsDuringAirCopy); return; } @@ -270,8 +270,7 @@ void AIRCOPY_StorePacket(void) uint16_t Crc = CRC_Calculate(&g_FSK_Buffer[1], 2 + 64); if (g_FSK_Buffer[34] != Crc) { - gErrorsDuringAirCopy++; - AIRCOPY_CheckComplete(); + AIRCOPY_CheckComplete(&gErrorsDuringAirCopy); return; } @@ -280,36 +279,18 @@ void AIRCOPY_StorePacket(void) const AIRCOPY_Segment_t *seg = AIRCOPY_FindSegmentForOffset(Offset); if (seg == NULL) { - gErrorsDuringAirCopy++; - AIRCOPY_CheckComplete(); + AIRCOPY_CheckComplete(&gErrorsDuringAirCopy); return; } - if (seg->write_mode == AIRCOPY_WRITE_BYTES) - { - /* Raw bytes stream, written in 8-byte EEPROM chunks */ - const uint8_t *p8 = (const uint8_t *)&g_FSK_Buffer[2]; + const uint8_t *pData = (const uint8_t *)&g_FSK_Buffer[2]; - for (unsigned int i = 0; i < 8; i++) - { - EEPROM_WriteBuffer(Offset + (i * 8), p8 + (i * 8)); - } - } - else + for (unsigned int i = 0; i < 8; i++) { - /* Structured data path (kept as-is) */ - const uint16_t *pData = &g_FSK_Buffer[2]; - - for (unsigned int i = 0; i < 8; i++) - { - EEPROM_WriteBuffer(Offset, pData); - pData += 4; - Offset += 8; - } + EEPROM_WriteBuffer(Offset + (i * 8), pData + (i * 8)); } - gAirCopyBlockNumber++; - AIRCOPY_CheckComplete(); + AIRCOPY_CheckComplete(&gAirCopyBlockNumber); } static void AIRCOPY_InitTransfer(bool isSendMode) diff --git a/App/app/beam.c b/App/app/beam.c index 96149938..0f024757 100644 --- a/App/app/beam.c +++ b/App/app/beam.c @@ -72,34 +72,7 @@ uint16_t gBeamCopiedChannel = 0xFFFFu; uint8_t gBeamRxWordCount; bool gBeamActive; -static VFO_Info_t gBeamBackupVfo; static VFO_Info_t gBeamRadioVfo; -static uint16_t gBeamBackupScreenChannel; -static uint8_t gBeamVfoIndex; -static uint8_t gBeamBackupRxVfoIndex; -static bool gBeamHasBackup; - -static void BEAM_SaveBackup(void) -{ - gBeamVfoIndex = gEeprom.TX_VFO; - memcpy(&gBeamBackupVfo, &gEeprom.VfoInfo[gBeamVfoIndex], sizeof(gBeamBackupVfo)); - gBeamBackupScreenChannel = gEeprom.ScreenChannel[gBeamVfoIndex]; - gBeamBackupRxVfoIndex = gEeprom.RX_VFO; - gBeamHasBackup = true; -} - -static void BEAM_RestoreBackup(void) -{ - if (!gBeamHasBackup) - return; - - memcpy(&gEeprom.VfoInfo[gBeamVfoIndex], &gBeamBackupVfo, sizeof(gBeamBackupVfo)); - gEeprom.ScreenChannel[gBeamVfoIndex] = gBeamBackupScreenChannel; - gEeprom.RX_VFO = gBeamBackupRxVfoIndex; - RADIO_SelectVfos(); - RADIO_SetupRegisters(true); - gBeamHasBackup = false; -} static void BEAM_SetRadioToBeamFrequency(void) { @@ -118,58 +91,46 @@ static void BEAM_SetRadioToBeamFrequency(void) BK4819_ResetFSK(); } -static void BEAM_CopyNameFromActiveVfo(char *name) +static void BEAM_SendPacket(void) { - memset(name, 0, 16); - - if (IS_MR_CHANNEL(gBeamBackupVfo.CHANNEL_SAVE)) { - SETTINGS_FetchChannelName(name, gBeamBackupVfo.CHANNEL_SAVE); - } else { - memcpy(name, gBeamBackupVfo.Name, sizeof(gBeamBackupVfo.Name)); - } -} - -static void BEAM_FillPayload(BEAM_Payload_t *payload) -{ - memset(payload, 0, sizeof(*payload)); + memset(g_FSK_Buffer, 0, sizeof(g_FSK_Buffer)); + g_FSK_Buffer[0] = 0xABCDu; + + BEAM_Payload_t * const payload = (BEAM_Payload_t *)&g_FSK_Buffer[2]; + + const VFO_Info_t *vfo = &gEeprom.VfoInfo[gEeprom.TX_VFO]; payload->magic = BEAM_PACKET_MAGIC; payload->version = BEAM_PACKET_VERSION; - payload->rx_frequency = gBeamBackupVfo.freq_config_RX.Frequency; - payload->tx_offset_frequency = gBeamBackupVfo.TX_OFFSET_FREQUENCY; - payload->rx_code = gBeamBackupVfo.freq_config_RX.Code; - payload->tx_code = gBeamBackupVfo.freq_config_TX.Code; - payload->rx_codetype = gBeamBackupVfo.freq_config_RX.CodeType; - payload->tx_codetype = gBeamBackupVfo.freq_config_TX.CodeType; - payload->modulation = gBeamBackupVfo.Modulation; - payload->tx_offset_direction = gBeamBackupVfo.TX_OFFSET_FREQUENCY_DIRECTION; - payload->tx_lock = gBeamBackupVfo.TX_LOCK; - payload->busy_channel_lock = gBeamBackupVfo.BUSY_CHANNEL_LOCK; - payload->output_power = gBeamBackupVfo.OUTPUT_POWER; - payload->channel_bandwidth = gBeamBackupVfo.CHANNEL_BANDWIDTH; - payload->frequency_reverse = gBeamBackupVfo.FrequencyReverse; - payload->dtmf_ptt_id_mode = gBeamBackupVfo.DTMF_PTT_ID_TX_MODE; + payload->rx_frequency = vfo->freq_config_RX.Frequency; + payload->tx_offset_frequency = vfo->TX_OFFSET_FREQUENCY; + payload->rx_code = vfo->freq_config_RX.Code; + payload->tx_code = vfo->freq_config_TX.Code; + payload->rx_codetype = vfo->freq_config_RX.CodeType; + payload->tx_codetype = vfo->freq_config_TX.CodeType; + payload->modulation = vfo->Modulation; + payload->tx_offset_direction = vfo->TX_OFFSET_FREQUENCY_DIRECTION; + payload->tx_lock = vfo->TX_LOCK; + payload->busy_channel_lock = vfo->BUSY_CHANNEL_LOCK; + payload->output_power = vfo->OUTPUT_POWER; + payload->channel_bandwidth = vfo->CHANNEL_BANDWIDTH; + payload->frequency_reverse = vfo->FrequencyReverse; + payload->dtmf_ptt_id_mode = vfo->DTMF_PTT_ID_TX_MODE; #ifdef ENABLE_DTMF_CALLING - payload->dtmf_decoding_enable = gBeamBackupVfo.DTMF_DECODING_ENABLE; + payload->dtmf_decoding_enable = vfo->DTMF_DECODING_ENABLE; #endif - payload->step_setting = gBeamBackupVfo.STEP_SETTING; - payload->scrambling_type = gBeamBackupVfo.SCRAMBLING_TYPE; - payload->band = gBeamBackupVfo.Band; - payload->scanlist = gBeamBackupVfo.SCANLIST_PARTICIPATION; - payload->compander = gBeamBackupVfo.Compander; + payload->step_setting = vfo->STEP_SETTING; + payload->scrambling_type = vfo->SCRAMBLING_TYPE; + payload->band = vfo->Band; + payload->scanlist = vfo->SCANLIST_PARTICIPATION; + payload->compander = vfo->Compander; - BEAM_CopyNameFromActiveVfo(payload->name); -} + if (IS_MR_CHANNEL(vfo->CHANNEL_SAVE)) { + SETTINGS_FetchChannelName(payload->name, vfo->CHANNEL_SAVE); + } else { + memcpy(payload->name, vfo->Name, sizeof(vfo->Name)); + } -static void BEAM_SendPacket(void) -{ - BEAM_Payload_t payload; - - BEAM_FillPayload(&payload); - - memset(g_FSK_Buffer, 0, sizeof(g_FSK_Buffer)); - g_FSK_Buffer[0] = 0xABCDu; - memcpy(&g_FSK_Buffer[2], &payload, sizeof(payload)); g_FSK_Buffer[34] = CRC_Calculate(&g_FSK_Buffer[1], 2 + 64); g_FSK_Buffer[35] = 0xDCBAu; @@ -193,30 +154,23 @@ static void BEAM_SendPacket(void) gUpdateDisplay = true; } -static uint16_t BEAM_FindFirstFreeChannel(void) +static void BEAM_SavePayloadToFirstFreeChannel(const BEAM_Payload_t *payload) { - for (uint16_t channel = MR_CHANNEL_FIRST; IS_MR_CHANNEL(channel); channel++) { - if (!RADIO_CheckValidChannel(channel, false, 0)) - return channel; + uint16_t channel = 0xFFFFu; + + for (uint16_t c = MR_CHANNEL_FIRST; IS_MR_CHANNEL(c); c++) { + if (!RADIO_CheckValidChannel(c, false, 0)) { + channel = c; + break; + } } - return 0xFFFFu; -} - -static bool BEAM_SavePayloadToFirstFreeChannel(const BEAM_Payload_t *payload) -{ - uint16_t channel = BEAM_FindFirstFreeChannel(); - char name[16]; - if (channel == 0xFFFFu) { gBeamStatus = BEAM_STATUS_RX_FULL; gUpdateDisplay = true; - return false; + return; } - memcpy(name, payload->name, sizeof(name)); - name[sizeof(name) - 1] = 0; - VFO_Info_t vfo; RADIO_InitInfo(&vfo, channel, payload->rx_frequency); @@ -243,17 +197,21 @@ static bool BEAM_SavePayloadToFirstFreeChannel(const BEAM_Payload_t *payload) vfo.Band = payload->band; vfo.SCANLIST_PARTICIPATION = payload->scanlist; vfo.Compander = payload->compander; - memcpy(vfo.Name, name, sizeof(vfo.Name)); + + memcpy(vfo.Name, payload->name, sizeof(vfo.Name)); + vfo.Name[sizeof(vfo.Name) - 1] = '\0'; + RADIO_ApplyOffset(&vfo); RADIO_ConfigureSquelchAndOutputPower(&vfo); - SETTINGS_SaveChannel(channel, gBeamVfoIndex, &vfo, 3); - SETTINGS_SaveChannelName(channel, name); + SETTINGS_SaveChannel(channel, gEeprom.TX_VFO, &vfo, 3); + SETTINGS_SaveChannelName(channel, vfo.Name); gBeamCopiedChannel = channel; gBeamStatus = BEAM_STATUS_RX_SAVED; gUpdateDisplay = true; - return true; + BACKLIGHT_TurnOn(); + return; } static void BEAM_KeyMenu(void) @@ -276,33 +234,26 @@ static void BEAM_KeyExit(void) BK4819_ResetFSK(); if (gBeamMode == BEAM_MODE_RX && gBeamCopiedChannel != 0xFFFFu) { - gEeprom.MrChannel[gBeamVfoIndex] = gBeamCopiedChannel; - gEeprom.ScreenChannel[gBeamVfoIndex] = gBeamCopiedChannel; - RADIO_ConfigureChannel(gBeamVfoIndex, VFO_CONFIGURE_RELOAD); - RADIO_SelectVfos(); - RADIO_SetupRegisters(true); - gBeamHasBackup = false; - } else { - BEAM_RestoreBackup(); + gEeprom.MrChannel[gEeprom.TX_VFO] = gBeamCopiedChannel; + gEeprom.ScreenChannel[gEeprom.TX_VFO] = gBeamCopiedChannel; + RADIO_ConfigureChannel(gEeprom.TX_VFO, VFO_CONFIGURE_RELOAD); } + + RADIO_SelectVfos(); + RADIO_SetupRegisters(true); GUI_SelectNextDisplay(DISPLAY_MAIN); - gRequestDisplayScreen = DISPLAY_MAIN; gBeamActive = false; - gUpdateDisplay = true; } void ACTION_Beam(void) { - BEAM_SaveBackup(); gBeamMode = BEAM_MODE_TX; gBeamStatus = BEAM_STATUS_READY; gBeamCopiedChannel = 0xFFFFu; gBeamRxWordCount = 0; gBeamActive = true; GUI_SelectNextDisplay(DISPLAY_MAIN); - gRequestDisplayScreen = DISPLAY_MAIN; - gUpdateDisplay = true; } void BEAM_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) @@ -316,7 +267,7 @@ void BEAM_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) switch (Key) { case KEY_UP: case KEY_DOWN: - gBeamMode = (gBeamMode == BEAM_MODE_TX) ? BEAM_MODE_RX : BEAM_MODE_TX; + gBeamMode ^= 1; // (gBeamMode == BEAM_MODE_TX) ? BEAM_MODE_RX : BEAM_MODE_TX gBeamStatus = BEAM_STATUS_READY; gBeamRxWordCount = 0; break; @@ -355,13 +306,12 @@ void BEAM_StorePacket(void) if (g_FSK_Buffer[34] != CRC_Calculate(&g_FSK_Buffer[1], 2 + 64)) goto error; - BEAM_Payload_t payload; - memcpy(&payload, &g_FSK_Buffer[2], sizeof(payload)); + BEAM_Payload_t * const payload = (BEAM_Payload_t *)&g_FSK_Buffer[2]; - if (payload.magic != BEAM_PACKET_MAGIC || payload.version != BEAM_PACKET_VERSION) + if (payload->magic != BEAM_PACKET_MAGIC || payload->version != BEAM_PACKET_VERSION) goto error; - BEAM_SavePayloadToFirstFreeChannel(&payload); + BEAM_SavePayloadToFirstFreeChannel(payload); BK4819_ResetFSK(); return; @@ -369,6 +319,7 @@ error: gBeamStatus = BEAM_STATUS_ERROR; gBeamRxWordCount = 0; gUpdateDisplay = true; + BACKLIGHT_TurnOn(); } #endif // ENABLE_FEAT_F4HWN_BEAM diff --git a/App/app/main.c b/App/app/main.c index a67bee90..4d8938db 100644 --- a/App/app/main.c +++ b/App/app/main.c @@ -262,34 +262,51 @@ static void processFKeyFunction(const KEY_Code_t Key, const bool beep) break; case KEY_7: -#ifdef ENABLE_VOX - ACTION_Vox(); -//#else -// toggle_chan_scanlist(); +#ifdef ENABLE_FEAT_F4HWN_GAME + if (!beep) { + APP_RunBreakout(); + } else { #endif +#ifdef ENABLE_VOX + ACTION_Vox(); +//#else +// toggle_chan_scanlist(); +#endif +#ifdef ENABLE_FEAT_F4HWN_GAME + } +#endif + break; case KEY_8: - gTxVfo->FrequencyReverse = gTxVfo->FrequencyReverse == false; - gRequestSaveChannel = 1; + if (!beep) { + ACTION_BackLightOnDemand(); + } + else { + gTxVfo->FrequencyReverse = gTxVfo->FrequencyReverse == false; + gRequestSaveChannel = 1; + } + break; case KEY_9: - if (RADIO_CheckValidChannel(gEeprom.CHAN_1_CALL, false, 0)) { - gEeprom.MrChannel[Vfo] = gEeprom.CHAN_1_CALL; - gEeprom.ScreenChannel[Vfo] = gEeprom.CHAN_1_CALL; + if (!beep) { + ACTION_BackLight(); + } + else { + if (RADIO_CheckValidChannel(gEeprom.CHAN_1_CALL, false, 0)) { + gEeprom.MrChannel[Vfo] = gEeprom.CHAN_1_CALL; + gEeprom.ScreenChannel[Vfo] = gEeprom.CHAN_1_CALL; #ifdef ENABLE_VOICE - AUDIO_SetVoiceID(0, VOICE_ID_CHANNEL_MODE); - AUDIO_SetDigitVoice(1, gEeprom.CHAN_1_CALL + 1); - gAnotherVoiceID = (VOICE_ID_t)0xFE; + AUDIO_SetVoiceID(0, VOICE_ID_CHANNEL_MODE); + AUDIO_SetDigitVoice(1, gEeprom.CHAN_1_CALL + 1); + gAnotherVoiceID = (VOICE_ID_t)0xFE; #endif - gRequestSaveVFO = true; - gVfoConfigureMode = VFO_CONFIGURE_RELOAD; - break; + gRequestSaveVFO = true; + gVfoConfigureMode = VFO_CONFIGURE_RELOAD; + } } - if (beep) - gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL; break; #ifdef ENABLE_FEAT_F4HWN // Set Squelch F + UP or Down and Step F + SIDE1 or F + SIDE2 @@ -655,30 +672,6 @@ static void MAIN_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) HideFKeyIcon(); - #ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS - if(gEeprom.MENU_LOCK == true && Key != 2) { - return; - } - #endif - - if(Key == 8) - { - ACTION_BackLightOnDemand(); - return; - } - else if(Key == 9) - { - ACTION_BackLight(); - return; - } - #ifdef ENABLE_FEAT_F4HWN_GAME - else if(Key == 7) - { - APP_RunBreakout(); - return; - } - #endif - processFKeyFunction(Key, false); } diff --git a/App/audio.c b/App/audio.c index 7b09141b..42308e96 100644 --- a/App/audio.c +++ b/App/audio.c @@ -41,7 +41,6 @@ static const uint16_t BEEP_Classic_array[][3] = { /* Tone Duration Repeats [BEEP_880HZ_200MS] = {880, 200, 1 }, [BEEP_880HZ_500MS] = {880, 500, 1 }, #endif - [BEEP_500HZ_60MS_DOUBLE_BEEP] = {500, 60, 2 }, #ifdef ENABLE_FEAT_F4HWN [BEEP_400HZ_30MS] = {400, 30, 1 }, [BEEP_500HZ_30MS] = {500, 30, 1 }, diff --git a/App/audio.h b/App/audio.h index 22790dbb..b9c83a91 100644 --- a/App/audio.h +++ b/App/audio.h @@ -38,7 +38,6 @@ enum BEEP_Type_t BEEP_880HZ_200MS, BEEP_880HZ_500MS, #endif - BEEP_500HZ_60MS_DOUBLE_BEEP, #ifdef ENABLE_FEAT_F4HWN BEEP_400HZ_30MS, BEEP_500HZ_30MS, diff --git a/App/helper/battery.c b/App/helper/battery.c index 8d451c57..c02a1814 100644 --- a/App/helper/battery.c +++ b/App/helper/battery.c @@ -228,7 +228,7 @@ void BATTERY_TimeSlice500ms(void) if (lowBatteryCountdown < lowBatteryPeriod) { if (lowBatteryCountdown == lowBatteryPeriod-1 && !gChargingWithTypeC && !gLowBatteryConfirmed) { - AUDIO_PlayBeep(BEEP_500HZ_60MS_DOUBLE_BEEP); + AUDIO_PlayBeep(BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL); } return; } @@ -241,7 +241,7 @@ void BATTERY_TimeSlice500ms(void) // not on charge if (!gLowBatteryConfirmed) { - AUDIO_PlayBeep(BEEP_500HZ_60MS_DOUBLE_BEEP); + AUDIO_PlayBeep(BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL); #ifdef ENABLE_VOICE AUDIO_SetVoiceID(0, VOICE_ID_LOW_VOLTAGE); #endif diff --git a/App/ui/main.c b/App/ui/main.c index 6353a856..aa0f462b 100644 --- a/App/ui/main.c +++ b/App/ui/main.c @@ -2100,9 +2100,10 @@ void UI_DisplayMain(void) else #endif #ifdef ENABLE_FEAT_F4HWN_SCAN_PROGRESS - if (!rx && gScanStateDir != SCAN_OFF && UI_DrawScanProgress()) + if (!rx && gScanStateDir != SCAN_OFF && gKeypadLocked == 0) { center_line = CENTER_LINE_SCAN_PROGRESS; + UI_DrawScanProgress(); } else #endif From 7d69b74b081821cca25ab62dd3e2c25355113260 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Wed, 6 May 2026 01:47:37 +0200 Subject: [PATCH 30/33] Fix(aircopy): include VFO area in Settings map so scan-range source frequencies are replicated --- App/app/aircopy.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/App/app/aircopy.c b/App/app/aircopy.c index 895392d3..884ff90c 100644 --- a/App/app/aircopy.c +++ b/App/app/aircopy.c @@ -90,12 +90,14 @@ DECLARE_AIRCOPY_BANK(1) static const AIRCOPY_Segment_t AIRCOPY_Segments_Settings[] = { { 0xA000, 0xA170, AIRCOPY_WRITE_BYTES }, { 0x880E, 0x886E, AIRCOPY_WRITE_BYTES }, + { 0x9000, 0x90D6, AIRCOPY_WRITE_BYTES }, // VFO area (so scan-range source frequencies are replicated) }; +// total_blocks = ceil(0x170/64) + ceil(0x60/64) + ceil(0xD6/64) = 6 + 2 + 4 = 12 static const AIRCOPY_TransferMap_t AIRCOPY_Map_Settings = { .segments = AIRCOPY_Segments_Settings, - .num_segments = 2, - .total_blocks = 8 + .num_segments = 3, + .total_blocks = 12 }; // Finally From 1ed550fb92037c9dc3fe9162d44aaf9e1577ae59 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Thu, 7 May 2026 15:32:07 +0200 Subject: [PATCH 31/33] Add LOGO option to POnMsg menu --- App/CMakeLists.txt | 1 + App/driver/eeprom_compat.c | 5 +++++ App/settings.h | 5 ++++- App/ui/menu.c | 3 +++ App/ui/menu.h | 6 +++++- App/ui/welcome.c | 30 +++++++++++++++++++++++++++++- CMakePresets.json | 2 ++ archive/quansheng.stock.logo.png | Bin 0 -> 2058 bytes 8 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 archive/quansheng.stock.logo.png diff --git a/App/CMakeLists.txt b/App/CMakeLists.txt index 44fd71ed..0850f989 100644 --- a/App/CMakeLists.txt +++ b/App/CMakeLists.txt @@ -214,6 +214,7 @@ if(ENABLE_FEAT_F4HWN_BEAM AND NOT ENABLE_AIRCOPY) message(FATAL_ERROR "ENABLE_FEAT_F4HWN_BEAM requires ENABLE_AIRCOPY (it reuses g_FSK_Buffer, AIRCOPY_Obfuscate and the FSK packet plumbing).") endif() enable_feature(ENABLE_FEAT_F4HWN_QRCODE) +enable_feature(ENABLE_FEAT_F4HWN_LOGO) # ---- DEBUGGING ---- diff --git a/App/driver/eeprom_compat.c b/App/driver/eeprom_compat.c index 39dde9aa..b4cf7bf3 100644 --- a/App/driver/eeprom_compat.c +++ b/App/driver/eeprom_compat.c @@ -67,6 +67,11 @@ static const AddrMapping_t ADDR_MAPPINGS[] = { // Settings Version * 16 Bytes 0x00A160 -> 0x00A170 _MK_MAPPING(0x010000, 0x00B000, 0x00B200), // Calibration 512 Bytes!!! + + _MK_MAPPING(0x011000, 0x00C000, 0x00D000), // Boot Logo sector (4 KB): + // [0x00..0x07] 8-byte header (reserved) + // [0x08..0x407] 128x64 monochrome bitmap, 1024 Bytes + // ST7565-native: 8 pages * 128 columns, column-major LSB-top }; static void AddrTranslate(uint16_t EEPROM_Addr, uint16_t Size, uint32_t *PY25Q16_Addr_out, uint16_t *Size_out, bool *End_out); diff --git a/App/settings.h b/App/settings.h index 81ffa637..07884bae 100644 --- a/App/settings.h +++ b/App/settings.h @@ -34,7 +34,10 @@ enum POWER_OnDisplayMode_t { #endif POWER_ON_DISPLAY_MODE_MESSAGE, POWER_ON_DISPLAY_MODE_VOLTAGE, - POWER_ON_DISPLAY_MODE_NONE +#ifdef ENABLE_FEAT_F4HWN_LOGO + POWER_ON_DISPLAY_MODE_LOGO, +#endif + POWER_ON_DISPLAY_MODE_NONE, }; typedef enum POWER_OnDisplayMode_t POWER_OnDisplayMode_t; diff --git a/App/ui/menu.c b/App/ui/menu.c index 2b5156b6..e0737607 100644 --- a/App/ui/menu.c +++ b/App/ui/menu.c @@ -295,6 +295,9 @@ const char gSubMenu_PONMSG[][8] = #endif "MESSAGE", "VOLTAGE", +#ifdef ENABLE_FEAT_F4HWN_LOGO + "LOGO", +#endif "NONE" }; diff --git a/App/ui/menu.h b/App/ui/menu.h index 0ae007af..10e8b7ff 100644 --- a/App/ui/menu.h +++ b/App/ui/menu.h @@ -211,7 +211,11 @@ extern const char gSubMenu_D_RSP[4][11]; extern const char* const gSubMenu_PTT_ID[5]; #ifdef ENABLE_FEAT_F4HWN - extern const char gSubMenu_PONMSG[5][8]; + #ifdef ENABLE_FEAT_F4HWN_LOGO + extern const char gSubMenu_PONMSG[6][8]; + #else + extern const char gSubMenu_PONMSG[5][8]; + #endif #else extern const char gSubMenu_PONMSG[4][8]; #endif diff --git a/App/ui/welcome.c b/App/ui/welcome.c index 69520590..6dfd0518 100644 --- a/App/ui/welcome.c +++ b/App/ui/welcome.c @@ -33,6 +33,19 @@ #include "screenshot.h" #endif +#ifdef ENABLE_FEAT_F4HWN_LOGO +// Boot logo storage in PY25Q16 external flash, aligned on a 4 KB sector, +// placed past the calibration zone (0x010000-0x010200). +// +// Layout inside the sector starting at LOGO_FLASH_ADDR: +// [0x00..0x07] : 8-byte header (reserved for future magic/version/flags) +// [0x08..0x407]: 128x64 monochrome bitmap (1024 B) +// ST7565-native: 8 pages * 128 columns, column-major LSB-top +#define LOGO_FLASH_ADDR 0x011000 +#define LOGO_HEADER_SIZE 8 +#define LOGO_BITMAP_ADDR (LOGO_FLASH_ADDR + LOGO_HEADER_SIZE) +#endif + #ifdef ENABLE_FEAT_F4HWN_QRCODE // QR code (version 4, 33x33 modules, EC level L) encoding: // https://github.com/armel/uv-k1-k5v3-firmware-custom @@ -216,10 +229,25 @@ void UI_DisplayWelcome(void) #endif UI_DisplayClear(); +#ifdef ENABLE_FEAT_F4HWN_LOGO + if (gEeprom.POWER_ON_DISPLAY_MODE == POWER_ON_DISPLAY_MODE_LOGO) { + // Skip 8-byte header, then read 128x64 bitmap (1024 B): + // page 0 -> gStatusLine, pages 1..7 -> gFrameBuffer. + PY25Q16_ReadBuffer(LOGO_BITMAP_ADDR, gStatusLine, sizeof(gStatusLine)); + PY25Q16_ReadBuffer(LOGO_BITMAP_ADDR + sizeof(gStatusLine), gFrameBuffer, sizeof(gFrameBuffer)); + ST7565_BlitStatusLine(); + ST7565_BlitFullScreen(); + #ifdef ENABLE_FEAT_F4HWN_SCREENSHOT + SCREENSHOT_Update(true); + #endif + return; + } +#endif + #ifdef ENABLE_FEAT_F4HWN ST7565_BlitStatusLine(); ST7565_BlitFullScreen(); - + if (gEeprom.POWER_ON_DISPLAY_MODE == POWER_ON_DISPLAY_MODE_NONE || gEeprom.POWER_ON_DISPLAY_MODE == POWER_ON_DISPLAY_MODE_SOUND) { ST7565_FillScreen(0x00); } diff --git a/CMakePresets.json b/CMakePresets.json index 444f422d..22442849 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -71,6 +71,7 @@ "ENABLE_FEAT_F4HWN_SCAN_PROGRESS": false, "ENABLE_FEAT_F4HWN_SCAN_FASTER": false, "ENABLE_FEAT_F4HWN_BEAM": false, + "ENABLE_FEAT_F4HWN_LOGO": false, "ENABLE_AGC_SHOW_DATA": false, "ENABLE_UART_RW_BK_REGS": false, "ENABLE_SWD": false, @@ -200,6 +201,7 @@ "ENABLE_FEAT_F4HWN_BEAM": true, "ENABLE_FEAT_F4HWN_QRCODE": true, "ENABLE_FEAT_F4HWN_MEM": true, + "ENABLE_FEAT_F4HWN_LOGO": true, "ENABLE_SWD": true, "EDITION_STRING": "Fusion", "TARGET": "f4hwn.fusion" diff --git a/archive/quansheng.stock.logo.png b/archive/quansheng.stock.logo.png new file mode 100644 index 0000000000000000000000000000000000000000..fe3cc9e799d769e8e925a4706fc50c4017337a6d GIT binary patch literal 2058 zcmV+l2=(`gP)3{!y{=D@B{(XJ^y!8Y=3-H$0 z|Mm4*fX@QF`2^nl^*`SLp9OgH3B391f4%|UbOG|UUvdF}FL)j~q*s=wUjUe1w3qe$ zUv|DKKwfC%)b72ZC?Li65s~0EmHm2_k0>G*vFq zh?NIKY<0o(^7TP**3(W2W4@dK+JYE&Mx0^+fHqqPt^(5wg7E9x0_(su2S!`Jbs)~E zx;_CBubzJ8+0gw0z&L22ecE#94!i2wmjDbd7%x^5M&DXJSjHO%O!@&VcPezd0KjCp zPSv3+0mTHc@{v*??g!K4T!b=IHp*-2y-iE(e*jgW%In3G00N z_mmK4L3}F!?b7FGWbeS-)t%kd0zeGb0YVH<-KnL7;N@V{wf_PTXF^E(s;~!GzK48| z=w<;R2I`O?2AFzE7-Qs)!u-PLedvd=R~Yqq?9p}?3ji@72UiCI<04f4OnSZ(#5?xZ zdlw7fP#hG*kW#PG7$K*Gh*ke<0IR?IYag}G)T_T2{qO|<7PEl()msF5g5TAAj zC2TmQdSZ>MzBRwx7G#~2nUnS-(i$**&qwO~R?eXd;7d6m<=z0=?`dFjY2fah&Beb1 zf9qYpOF#T`*a84U(-5Ejfml(1hc5>rccpBj;JUvBJ@N43fuRD7`am9}9JTc3h0kvArs{q=CrSsPCfD9d^Irm^l{83<8 z^T&0!fcO&4xY2ES>Xj$TOCj|=ocC=^4fF2b^kp6nhpEa%Kq2R z112+vbsK^QhBCmzxfWS0fTREvlj4oI@_@*>60*q!QOBVPlVIeuuK^-<9s^9?2c~w7 z2depJI!w=n(au@e42=AmJ?G37bN=E2%mcNz0H%#?$*D0A`Pr;Kxc!$z<8Hy4BIDFW z+RsR!Ef<7*q!uuw&HG9_)*N*qFD@=X2BkFk7Ve3ywSZxGYkaK>)R0xZ z#;*o!AfK%PxBJ}C#^#VM{Q`?QUtECAC(i=6G%`yWOmjsN>HSaMk- z?LFkYCQQ$)XJo8Wqz~A<2sO|AY6DUYm2bq#qs9P350zgmfKE~CX^Z64#iMrSDYFhl zgK6u7<>dpqx&tr_*ybR5ukTZJV-oV>mTgSpR_xrLdCK%D7YiWyR{$nQ*&K|VwgIr@ zL%t*n5tcZfc#UmezYeS2eTy+aqE4zlW(nm0xO?UO!_(>LdywYSs+ zhGQTW0Yd7iHL9-fXdJo#`SLIwVDgvbr6Iyt=Th#yJY25{qaWhZVU&3ujB&~JAP--F zyiACpITW}(tjsgM0&eHl_-i_Bt7R{u)`G~Dhf%LFJ?MuofJ1OdN|8Fm(B$p~Jf53p z!R;C_UE1tDYgD`H>F+^yu>d&;m<}DLcE)Xy!0O))g}PYN(Fi8Mz5!)4SMeRCTkKYjYw04(Utf$*ih2<(*TdI6kS4iv5r z7~&GmfzWGi>eWBj(^gghZJ8=(Js}4uCJWk#6FjGR}6%aK5 z&EXry8jX0N$LSYfeivR+B7H#n>W1qw<1=5GT<`MH{$6eY>L9$pM^J0Ly8hA&@amVh z$NPO2V2>%DxW;DzPJDTLwEkIuJ*If#8lMF?@#XE&`ey<5nBs|Rd=}uumuKtG;(q`D o0RR8skhUcN000I_L_t&o0K|xE>Sn#+4FCWD07*qoM6N<$f(C`>{r~^~ literal 0 HcmV?d00001 From ec9fdd17bf2696e448321c81fe6d574012855c24 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Thu, 7 May 2026 19:24:59 +0200 Subject: [PATCH 32/33] Prepare new version v5.5.0 --- CMakePresets.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakePresets.json b/CMakePresets.json index 22442849..abbeb628 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -76,7 +76,7 @@ "ENABLE_UART_RW_BK_REGS": false, "ENABLE_SWD": false, "VERSION_STRING_1": "v0.22", - "VERSION_STRING_2": "v5.4.0" + "VERSION_STRING_2": "v5.5.0" } }, { From 8f89312e9995042c9d94d2a4d0f018284b92c83e Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Fri, 8 May 2026 03:17:05 +0200 Subject: [PATCH 33/33] Add binaries --- README.md | 4 ++++ archive/f4hwn.fusion.v5.5.0.bin | Bin 0 -> 95836 bytes archive/f4hwn.fusion.v5.5.0.sa818.bin | Bin 0 -> 95836 bytes 3 files changed, 4 insertions(+) create mode 100755 archive/f4hwn.fusion.v5.5.0.bin create mode 100755 archive/f4hwn.fusion.v5.5.0.sa818.bin diff --git a/README.md b/README.md index 809c155c..aba9e3ed 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,7 @@ Special thanks to Jean-Cyrille F6IWW (3 times), Fabrice 14RC123, David F4BPP, Ol * add SetOff menu to set the delay before the transceiver goes into deep sleep (Off or 1 minute to 2 hours), * add SetNFM menu to set Narrow width (12.5kHz or 6.25kHz), * add SetVol menu to adjust RX audio volume, + * add SetScn menu to set Scan mode * rename BatVol menu (52/63) to SysInf, which displays the firmware version in addition to the battery status, * improve PonMsg menu, * improve BackLt menu, @@ -138,6 +139,8 @@ Special thanks to Jean-Cyrille F6IWW (3 times), Fabrice 14RC123, David F4BPP, Ol * `ALL` scans every channel except those set to `OFF`, * named scan lists are shown in the UI and status bar when available, * if the selected scan list is empty or invalid, the firmware automatically jumps to the next valid one, + * very fast scan mode (around 150 freq/s), + * frequencies exclusions, * add resume mode on startup (scan, spectrum analyzer and FM radio), * improve VFO persistence and restore behavior: * save the Squelch level adjusted with F + UP or F + DOWN, @@ -151,6 +154,7 @@ Special thanks to Jean-Cyrille F6IWW (3 times), Fabrice 14RC123, David F4BPP, Ol * MUTE, * POWER HIGH, * REMOVE OFFSET, + * BEAM, * new key combinations: * add the F + UP or F + DOWN key combination to dynamically change the Squelch level, * add the F + F1 or F + F2 key combination to dynamically change the Step, diff --git a/archive/f4hwn.fusion.v5.5.0.bin b/archive/f4hwn.fusion.v5.5.0.bin new file mode 100755 index 0000000000000000000000000000000000000000..f9e0782dfe11edcce04306888c733c72608113fc GIT binary patch literal 95836 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