From b8936ace0e86aaa32bbba6da1c600fb0be1b337a Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Tue, 5 May 2026 04:35:03 +0200 Subject: [PATCH] Add beam feature behind ENABLE_FEAT_F4HWN_BEAM flag --- App/CMakeLists.txt | 9 +- App/app/action.c | 6 + App/app/aircopy.c | 8 +- App/app/aircopy.h | 4 + App/app/app.c | 68 ++++++++- App/app/beam.c | 363 +++++++++++++++++++++++++++++++++++++++++++++ App/app/beam.h | 53 +++++++ App/settings.h | 3 + App/ui/main.c | 52 +++++++ App/ui/main.h | 5 +- App/ui/menu.c | 3 + CMakePresets.json | 2 + 12 files changed, 562 insertions(+), 14 deletions(-) create mode 100644 App/app/beam.c create mode 100644 App/app/beam.h diff --git a/App/CMakeLists.txt b/App/CMakeLists.txt index 66a5dd0e..44fd71ed 100644 --- a/App/CMakeLists.txt +++ b/App/CMakeLists.txt @@ -123,7 +123,7 @@ enable_feature(ENABLE_FMRADIO ui/fmradio.c ) enable_feature(ENABLE_AIRCOPY - app/aircopy.c + app/aircopy.c ui/aircopy.c ) enable_feature(ENABLE_NOAA) @@ -206,6 +206,13 @@ enable_feature(ENABLE_FEAT_F4HWN_GMRS_FRS_MURS) enable_feature(ENABLE_FEAT_F4HWN_CA) enable_feature(ENABLE_FEAT_F4HWN_DEBUG) enable_feature(ENABLE_FEAT_F4HWN_MEM) +enable_feature(ENABLE_FEAT_F4HWN_BEAM + app/beam.c +) + +if(ENABLE_FEAT_F4HWN_BEAM AND NOT ENABLE_AIRCOPY) + message(FATAL_ERROR "ENABLE_FEAT_F4HWN_BEAM requires ENABLE_AIRCOPY (it reuses g_FSK_Buffer, AIRCOPY_Obfuscate and the FSK packet plumbing).") +endif() enable_feature(ENABLE_FEAT_F4HWN_QRCODE) # ---- DEBUGGING ---- diff --git a/App/app/action.c b/App/app/action.c index 680fe4a1..d399023b 100644 --- a/App/app/action.c +++ b/App/app/action.c @@ -45,6 +45,9 @@ #ifdef ENABLE_REGA #include "app/rega.h" #endif +#ifdef ENABLE_FEAT_F4HWN_BEAM + #include "app/beam.h" +#endif #if defined(ENABLE_FMRADIO) static void ACTION_Scan_FM(bool bRestart); @@ -132,6 +135,9 @@ void (*action_opt_table[])(void) = { [ACTION_OPT_REGA_ALARM] = &ACTION_RegaAlarm, [ACTION_OPT_REGA_TEST] = &ACTION_RegaTest, #endif +#ifdef ENABLE_FEAT_F4HWN_BEAM + [ACTION_OPT_BEAM] = &ACTION_Beam, +#endif }; static_assert(ARRAY_SIZE(action_opt_table) == ACTION_OPT_LEN); diff --git a/App/app/aircopy.c b/App/app/aircopy.c index 97d85839..ea8973d9 100644 --- a/App/app/aircopy.c +++ b/App/app/aircopy.c @@ -172,9 +172,9 @@ static inline void AIRCOPY_CheckComplete(void) } } -static inline void AIRCOPY_Obfuscation(void) +void AIRCOPY_Obfuscate(unsigned int count) { - for (unsigned int i = 0; i < 34; i++) { + for (unsigned int i = 0; i < count; i++) { g_FSK_Buffer[i + 1] ^= Obfuscation[i % 8]; } } @@ -230,7 +230,7 @@ bool AIRCOPY_SendMessage(void) g_FSK_Buffer[34] = CRC_Calculate(&g_FSK_Buffer[1], 2 + 64); - AIRCOPY_Obfuscation(); + AIRCOPY_Obfuscate(34); RADIO_SetTxParameters(); @@ -266,7 +266,7 @@ void AIRCOPY_StorePacket(void) return; } - AIRCOPY_Obfuscation(); + AIRCOPY_Obfuscate(34); uint16_t Crc = CRC_Calculate(&g_FSK_Buffer[1], 2 + 64); if (g_FSK_Buffer[34] != Crc) { diff --git a/App/app/aircopy.h b/App/app/aircopy.h index ce2a4032..f84f22d0 100644 --- a/App/app/aircopy.h +++ b/App/app/aircopy.h @@ -106,5 +106,9 @@ void AIRCOPY_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld); const AIRCOPY_TransferMap_t* AIRCOPY_GetCurrentMap(void); +// XOR-obfuscate `count` words of g_FSK_Buffer starting at index 1. +// Self-inverse: applying twice restores the original buffer. +void AIRCOPY_Obfuscate(unsigned int count); + #endif // ENABLE_AIRCOPY #endif // APP_AIRCOPY_H diff --git a/App/app/app.c b/App/app/app.c index 043f23b8..f404782e 100644 --- a/App/app/app.c +++ b/App/app/app.c @@ -24,6 +24,9 @@ #ifdef ENABLE_AIRCOPY #include "app/aircopy.h" #endif +#ifdef ENABLE_FEAT_F4HWN_BEAM + #include "app/beam.h" +#endif #include "app/app.h" #include "app/chFrScanner.h" #include "app/dtmf.h" @@ -796,6 +799,25 @@ static void CheckRadioInterrupts(void) AIRCOPY_StorePacket(); } #endif + +#ifdef ENABLE_FEAT_F4HWN_BEAM + if ((interrupts.fskFifoAlmostFull || interrupts.fskRxFinied) && + gBeamActive && + gBeamMode == BEAM_MODE_RX && + (gBeamStatus == BEAM_STATUS_RX_WAIT || gBeamStatus == BEAM_STATUS_ERROR)) + { + const unsigned int wordsToRead = interrupts.fskRxFinied ? (36 - gFSKWriteIndex) : 4; + for (unsigned int i = 0; i < wordsToRead; i++) { + const uint16_t word = BK4819_ReadRegister(BK4819_REG_5F); + if (gFSKWriteIndex < 36) + g_FSK_Buffer[gFSKWriteIndex++] = word; + } + + gBeamRxWordCount = gFSKWriteIndex; + gUpdateDisplay = true; + BEAM_StorePacket(); + } +#endif } } @@ -1044,6 +1066,9 @@ void APP_Update(void) && gScanStateDir == SCAN_OFF && !gPttIsPressed && gCurrentFunction != FUNCTION_POWER_SAVE +#ifdef ENABLE_FEAT_F4HWN_BEAM + && !gBeamActive +#endif #ifdef ENABLE_VOICE && gVoiceWriteIndex == 0 #endif @@ -1122,7 +1147,11 @@ void APP_Update(void) if (gEeprom.DUAL_WATCH != DUAL_WATCH_OFF && gScanStateDir == SCAN_OFF && - !gCssBackgroundScan) + !gCssBackgroundScan +#ifdef ENABLE_FEAT_F4HWN_BEAM + && !gBeamActive +#endif + ) { // dual watch mode, toggle between the two VFO's DualwatchAlternate(); goToSleep = false; @@ -1133,7 +1162,11 @@ void APP_Update(void) gPowerSave_10ms = power_save1_10ms; // come back here in a bit gRxIdleMode = false; // RX is awake } - else if (gEeprom.DUAL_WATCH == DUAL_WATCH_OFF || gScanStateDir != SCAN_OFF || gCssBackgroundScan || goToSleep) + else if ( +#ifdef ENABLE_FEAT_F4HWN_BEAM + !gBeamActive && +#endif + (gEeprom.DUAL_WATCH == DUAL_WATCH_OFF || gScanStateDir != SCAN_OFF || gCssBackgroundScan || goToSleep)) { // dual watch mode off or scanning or rssi update request // go back to sleep @@ -1152,7 +1185,11 @@ void APP_Update(void) // Authentic device checked removed } - else { + else +#ifdef ENABLE_FEAT_F4HWN_BEAM + if (!gBeamActive) +#endif + { // toggle between the two VFO's DualwatchAlternate(); gPowerSave_10ms = power_save1_10ms; @@ -1608,11 +1645,14 @@ void APP_TimeSlice500ms(void) gWakeUp = false; } + if(gCurrentFunction != FUNCTION_TRANSMIT && !FUNCTION_IsRx() #ifdef ENABLE_AIRCOPY - if(gCurrentFunction != FUNCTION_TRANSMIT && !FUNCTION_IsRx() && gScreenToDisplay != DISPLAY_AIRCOPY) - #else - if(gCurrentFunction != FUNCTION_TRANSMIT && !FUNCTION_IsRx()) + && gScreenToDisplay != DISPLAY_AIRCOPY #endif + #ifdef ENABLE_FEAT_F4HWN_BEAM + && !gBeamActive + #endif + ) { if (gSleepModeCountdown_500ms > 0 && --gSleepModeCountdown_500ms == 0) { gBacklightCountdown_500ms = 0; @@ -1698,6 +1738,9 @@ void APP_TimeSlice500ms(void) #endif #ifdef ENABLE_AIRCOPY && gScreenToDisplay != DISPLAY_AIRCOPY +#endif +#ifdef ENABLE_FEAT_F4HWN_BEAM + && !gBeamActive #endif ) { if (gEeprom.AUTO_KEYPAD_LOCK && gKeyLockCountdown > 0 && !gDTMF_InputMode @@ -2104,12 +2147,21 @@ static void ProcessKey(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) } #endif } - else if (gScreenToDisplay != DISPLAY_INVALID && ( + else if ( +#ifdef ENABLE_FEAT_F4HWN_BEAM + gBeamActive || +#endif + (gScreenToDisplay != DISPLAY_INVALID && ( (Key != KEY_SIDE1 && Key != KEY_SIDE2) #ifdef ENABLE_FEAT_F4HWN // For F + SIDE1 or F + SIDE2 || (gWasFKeyPressed && (Key == KEY_SIDE1 || Key == KEY_SIDE2)) #endif - )) { + ))) { +#ifdef ENABLE_FEAT_F4HWN_BEAM + if (gBeamActive) + BEAM_ProcessKeys(Key, bKeyPressed, bKeyHeld); + else +#endif ProcessKeysFunctions[gScreenToDisplay](Key, bKeyPressed, bKeyHeld); } else if (!SCANNER_IsScanning() diff --git a/App/app/beam.c b/App/app/beam.c new file mode 100644 index 00000000..08b9d169 --- /dev/null +++ b/App/app/beam.c @@ -0,0 +1,363 @@ +/* Copyright 2026 + * + * Licensed under the Apache License, Version 2.0. + */ + +#ifdef ENABLE_FEAT_F4HWN_BEAM + +#include +#include + +#include "app/aircopy.h" +#include "app/beam.h" +#include "audio.h" +#include "driver/bk4819.h" +#include "driver/crc.h" +#include "driver/st7565.h" +#include "frequencies.h" +#include "misc.h" +#include "radio.h" +#include "settings.h" +#include "ui/main.h" +#include "ui/ui.h" + +#define BEAM_PACKET_MAGIC 0xBEA5u +#define BEAM_PACKET_VERSION 2u + +// One byte per field instead of bit-packing — payload still fits in 64 bytes, +// and avoids costly shift/mask/store sequences on Cortex-M. +typedef struct { + uint16_t magic; + uint8_t version; + uint8_t _pad; + uint32_t rx_frequency; + uint32_t tx_offset_frequency; + uint8_t rx_code; + uint8_t tx_code; + uint8_t rx_codetype; + uint8_t tx_codetype; + uint8_t modulation; + uint8_t tx_offset_direction; + uint8_t tx_lock; + uint8_t busy_channel_lock; + uint8_t output_power; + uint8_t channel_bandwidth; + uint8_t frequency_reverse; + uint8_t dtmf_ptt_id_mode; + uint8_t dtmf_decoding_enable; + uint8_t step_setting; + uint8_t scrambling_type; + uint8_t band; + uint8_t scanlist; + uint8_t compander; + char name[16]; +} BEAM_Payload_t; + +static_assert(sizeof(BEAM_Payload_t) <= 64); + +BEAM_Mode_t gBeamMode = BEAM_MODE_TX; +BEAM_Status_t gBeamStatus = BEAM_STATUS_READY; +uint16_t gBeamCopiedChannel = 0xFFFFu; +uint8_t gBeamRxWordCount; +bool gBeamActive; + +static VFO_Info_t gBeamBackupVfo; +static VFO_Info_t gBeamRadioVfo; +static uint16_t gBeamBackupScreenChannel; +static uint8_t gBeamVfoIndex; +static uint8_t gBeamBackupRxVfoIndex; +static bool gBeamHasBackup; + +static void BEAM_SaveBackup(void) +{ + gBeamVfoIndex = gEeprom.TX_VFO; + memcpy(&gBeamBackupVfo, &gEeprom.VfoInfo[gBeamVfoIndex], sizeof(gBeamBackupVfo)); + gBeamBackupScreenChannel = gEeprom.ScreenChannel[gBeamVfoIndex]; + gBeamBackupRxVfoIndex = gEeprom.RX_VFO; + gBeamHasBackup = true; +} + +static void BEAM_RestoreBackup(void) +{ + if (!gBeamHasBackup) + return; + + memcpy(&gEeprom.VfoInfo[gBeamVfoIndex], &gBeamBackupVfo, sizeof(gBeamBackupVfo)); + gEeprom.ScreenChannel[gBeamVfoIndex] = gBeamBackupScreenChannel; + gEeprom.RX_VFO = gBeamBackupRxVfoIndex; + RADIO_SelectVfos(); + RADIO_SetupRegisters(true); + gBeamHasBackup = false; +} + +static void BEAM_SetRadioToBeamFrequency(void) +{ + const uint16_t channel = FREQ_CHANNEL_FIRST + BAND6_400MHz; + + RADIO_InitInfo(&gBeamRadioVfo, channel, DEFAULT_FREQ); + gBeamRadioVfo.CHANNEL_BANDWIDTH = BANDWIDTH_NARROW; + gBeamRadioVfo.OUTPUT_POWER = OUTPUT_POWER_LOW1; + RADIO_ConfigureSquelchAndOutputPower(&gBeamRadioVfo); + + gRxVfo = &gBeamRadioVfo; + gTxVfo = &gBeamRadioVfo; + gCurrentVfo = gRxVfo; + RADIO_SetupRegisters(true); + BK4819_SetupAircopy(); + BK4819_ResetFSK(); +} + +static void BEAM_CopyNameFromActiveVfo(char *name) +{ + memset(name, 0, 16); + + if (IS_MR_CHANNEL(gBeamBackupVfo.CHANNEL_SAVE)) { + SETTINGS_FetchChannelName(name, gBeamBackupVfo.CHANNEL_SAVE); + } else { + memcpy(name, gBeamBackupVfo.Name, sizeof(gBeamBackupVfo.Name)); + } +} + +static void BEAM_FillPayload(BEAM_Payload_t *payload) +{ + memset(payload, 0, sizeof(*payload)); + + payload->magic = BEAM_PACKET_MAGIC; + payload->version = BEAM_PACKET_VERSION; + payload->rx_frequency = gBeamBackupVfo.freq_config_RX.Frequency; + payload->tx_offset_frequency = gBeamBackupVfo.TX_OFFSET_FREQUENCY; + payload->rx_code = gBeamBackupVfo.freq_config_RX.Code; + payload->tx_code = gBeamBackupVfo.freq_config_TX.Code; + payload->rx_codetype = gBeamBackupVfo.freq_config_RX.CodeType; + payload->tx_codetype = gBeamBackupVfo.freq_config_TX.CodeType; + payload->modulation = gBeamBackupVfo.Modulation; + payload->tx_offset_direction = gBeamBackupVfo.TX_OFFSET_FREQUENCY_DIRECTION; + payload->tx_lock = gBeamBackupVfo.TX_LOCK; + payload->busy_channel_lock = gBeamBackupVfo.BUSY_CHANNEL_LOCK; + payload->output_power = gBeamBackupVfo.OUTPUT_POWER; + payload->channel_bandwidth = gBeamBackupVfo.CHANNEL_BANDWIDTH; + payload->frequency_reverse = gBeamBackupVfo.FrequencyReverse; + payload->dtmf_ptt_id_mode = gBeamBackupVfo.DTMF_PTT_ID_TX_MODE; +#ifdef ENABLE_DTMF_CALLING + payload->dtmf_decoding_enable = gBeamBackupVfo.DTMF_DECODING_ENABLE; +#endif + payload->step_setting = gBeamBackupVfo.STEP_SETTING; + payload->scrambling_type = gBeamBackupVfo.SCRAMBLING_TYPE; + payload->band = gBeamBackupVfo.Band; + payload->scanlist = gBeamBackupVfo.SCANLIST_PARTICIPATION; + payload->compander = gBeamBackupVfo.Compander; + + BEAM_CopyNameFromActiveVfo(payload->name); +} + +static void BEAM_SendPacket(void) +{ + BEAM_Payload_t payload; + + BEAM_FillPayload(&payload); + + memset(g_FSK_Buffer, 0, sizeof(g_FSK_Buffer)); + g_FSK_Buffer[0] = 0xABCDu; + memcpy(&g_FSK_Buffer[2], &payload, sizeof(payload)); + g_FSK_Buffer[34] = CRC_Calculate(&g_FSK_Buffer[1], 2 + 64); + g_FSK_Buffer[35] = 0xDCBAu; + + AIRCOPY_Obfuscate(32); + + // Show "SENDING" before the blocking FSK transmission so the user gets + // immediate visual feedback instead of a frozen "BEAM TX" screen. + gBeamStatus = BEAM_STATUS_TX_WAIT; + UI_DisplayMain(); + ST7565_BlitFullScreen(); + + RADIO_SetTxParameters(); + BK4819_SendFSKData(g_FSK_Buffer); + BK4819_SetupPowerAmplifier(0, 0); + BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, false); + + RADIO_SelectVfos(); + RADIO_SetupRegisters(true); + gBeamStatus = BEAM_STATUS_TX_DONE; + gBeepToPlay = BEEP_880HZ_60MS_TRIPLE_BEEP; + gUpdateDisplay = true; +} + +static uint16_t BEAM_FindFirstFreeChannel(void) +{ + for (uint16_t channel = MR_CHANNEL_FIRST; IS_MR_CHANNEL(channel); channel++) { + if (!RADIO_CheckValidChannel(channel, false, 0)) + return channel; + } + + return 0xFFFFu; +} + +static bool BEAM_SavePayloadToFirstFreeChannel(const BEAM_Payload_t *payload) +{ + uint16_t channel = BEAM_FindFirstFreeChannel(); + char name[16]; + + if (channel == 0xFFFFu) { + gBeamStatus = BEAM_STATUS_RX_FULL; + gUpdateDisplay = true; + return false; + } + + memcpy(name, payload->name, sizeof(name)); + name[sizeof(name) - 1] = 0; + + VFO_Info_t vfo; + RADIO_InitInfo(&vfo, channel, payload->rx_frequency); + + // CRC + magic + version already validate the payload — no need to clamp fields. + vfo.TX_OFFSET_FREQUENCY = payload->tx_offset_frequency; + vfo.freq_config_RX.Code = payload->rx_code; + vfo.freq_config_TX.Code = payload->tx_code; + vfo.freq_config_RX.CodeType = payload->rx_codetype; + vfo.freq_config_TX.CodeType = payload->tx_codetype; + vfo.TX_OFFSET_FREQUENCY_DIRECTION = payload->tx_offset_direction; + vfo.Modulation = payload->modulation; + vfo.TX_LOCK = payload->tx_lock; + vfo.BUSY_CHANNEL_LOCK = payload->busy_channel_lock; + vfo.OUTPUT_POWER = payload->output_power; + vfo.CHANNEL_BANDWIDTH = payload->channel_bandwidth; + vfo.FrequencyReverse = payload->frequency_reverse; + vfo.DTMF_PTT_ID_TX_MODE = payload->dtmf_ptt_id_mode; +#ifdef ENABLE_DTMF_CALLING + vfo.DTMF_DECODING_ENABLE = payload->dtmf_decoding_enable; +#endif + vfo.STEP_SETTING = payload->step_setting; + vfo.StepFrequency = gStepFrequencyTable[vfo.STEP_SETTING]; + vfo.SCRAMBLING_TYPE = payload->scrambling_type; + vfo.Band = payload->band; + vfo.SCANLIST_PARTICIPATION = payload->scanlist; + vfo.Compander = payload->compander; + memcpy(vfo.Name, name, sizeof(vfo.Name)); + RADIO_ApplyOffset(&vfo); + RADIO_ConfigureSquelchAndOutputPower(&vfo); + + SETTINGS_SaveChannel(channel, gBeamVfoIndex, &vfo, 3); + SETTINGS_SaveChannelName(channel, name); + + gBeamCopiedChannel = channel; + gBeamStatus = BEAM_STATUS_RX_SAVED; + gUpdateDisplay = true; + return true; +} + +static void BEAM_KeyMenu(void) +{ + BEAM_SetRadioToBeamFrequency(); + + if (gBeamMode == BEAM_MODE_TX) { + BEAM_SendPacket(); + } else { + gBeamStatus = BEAM_STATUS_RX_WAIT; + gBeamCopiedChannel = 0xFFFFu; + gBeamRxWordCount = 0; + gFSKWriteIndex = 0; + BK4819_PrepareFSKReceive(); + } +} + +static void BEAM_KeyExit(void) +{ + BK4819_ResetFSK(); + + if (gBeamMode == BEAM_MODE_RX && gBeamCopiedChannel != 0xFFFFu) { + gEeprom.MrChannel[gBeamVfoIndex] = gBeamCopiedChannel; + gEeprom.ScreenChannel[gBeamVfoIndex] = gBeamCopiedChannel; + RADIO_ConfigureChannel(gBeamVfoIndex, VFO_CONFIGURE_RELOAD); + RADIO_SelectVfos(); + RADIO_SetupRegisters(true); + gBeamHasBackup = false; + } else { + BEAM_RestoreBackup(); + } + + GUI_SelectNextDisplay(DISPLAY_MAIN); + gRequestDisplayScreen = DISPLAY_MAIN; + gBeamActive = false; + gUpdateDisplay = true; +} + +void ACTION_Beam(void) +{ + BEAM_SaveBackup(); + gBeamMode = BEAM_MODE_TX; + gBeamStatus = BEAM_STATUS_READY; + gBeamCopiedChannel = 0xFFFFu; + gBeamRxWordCount = 0; + gBeamActive = true; + GUI_SelectNextDisplay(DISPLAY_MAIN); + gRequestDisplayScreen = DISPLAY_MAIN; + gUpdateDisplay = true; +} + +void BEAM_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) +{ + if (bKeyHeld || !bKeyPressed) + return; + + if (Key != KEY_PTT) + gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL; + + switch (Key) { + case KEY_UP: + case KEY_DOWN: + gBeamMode = (gBeamMode == BEAM_MODE_TX) ? BEAM_MODE_RX : BEAM_MODE_TX; + gBeamStatus = BEAM_STATUS_READY; + gBeamRxWordCount = 0; + break; + case KEY_MENU: + BEAM_KeyMenu(); + break; + case KEY_EXIT: + BEAM_KeyExit(); + return; + case KEY_PTT: + break; + default: + gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL; + break; + } + + gUpdateDisplay = true; +} + +void BEAM_StorePacket(void) +{ + if (gFSKWriteIndex < 36) + return; + + gBeamRxWordCount = gFSKWriteIndex; + gFSKWriteIndex = 0; + + const uint16_t Status = BK4819_ReadRegister(BK4819_REG_0B); + BK4819_PrepareFSKReceive(); + + if ((Status & 0x0010U) != 0 || g_FSK_Buffer[0] != 0xABCDu || g_FSK_Buffer[35] != 0xDCBAu) + goto error; + + AIRCOPY_Obfuscate(32); + + if (g_FSK_Buffer[34] != CRC_Calculate(&g_FSK_Buffer[1], 2 + 64)) + goto error; + + BEAM_Payload_t payload; + memcpy(&payload, &g_FSK_Buffer[2], sizeof(payload)); + + if (payload.magic != BEAM_PACKET_MAGIC || payload.version != BEAM_PACKET_VERSION) + goto error; + + BEAM_SavePayloadToFirstFreeChannel(&payload); + BK4819_ResetFSK(); + return; + +error: + gBeamStatus = BEAM_STATUS_ERROR; + gBeamRxWordCount = 0; + gUpdateDisplay = true; +} + +#endif // ENABLE_FEAT_F4HWN_BEAM diff --git a/App/app/beam.h b/App/app/beam.h new file mode 100644 index 00000000..6571119e --- /dev/null +++ b/App/app/beam.h @@ -0,0 +1,53 @@ +/* Copyright 2026 Armel F4HWN + * https://github.com/armel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef APP_BEAM_H +#define APP_BEAM_H + +#ifdef ENABLE_FEAT_F4HWN_BEAM + +#include +#include + +#include "driver/keyboard.h" + +typedef enum { + BEAM_MODE_TX = 0, + BEAM_MODE_RX +} BEAM_Mode_t; + +typedef enum { + BEAM_STATUS_READY = 0, + BEAM_STATUS_TX_DONE, + BEAM_STATUS_RX_WAIT, + BEAM_STATUS_RX_SAVED, + BEAM_STATUS_RX_FULL, + BEAM_STATUS_ERROR, + BEAM_STATUS_TX_WAIT +} BEAM_Status_t; + +extern BEAM_Mode_t gBeamMode; +extern BEAM_Status_t gBeamStatus; +extern uint16_t gBeamCopiedChannel; +extern uint8_t gBeamRxWordCount; +extern bool gBeamActive; + +void ACTION_Beam(void); +void BEAM_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld); +void BEAM_StorePacket(void); + +#endif // ENABLE_FEAT_F4HWN_BEAM +#endif // APP_BEAM_H diff --git a/App/settings.h b/App/settings.h index b833c61d..81ffa637 100644 --- a/App/settings.h +++ b/App/settings.h @@ -127,6 +127,9 @@ enum ACTION_OPT_t { #ifdef ENABLE_REGA ACTION_OPT_REGA_ALARM, ACTION_OPT_REGA_TEST, +#endif +#ifdef ENABLE_FEAT_F4HWN_BEAM + ACTION_OPT_BEAM, #endif ACTION_OPT_LEN }; diff --git a/App/ui/main.c b/App/ui/main.c index 8c2c42a5..6353a856 100644 --- a/App/ui/main.c +++ b/App/ui/main.c @@ -19,6 +19,11 @@ #include "app/chFrScanner.h" #include "app/dtmf.h" + +#ifdef ENABLE_FEAT_F4HWN_BEAM + #include "app/beam.h" +#endif + #ifdef ENABLE_AM_FIX #include "am_fix.h" #endif @@ -84,6 +89,46 @@ static uint8_t gScanProgressPriorityState; #define SCAN_PROGRESS_PRIORITY_HOLD_FRAMES 6 #endif +#ifdef ENABLE_FEAT_F4HWN_BEAM +static void UI_MAIN_DrawBeamLine(void) +{ + const char *text; +#ifdef ENABLE_FEAT_F4HWN + const unsigned int line = isMainOnly() ? 5 : 3; +#else + const unsigned int line = 3; +#endif + + switch (gBeamStatus) { + case BEAM_STATUS_TX_WAIT: + text = "SENDING"; + break; + case BEAM_STATUS_TX_DONE: + text = "SENT"; + break; + case BEAM_STATUS_RX_WAIT: + text = "WAITING"; + break; + case BEAM_STATUS_RX_SAVED: + text = "RECEIVED"; + break; + case BEAM_STATUS_RX_FULL: + text = "MEM FULL"; + break; + case BEAM_STATUS_ERROR: + text = "ERROR"; + break; + case BEAM_STATUS_READY: + default: + text = (gBeamMode == BEAM_MODE_TX) ? "BEAM TX" : "BEAM RX"; + break; + } + + memset(gFrameBuffer[line], 0, LCD_WIDTH); + UI_PrintStringSmallBold(text, 2, 127, line); +} +#endif + const char *VfoStateStr[] = { [VFO_STATE_NORMAL]="", [VFO_STATE_BUSY]="BUSY", @@ -2047,6 +2092,13 @@ void UI_DisplayMain(void) const bool rx = FUNCTION_IsRx(); +#ifdef ENABLE_FEAT_F4HWN_BEAM + if (gBeamActive) { + center_line = CENTER_LINE_BEAM; + UI_MAIN_DrawBeamLine(); + } + else +#endif #ifdef ENABLE_FEAT_F4HWN_SCAN_PROGRESS if (!rx && gScanStateDir != SCAN_OFF && UI_DrawScanProgress()) { diff --git a/App/ui/main.h b/App/ui/main.h index 072612bd..43abb3aa 100644 --- a/App/ui/main.h +++ b/App/ui/main.h @@ -28,7 +28,10 @@ enum center_line_t { CENTER_LINE_RSSI, CENTER_LINE_AM_FIX_DATA, CENTER_LINE_DTMF_DEC, - CENTER_LINE_CHARGE_DATA + CENTER_LINE_CHARGE_DATA, +#ifdef ENABLE_FEAT_F4HWN_BEAM + CENTER_LINE_BEAM +#endif }; enum Vfo_txtr_mode{ diff --git a/App/ui/menu.c b/App/ui/menu.c index 38d74b1e..774afce0 100644 --- a/App/ui/menu.c +++ b/App/ui/menu.c @@ -487,6 +487,9 @@ const t_sidefunction gSubMenu_SIDEFUNCTIONS[] = #ifdef ENABLE_REGA {"REGA\nALARM", ACTION_OPT_REGA_ALARM}, {"REGA\nTEST", ACTION_OPT_REGA_TEST}, +#endif +#ifdef ENABLE_FEAT_F4HWN_BEAM + {"BEAM", ACTION_OPT_BEAM}, #endif {"LOCK\nKEYPAD", ACTION_OPT_KEYLOCK}, {"VFO A\nVFO B", ACTION_OPT_A_B}, diff --git a/CMakePresets.json b/CMakePresets.json index 1966b83a..444f422d 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -70,6 +70,7 @@ "ENABLE_FEAT_F4HWN_MEM": false, "ENABLE_FEAT_F4HWN_SCAN_PROGRESS": false, "ENABLE_FEAT_F4HWN_SCAN_FASTER": false, + "ENABLE_FEAT_F4HWN_BEAM": false, "ENABLE_AGC_SHOW_DATA": false, "ENABLE_UART_RW_BK_REGS": false, "ENABLE_SWD": false, @@ -196,6 +197,7 @@ "ENABLE_FEAT_F4HWN_AUDIO_SCOPE": true, "ENABLE_FEAT_F4HWN_SCAN_PROGRESS": true, "ENABLE_FEAT_F4HWN_SCAN_FASTER": true, + "ENABLE_FEAT_F4HWN_BEAM": true, "ENABLE_FEAT_F4HWN_QRCODE": true, "ENABLE_FEAT_F4HWN_MEM": true, "ENABLE_SWD": true,