diff --git a/App/CMakeLists.txt b/App/CMakeLists.txt index 441ec36a..0850f989 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) @@ -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) @@ -205,7 +206,15 @@ 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) +enable_feature(ENABLE_FEAT_F4HWN_LOGO) # ---- DEBUGGING ---- diff --git a/App/app/action.c b/App/app/action.c index 8d88b947..af2fcab2 100644 --- a/App/app/action.c +++ b/App/app/action.c @@ -40,10 +40,14 @@ #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" #endif +#ifdef ENABLE_FEAT_F4HWN_BEAM + #include "app/beam.h" +#endif #if defined(ENABLE_FMRADIO) static void ACTION_Scan_FM(bool bRestart); @@ -131,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); @@ -212,7 +219,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(); @@ -237,15 +244,14 @@ void ACTION_Scan(bool bRestart) // channel mode. Keep scanning but toggle between scan lists RADIO_NextValidList(1); + UI_MAIN_NotifyScanProgressDataChanged(); #ifdef ENABLE_FEAT_F4HWN_RESUME_STATE SETTINGS_WriteCurrentState(); #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 @@ -343,22 +349,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 @@ -382,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 97d85839..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 @@ -158,8 +160,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; @@ -172,9 +176,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 +234,7 @@ bool AIRCOPY_SendMessage(void) g_FSK_Buffer[34] = CRC_Calculate(&g_FSK_Buffer[1], 2 + 64); - AIRCOPY_Obfuscation(); + AIRCOPY_Obfuscate(34); RADIO_SetTxParameters(); @@ -257,21 +261,18 @@ 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; } - AIRCOPY_Obfuscation(); + AIRCOPY_Obfuscate(34); 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 +281,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/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 7cfa3ca3..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 } } @@ -994,7 +1016,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); @@ -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; @@ -1560,7 +1597,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; } } @@ -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 @@ -1908,7 +1951,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; } @@ -2088,7 +2131,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) @@ -2103,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..0f024757 --- /dev/null +++ b/App/app/beam.c @@ -0,0 +1,325 @@ +/* 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. + */ + +#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 gBeamRadioVfo; + +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_SendPacket(void) +{ + 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 = 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 = vfo->DTMF_DECODING_ENABLE; +#endif + 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; + + if (IS_MR_CHANNEL(vfo->CHANNEL_SAVE)) { + SETTINGS_FetchChannelName(payload->name, vfo->CHANNEL_SAVE); + } else { + memcpy(payload->name, vfo->Name, sizeof(vfo->Name)); + } + + 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 void BEAM_SavePayloadToFirstFreeChannel(const BEAM_Payload_t *payload) +{ + 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; + } + } + + if (channel == 0xFFFFu) { + gBeamStatus = BEAM_STATUS_RX_FULL; + gUpdateDisplay = true; + return; + } + + 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, payload->name, sizeof(vfo.Name)); + vfo.Name[sizeof(vfo.Name) - 1] = '\0'; + + RADIO_ApplyOffset(&vfo); + RADIO_ConfigureSquelchAndOutputPower(&vfo); + + SETTINGS_SaveChannel(channel, gEeprom.TX_VFO, &vfo, 3); + SETTINGS_SaveChannelName(channel, vfo.Name); + + gBeamCopiedChannel = channel; + gBeamStatus = BEAM_STATUS_RX_SAVED; + gUpdateDisplay = true; + BACKLIGHT_TurnOn(); + return; +} + +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[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); + gBeamActive = false; +} + +void ACTION_Beam(void) +{ + gBeamMode = BEAM_MODE_TX; + gBeamStatus = BEAM_STATUS_READY; + gBeamCopiedChannel = 0xFFFFu; + gBeamRxWordCount = 0; + gBeamActive = true; + GUI_SelectNextDisplay(DISPLAY_MAIN); +} + +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 ^= 1; // (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 * const payload = (BEAM_Payload_t *)&g_FSK_Buffer[2]; + + 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; + BACKLIGHT_TurnOn(); +} + +#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/app/breakout.c b/App/app/breakout.c index cbbf4c3f..e9867a25 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,18 +63,16 @@ int randInt(int min, int max) { void reset(void) { ballCount = BALL_NUMBER; - levelCountBreackout = 1; + levelCountBreakout = 1; score = 0; } // PlayBeep void playBeep(uint16_t tone) { - BK4819_PlayTone(tone, true); // 500 Hz ON + BK4819_PrepareToPlayTone(true); AUDIO_AudioPathOn(); - BK4819_ExitTxMute(); - SYSTEM_DelayMs(100); - BK4819_EnterTxMute(); + BK4819_PlayToneRaw(tone, 100); AUDIO_AudioPathOff(); } @@ -191,10 +80,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 +95,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 +110,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 +142,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 +150,6 @@ void drawBall() { ballCount--; UI_DisplayClear(); drawScore(); - isBeep = true; tone = 800; if (ballCount < 0) { reset(); @@ -261,15 +157,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 +183,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 +195,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 +207,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 +246,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 +282,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 +316,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 +337,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... + } } -} \ No newline at end of file + + ST7565_BlitStatusLine(); // Blank status line + ST7565_BlitFullScreen(); + + #ifdef ENABLE_FEAT_F4HWN_SCREENSHOT + if(isPaused || swap == 0) + { + SCREENSHOT_Update(false); + } + #endif + } +} 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/chFrScanner.c b/App/app/chFrScanner.c index b64e0dd1..7206ee7e 100644 --- a/App/app/chFrScanner.c +++ b/App/app/chFrScanner.c @@ -1,9 +1,16 @@ +#include + #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; @@ -13,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 { @@ -36,13 +59,566 @@ 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_FEAT_F4HWN_SCAN_FASTER +static bool ScanFastEnabled(void) +{ + return gSetting_set_scn; +} +#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) +{ + 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 +#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 +#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 VFO_Info_t scanFastDisplayVfo; +static bool scanFastDisplayVfoValid; + +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; + scanFastDisplayVfoValid = false; +} + +static void ScanFastResetNoiseFloor(void) +{ + scanFastNoiseFloor = SCAN_FAST_RSSI_MAX; +} + +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); +} + +const VFO_Info_t *CHFRSCANNER_GetScanDisplayVfo(void) +{ + if (!ScanFastEnabled() || !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)) + { + scanFastDisplayVfoValid = false; + return false; + } + + scanFastDisplayVfo = gEeprom.VfoInfo[gEeprom.RX_VFO]; + + 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; + else + scanFastNoiseFloor = (uint16_t)((7u * scanFastNoiseFloor + rssi + 4u) >> 3); + + 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 + } + + 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 + +#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 = ScanRangeNextFrequency(); + 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 = ScanRangeNextFrequency(); + 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 = ScanFastReadCandidateRssi(); + 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) +{ + uint32_t frequency; + ModulationMode_t modulation; + + if (gRxVfo->SquelchOpenRSSIThresh == 0) + { + scanFastLastFullTuneCandidate = false; + return true; + } + + if (!ScanFastUpdateDisplayVfo(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); + + if (ScanFastIsCandidate(ScanFastReadCandidateRssi())) + { + 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; + 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 @@ -55,16 +631,21 @@ void CHFRSCANNER_Start(const bool storeBackupSettings, const int8_t scan_directi } RADIO_SelectVfos(); + CHFRSCANNER_AbortActiveReception(); 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)) { if(!RADIO_CheckValidList(gEeprom.SCAN_LIST_DEFAULT)) { RADIO_NextValidList(1); + UI_MAIN_NotifyScanProgressDataChanged(); } // channel mode @@ -93,6 +674,16 @@ void CHFRSCANNER_Start(const bool storeBackupSettings, const int8_t scan_directi gScanPauseMode = false; } +void CHFRSCANNER_ManualResume(const int8_t scan_direction) +{ + CHFRSCANNER_Start(false, scan_direction); + + gScanPauseDelayIn_10ms = (gRxVfo->SquelchOpenRSSIThresh == 0) + ? scan_pause_delay_in_3_10ms + : 1; + gScheduleScanListen = false; +} + /* void CHFRSCANNER_ContinueScanning(void) { @@ -119,6 +710,18 @@ void CHFRSCANNER_ContinueScanning(void) void CHFRSCANNER_ContinueScanning(void) { +#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER + if (scanFastLastFullTuneCandidate && + gCurrentFunction != FUNCTION_INCOMING && + !g_SquelchLost) + { + // 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 + if (gCurrentFunction == FUNCTION_INCOMING && (IS_FREQ_CHANNEL(gNextMrChannel) || gCurrentCodeType == CODE_TYPE_OFF)) { @@ -136,6 +739,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 +855,46 @@ static void NextFreqChannel(void) { #ifdef ENABLE_SCAN_RANGES if(gScanRangeStart) { - gRxVfo->freq_config_RX.Frequency = APP_SetFreqByStepAndLimits(gRxVfo, gScanStateDir, gScanRangeStart, gScanRangeStop); +#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER + if (ScanFastEnabled()) + { + 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 = ScanRangeNextFrequency(); + } + else + { + scanFastLastFullTuneCandidate = true; + } + } + else + { + scanFastLastFullTuneCandidate = false; + gRxVfo->freq_config_RX.Frequency = ScanRangeNextFrequency(); + } +#else + gRxVfo->freq_config_RX.Frequency = ScanRangeNextFrequency(); +#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 +995,58 @@ 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 + { // 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 } - +#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; //sprintf(str, "----> Chan %d\n", chan + 1); //LogUart(str); } - if (gNextMrChannel != prev_chan) +#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER + SetMemScanProgressChannel(gNextMrChannel); + + if (ScanFastEnabled() && !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 +1060,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/app/chFrScanner.h b/App/app/chFrScanner.h index 51b2c9a6..264ccc73 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; @@ -13,12 +15,18 @@ 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); 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); @@ -29,4 +37,4 @@ void CHFRSCANNER_ContinueScanning(void); extern uint32_t lastFoundFrqOrChanOld; #endif -#endif \ No newline at end of file +#endif 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/main.c b/App/app/main.c index 35e58e03..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 @@ -300,6 +317,7 @@ static void processFKeyFunction(const KEY_Code_t Key, const bool beep) if (gScanStateDir != SCAN_OFF) { RADIO_NextValidList(isKeyUp ? 1 : -1); + UI_MAIN_NotifyScanProgressDataChanged(); } else { // Adjust squelch: UP increments, DOWN decrements if (gSquelchLevelOriginal == 10) @@ -482,6 +500,7 @@ 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(); #ifdef ENABLE_FEAT_F4HWN_RESUME_STATE SETTINGS_WriteCurrentState(); #endif @@ -500,6 +519,7 @@ 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(); #ifdef ENABLE_FEAT_F4HWN_RESUME_STATE SETTINGS_WriteCurrentState(); @@ -652,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); } @@ -756,11 +752,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; @@ -1010,9 +1020,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; } diff --git a/App/app/menu.c b/App/app/menu.c index 63a33048..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; @@ -800,7 +806,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; @@ -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) @@ -1507,6 +1523,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 +1731,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 +1831,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 +1902,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 +2035,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; } diff --git a/App/app/spectrum.c b/App/app/spectrum.c index bc150502..5cc0982b 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 @@ -438,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; @@ -618,7 +640,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]); @@ -966,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; @@ -1509,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() @@ -1574,6 +1594,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 +1618,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(); } @@ -1660,8 +1675,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); @@ -1948,7 +1975,7 @@ static void RenderFreqInput() { UI_PrintString(freqInputString, 2, 127, 0, 8); } static void RenderStatus() { - memset(gStatusLine, 0, sizeof(gStatusLine)); + UI_StatusClear(); DrawStatus(); ST7565_BlitStatusLine(); } @@ -2570,8 +2597,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/audio.c b/App/audio.c index 2a61b6d3..42308e96 100644 --- a/App/audio.c +++ b/App/audio.c @@ -33,31 +33,32 @@ #include "settings.h" #include "ui/ui.h" +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 + [BEEP_880HZ_200MS] = {880, 200, 1 }, + [BEEP_880HZ_500MS] = {880, 500, 1 }, +#endif +#ifdef ENABLE_FEAT_F4HWN + [BEEP_400HZ_30MS] = {400, 30, 1 }, + [BEEP_500HZ_30MS] = {500, 30, 1 }, + [BEEP_600HZ_30MS] = {600, 30, 1 }, +#endif + [BEEP_880HZ_60MS_TRIPLE_BEEP] = {880, 60, 3 } +}; 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) @@ -66,6 +67,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); @@ -80,40 +84,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,7 +91,7 @@ void AUDIO_PlayBeep(BEEP_Type_t Beep) } #endif - BK4819_PlayTone(ToneFrequency, true); + BK4819_PrepareToPlayTone(true); SYSTEM_DelayMs(2); @@ -129,45 +99,12 @@ void AUDIO_PlayBeep(BEEP_Type_t Beep) 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; + 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); } - SYSTEM_DelayMs(Duration); - BK4819_EnterTxMute(); - SYSTEM_DelayMs(20); - AUDIO_AudioPathOff(); SYSTEM_DelayMs(5); @@ -182,7 +119,6 @@ void AUDIO_PlayBeep(BEEP_Type_t Beep) SYSTEM_DelayMs(10); #endif - if (gEnableSpeaker) AUDIO_AudioPathOn(); @@ -197,7 +133,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..b9c83a91 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, @@ -31,13 +38,12 @@ 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, 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/dcs.c b/App/dcs.c index a54f48b2..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" @@ -48,6 +49,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; @@ -115,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++; @@ -141,3 +149,13 @@ 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) +{ + return DCS_GetApprovedIndex(Option, ARRAY_SIZE(DCS_Options), ARRAY_SIZE(DCS_ExtraIdx), DCS_ExtraIdx); +} 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/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 diff --git a/App/driver/eeprom_compat.c b/App/driver/eeprom_compat.c index 59b5e874..b4cf7bf3 100644 --- a/App/driver/eeprom_compat.c +++ b/App/driver/eeprom_compat.c @@ -59,13 +59,19 @@ 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 _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/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/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/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/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/scheduler.c b/App/scheduler.c index 3d12fb22..0d2031a1 100644 --- a/App/scheduler.c +++ b/App/scheduler.c @@ -98,6 +98,38 @@ 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 (gSetting_set_scn + && 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/App/settings.c b/App/settings.c index ec2abeef..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; @@ -495,10 +499,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; @@ -583,6 +583,124 @@ 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; +} + +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) @@ -1013,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/settings.h b/App/settings.h index 77e22463..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; @@ -127,6 +130,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 }; @@ -307,9 +313,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/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/main.c b/App/ui/main.c index 576fa0fa..aa0f462b 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", @@ -108,6 +153,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) @@ -221,7 +267,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 +306,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 +552,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; } @@ -920,6 +970,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 +989,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 @@ -1198,7 +1249,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); @@ -1212,7 +1263,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); @@ -1297,14 +1348,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 @@ -1388,6 +1431,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; @@ -1747,6 +1796,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 = ""; @@ -1936,10 +1990,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; } @@ -1947,23 +2003,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 @@ -2036,10 +2092,18 @@ 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 (gScanStateDir != SCAN_OFF && UI_DrawScanProgress()) + if (!rx && gScanStateDir != SCAN_OFF && gKeypadLocked == 0) { center_line = CENTER_LINE_SCAN_PROGRESS; + UI_DrawScanProgress(); } else #endif 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 dbf076c8..e0737607 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 @@ -292,6 +295,9 @@ const char gSubMenu_PONMSG[][8] = #endif "MESSAGE", "VOLTAGE", +#ifdef ENABLE_FEAT_F4HWN_LOGO + "LOGO", +#endif "NONE" }; @@ -414,6 +420,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] = { @@ -497,6 +511,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 }; @@ -593,6 +610,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 +725,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 +1078,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]; @@ -1348,6 +1367,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) { @@ -1466,16 +1491,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,38 +1508,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, approved_index + 1); - else - sprintf(top_right_badge, "%02u/--", (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/00"); + else if (approved_index != 0xFF) + sprintf(top_right_badge, "%03u/%02u", (unsigned)gSubMenuSelection, (unsigned)approved_index + 1); + 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); diff --git a/App/ui/menu.h b/App/ui/menu.h index 18adae4f..10e8b7ff 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 @@ -205,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/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..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 @@ -189,7 +202,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,24 +222,41 @@ 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(); #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); + } #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)); diff --git a/CMakePresets.json b/CMakePresets.json index 7f3e04e2..abbeb628 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -69,11 +69,14 @@ "ENABLE_FEAT_F4HWN_QRCODE": false, "ENABLE_FEAT_F4HWN_MEM": false, "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, "VERSION_STRING_1": "v0.22", - "VERSION_STRING_2": "v5.4.0" + "VERSION_STRING_2": "v5.5.0" } }, { @@ -194,8 +197,11 @@ "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_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" @@ -232,4 +238,4 @@ "configurePreset": "Fusion" } ] -} \ No newline at end of file +} 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 00000000..f9e0782d Binary files /dev/null and b/archive/f4hwn.fusion.v5.5.0.bin differ diff --git a/archive/f4hwn.fusion.v5.5.0.sa818.bin b/archive/f4hwn.fusion.v5.5.0.sa818.bin new file mode 100755 index 00000000..455e433d Binary files /dev/null and b/archive/f4hwn.fusion.v5.5.0.sa818.bin differ diff --git a/archive/quansheng.stock.logo.png b/archive/quansheng.stock.logo.png new file mode 100644 index 00000000..fe3cc9e7 Binary files /dev/null and b/archive/quansheng.stock.logo.png differ