UA1ZBE Custom Firmware v1.0

Features:
- VFO mode with direct frequency input
- POCSAG decoder (512/1200 baud) with BCH(31,21) correction
- Full-screen Spectrum analyzer
- FM Radio receiver
- RSSI signal indicator overlay
- Custom boot splash (UA1ZBE / POCSAG pager / build date)

Architecture:
- app/mode.c — mode dispatcher (VFO/POCSAG/Spectrum/FM)
- app/boot_splash.c — 2-second boot splash
- app/pocsag/ — POCSAG decoder + BCH correction
- app/display_rssi.c — RSSI indicator
- main.c — entry point with custom init
- syscalls.c — bare-metal _sbrk stub

Build: arm-none-eabi-gcc -Os -flto -Wall -Werror -Wextra
Size: 57.9KB Flash / 3.6KB RAM

Controls:
- 0-9: Direct frequency input (VFO)
- SK2: POCSAG mode
- SK1: Spectrum analyzer
- 0: FM Radio
- EXIT: Return to VFO
- F/*: Toggle 512/1200 baud (in POCSAG)
This commit is contained in:
2026-04-12 00:44:05 +03:00
parent 5c1229659b
commit a1206d6fd4
3435 changed files with 593221 additions and 2 deletions

453
app/action.c Normal file
View File

@@ -0,0 +1,453 @@
/* Copyright 2023 Dual Tachyon
* https://github.com/DualTachyon
*
* 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.
*/
#include <assert.h>
#include <string.h>
#include "app/action.h"
#include "app/app.h"
#include "app/chFrScanner.h"
#include "app/common.h"
#include "app/dtmf.h"
#ifdef ENABLE_FLASHLIGHT
#include "app/flashlight.h"
#endif
#ifdef ENABLE_FMRADIO
#include "app/fm.h"
#endif
#include "app/scanner.h"
#include "audio.h"
#include "bsp/dp32g030/gpio.h"
#ifdef ENABLE_FMRADIO
#include "driver/bk1080.h"
#endif
#include "driver/bk4819.h"
#include "driver/gpio.h"
#include "driver/backlight.h"
#include "functions.h"
#include "misc.h"
#include "settings.h"
#include "ui/inputbox.h"
#include "ui/ui.h"
#if defined(ENABLE_FMRADIO)
static void ACTION_Scan_FM(bool bRestart);
#endif
#if defined(ENABLE_ALARM) || defined(ENABLE_TX1750)
static void ACTION_AlarmOr1750(bool b1750);
inline static void ACTION_Alarm() { ACTION_AlarmOr1750(false); }
inline static void ACTION_1750() { ACTION_AlarmOr1750(true); };
#endif
#ifdef ENABLE_SPECTRUM
#include "app/spectrum.h"
#endif
inline static void ACTION_ScanRestart() { ACTION_Scan(true); };
void (*action_opt_table[])(void) = {
[ACTION_OPT_NONE] = &FUNCTION_NOP,
[ACTION_OPT_POWER] = &ACTION_Power,
[ACTION_OPT_MONITOR] = &ACTION_Monitor,
[ACTION_OPT_SCAN] = &ACTION_ScanRestart,
[ACTION_OPT_KEYLOCK] = &COMMON_KeypadLockToggle,
[ACTION_OPT_A_B] = &COMMON_SwitchVFOs,
[ACTION_OPT_VFO_MR] = &COMMON_SwitchVFOMode,
[ACTION_OPT_SWITCH_DEMODUL] = &ACTION_SwitchDemodul,
#ifdef ENABLE_FLASHLIGHT
[ACTION_OPT_FLASHLIGHT] = &ACTION_FlashLight,
#else
[ACTION_OPT_FLASHLIGHT] = &FUNCTION_NOP,
#endif
#ifdef ENABLE_VOX
[ACTION_OPT_VOX] = &ACTION_Vox,
#else
[ACTION_OPT_VOX] = &FUNCTION_NOP,
#endif
#ifdef ENABLE_FMRADIO
[ACTION_OPT_FM] = &ACTION_FM,
#else
[ACTION_OPT_FM] = &FUNCTION_NOP,
#endif
#ifdef ENABLE_ALARM
[ACTION_OPT_ALARM] = &ACTION_Alarm,
#else
[ACTION_OPT_ALARM] = &FUNCTION_NOP,
#endif
#ifdef ENABLE_TX1750
[ACTION_OPT_1750] = &ACTION_1750,
#else
[ACTION_OPT_1750] = &FUNCTION_NOP,
#endif
#ifdef ENABLE_BLMIN_TMP_OFF
[ACTION_OPT_BLMIN_TMP_OFF] = &ACTION_BlminTmpOff,
#else
[ACTION_OPT_BLMIN_TMP_OFF] = &FUNCTION_NOP,
#endif
#ifdef ENABLE_SPECTRUM
[ACTION_OPT_SPECTRUM] = &APP_RunSpectrum,
#else
[ACTION_OPT_SPECTRUM] = &FUNCTION_NOP,
#endif
};
static_assert(ARRAY_SIZE(action_opt_table) == ACTION_OPT_LEN);
void ACTION_Power(void)
{
if (++gTxVfo->OUTPUT_POWER > OUTPUT_POWER_HIGH)
gTxVfo->OUTPUT_POWER = OUTPUT_POWER_LOW;
gRequestSaveChannel = 1;
gRequestDisplayScreen = gScreenToDisplay;
#ifdef ENABLE_VOICE
gAnotherVoiceID = VOICE_ID_POWER;
#endif
}
void ACTION_Monitor(void)
{
if (gCurrentFunction != FUNCTION_MONITOR) { // enable the monitor
RADIO_SelectVfos();
#ifdef ENABLE_NOAA
if (IS_NOAA_CHANNEL(gRxVfo->CHANNEL_SAVE) && gIsNoaaMode)
gNoaaChannel = gRxVfo->CHANNEL_SAVE - NOAA_CHANNEL_FIRST;
#endif
RADIO_SetupRegisters(true);
APP_StartListening(FUNCTION_MONITOR);
return;
}
gMonitor = false;
if (gScanStateDir != SCAN_OFF) {
gScanPauseDelayIn_10ms = scan_pause_delay_in_1_10ms;
gScheduleScanListen = false;
gScanPauseMode = true;
}
#ifdef ENABLE_NOAA
if (gEeprom.DUAL_WATCH == DUAL_WATCH_OFF && gIsNoaaMode) {
gNOAA_Countdown_10ms = NOAA_countdown_10ms;
gScheduleNOAA = false;
}
#endif
RADIO_SetupRegisters(true);
#ifdef ENABLE_FMRADIO
if (gFmRadioMode) {
FM_Start();
gRequestDisplayScreen = DISPLAY_FM;
}
else
#endif
gRequestDisplayScreen = gScreenToDisplay;
}
void ACTION_Scan(bool bRestart)
{
(void)bRestart;
#ifdef ENABLE_FMRADIO
if (gFmRadioMode) {
ACTION_Scan_FM(bRestart);
return;
}
#endif
if (SCANNER_IsScanning()) {
return;
}
// not scanning
gMonitor = false;
#ifdef ENABLE_DTMF_CALLING
DTMF_clear_RX();
#endif
gDTMF_RX_live_timeout = 0;
memset(gDTMF_RX_live, 0, sizeof(gDTMF_RX_live));
RADIO_SelectVfos();
#ifdef ENABLE_NOAA
if (IS_NOAA_CHANNEL(gRxVfo->CHANNEL_SAVE)) {
return;
}
#endif
GUI_SelectNextDisplay(DISPLAY_MAIN);
if (gScanStateDir != SCAN_OFF) {
// already scanning
if (!IS_MR_CHANNEL(gNextMrChannel)) {
CHFRSCANNER_Stop();
#ifdef ENABLE_VOICE
gAnotherVoiceID = VOICE_ID_SCANNING_STOP;
#endif
return;
}
// channel mode. Keep scanning but toggle between scan lists
gEeprom.SCAN_LIST_DEFAULT = (gEeprom.SCAN_LIST_DEFAULT + 1) % 3;
// jump to the next channel
CHFRSCANNER_Start(false, gScanStateDir);
gScanPauseDelayIn_10ms = 1;
gScheduleScanListen = false;
} else {
// start scanning
CHFRSCANNER_Start(true, SCAN_FWD);
#ifdef ENABLE_VOICE
AUDIO_SetVoiceID(0, VOICE_ID_SCANNING_BEGIN);
AUDIO_PlaySingleVoice(true);
#endif
// clear the other vfo's rssi level (to hide the antenna symbol)
gVFO_RSSI_bar_level[(gEeprom.RX_VFO + 1) & 1U] = 0;
// let the user see DW is not active
gDualWatchActive = false;
}
gUpdateStatus = true;
}
void ACTION_SwitchDemodul(void)
{
gRequestSaveChannel = 1;
gTxVfo->Modulation++;
if(gTxVfo->Modulation == MODULATION_UKNOWN)
gTxVfo->Modulation = MODULATION_FM;
}
void ACTION_Handle(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
{
if (gScreenToDisplay == DISPLAY_MAIN && gDTMF_InputMode){
// entering DTMF code
gPttWasReleased = true;
if (Key != KEY_SIDE1 || bKeyHeld || !bKeyPressed){
return;
}
// side1 btn pressed
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
gRequestDisplayScreen = DISPLAY_MAIN;
if (gDTMF_InputBox_Index <= 0) {
// turn off DTMF input box if no codes left
gDTMF_InputMode = false;
return;
}
// DTMF codes are in the input box
gDTMF_InputBox[--gDTMF_InputBox_Index] = '-'; // delete one code
#ifdef ENABLE_VOICE
gAnotherVoiceID = VOICE_ID_CANCEL;
#endif
return;
}
enum ACTION_OPT_t funcShort = ACTION_OPT_NONE;
enum ACTION_OPT_t funcLong = ACTION_OPT_NONE;
switch(Key) {
case KEY_SIDE1:
funcShort = gEeprom.KEY_1_SHORT_PRESS_ACTION;
funcLong = gEeprom.KEY_1_LONG_PRESS_ACTION;
break;
case KEY_SIDE2:
funcShort = gEeprom.KEY_2_SHORT_PRESS_ACTION;
funcLong = gEeprom.KEY_2_LONG_PRESS_ACTION;
break;
case KEY_MENU:
funcLong = gEeprom.KEY_M_LONG_PRESS_ACTION;
break;
default:
break;
}
if (!bKeyHeld && bKeyPressed) // button pushed
{
return;
}
// held or released beyond this point
if(!(bKeyHeld && !bKeyPressed)) // don't beep on released after hold
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
if (bKeyHeld || bKeyPressed) // held
{
funcShort = funcLong;
if (!bKeyPressed) //ignore release if held
return;
}
// held or released after short press beyond this point
action_opt_table[funcShort]();
}
#ifdef ENABLE_FMRADIO
void ACTION_FM(void)
{
if (gCurrentFunction != FUNCTION_TRANSMIT && gCurrentFunction != FUNCTION_MONITOR)
{
gInputBoxIndex = 0;
if (gFmRadioMode) {
FM_TurnOff();
gFlagReconfigureVfos = true;
gRequestDisplayScreen = DISPLAY_MAIN;
#ifdef ENABLE_VOX
gVoxResumeCountdown = 80;
#endif
return;
}
gMonitor = false;
RADIO_SelectVfos();
RADIO_SetupRegisters(true);
FM_Start();
gRequestDisplayScreen = DISPLAY_FM;
}
}
static void ACTION_Scan_FM(bool bRestart)
{
if (FUNCTION_IsRx())
return;
GUI_SelectNextDisplay(DISPLAY_FM);
gMonitor = false;
if (gFM_ScanState != FM_SCAN_OFF) {
FM_PlayAndUpdate();
#ifdef ENABLE_VOICE
gAnotherVoiceID = VOICE_ID_SCANNING_STOP;
#endif
return;
}
uint16_t freq;
if (bRestart) {
gFM_AutoScan = true;
gFM_ChannelPosition = 0;
FM_EraseChannels();
freq = BK1080_GetFreqLoLimit(gEeprom.FM_Band);
} else {
gFM_AutoScan = false;
gFM_ChannelPosition = 0;
freq = gEeprom.FM_FrequencyPlaying;
}
BK1080_GetFrequencyDeviation(freq);
FM_Tune(freq, 1, bRestart);
#ifdef ENABLE_VOICE
gAnotherVoiceID = VOICE_ID_SCANNING_BEGIN;
#endif
}
#endif
#if defined(ENABLE_ALARM) || defined(ENABLE_TX1750)
static void ACTION_AlarmOr1750(const bool b1750)
{
#if defined(ENABLE_ALARM)
const AlarmState_t alarm_mode = (gEeprom.ALARM_MODE == ALARM_MODE_TONE) ? ALARM_STATE_TXALARM : ALARM_STATE_SITE_ALARM;
gAlarmRunningCounter = 0;
#endif
#if defined(ENABLE_ALARM) && defined(ENABLE_TX1750)
gAlarmState = b1750 ? ALARM_STATE_TX1750 : alarm_mode;
#elif defined(ENABLE_ALARM)
gAlarmState = alarm_mode;
#else
gAlarmState = ALARM_STATE_TX1750;
#endif
(void)b1750;
gInputBoxIndex = 0;
gFlagPrepareTX = gAlarmState != ALARM_STATE_OFF;
if (gScreenToDisplay != DISPLAY_MENU) // 1of11 .. don't close the menu
gRequestDisplayScreen = DISPLAY_MAIN;
}
#endif
#ifdef ENABLE_VOX
void ACTION_Vox(void)
{
gEeprom.VOX_SWITCH = !gEeprom.VOX_SWITCH;
gRequestSaveSettings = true;
gFlagReconfigureVfos = true;
gUpdateStatus = true;
#ifdef ENABLE_VOICE
gAnotherVoiceID = VOICE_ID_VOX;
#endif
}
#endif
#ifdef ENABLE_BLMIN_TMP_OFF
void ACTION_BlminTmpOff(void)
{
if(++gEeprom.BACKLIGHT_MIN_STAT == BLMIN_STAT_UNKNOWN) {
gEeprom.BACKLIGHT_MIN_STAT = BLMIN_STAT_ON;
BACKLIGHT_SetBrightness(gEeprom.BACKLIGHT_MIN);
} else {
BACKLIGHT_SetBrightness(0);
}
}
#endif

40
app/action.h Normal file
View File

@@ -0,0 +1,40 @@
/* Copyright 2023 Dual Tachyon
* https://github.com/DualTachyon
*
* 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_ACTION_H
#define APP_ACTION_H
#include "driver/keyboard.h"
void ACTION_Power(void);
void ACTION_Monitor(void);
void ACTION_Scan(bool bRestart);
#ifdef ENABLE_VOX
void ACTION_Vox(void);
#endif
#ifdef ENABLE_FMRADIO
void ACTION_FM(void);
#endif
void ACTION_SwitchDemodul(void);
#ifdef ENABLE_BLMIN_TMP_OFF
void ACTION_BlminTmpOff(void);
#endif
void ACTION_Handle(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld);
#endif

242
app/aircopy.c Normal file
View File

@@ -0,0 +1,242 @@
/* Copyright 2023 Dual Tachyon
* https://github.com/DualTachyon
*
* 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_AIRCOPY
#include "app/aircopy.h"
#include "audio.h"
#include "driver/bk4819.h"
#include "driver/crc.h"
#include "driver/eeprom.h"
#include "frequencies.h"
#include "misc.h"
#include "radio.h"
#include "ui/helper.h"
#include "ui/inputbox.h"
#include "ui/ui.h"
static const uint16_t Obfuscation[8] = { 0x6C16, 0xE614, 0x912E, 0x400D, 0x3521, 0x40D5, 0x0313, 0x80E9 };
AIRCOPY_State_t gAircopyState;
uint16_t gAirCopyBlockNumber;
uint16_t gErrorsDuringAirCopy;
uint8_t gAirCopyIsSendMode;
uint16_t g_FSK_Buffer[36];
bool AIRCOPY_SendMessage(void)
{
static uint8_t gAircopySendCountdown = 1;
if (gAircopyState != AIRCOPY_TRANSFER) {
return 1;
}
if (--gAircopySendCountdown) {
return 1;
}
g_FSK_Buffer[1] = (gAirCopyBlockNumber & 0x3FF) << 6;
EEPROM_ReadBuffer(g_FSK_Buffer[1], &g_FSK_Buffer[2], 64);
g_FSK_Buffer[34] = CRC_Calculate(&g_FSK_Buffer[1], 2 + 64);
for (unsigned int i = 0; i < 34; i++) {
g_FSK_Buffer[i + 1] ^= Obfuscation[i % 8];
}
if (++gAirCopyBlockNumber >= 0x78) {
gAircopyState = AIRCOPY_COMPLETE;
}
RADIO_SetTxParameters();
BK4819_SendFSKData(g_FSK_Buffer);
BK4819_SetupPowerAmplifier(0, 0);
BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, false);
gAircopySendCountdown = 30;
return 0;
}
void AIRCOPY_StorePacket(void)
{
if (gFSKWriteIndex < 36) {
return;
}
gFSKWriteIndex = 0;
gUpdateDisplay = true;
uint16_t Status = BK4819_ReadRegister(BK4819_REG_0B);
BK4819_PrepareFSKReceive();
// Doc says bit 4 should be 1 = CRC OK, 0 = CRC FAIL, but original firmware checks for FAIL.
if ((Status & 0x0010U) != 0 || g_FSK_Buffer[0] != 0xABCD || g_FSK_Buffer[35] != 0xDCBA) {
gErrorsDuringAirCopy++;
return;
}
for (unsigned int i = 0; i < 34; i++) {
g_FSK_Buffer[i + 1] ^= Obfuscation[i % 8];
}
uint16_t CRC = CRC_Calculate(&g_FSK_Buffer[1], 2 + 64);
if (g_FSK_Buffer[34] != CRC) {
gErrorsDuringAirCopy++;
return;
}
uint16_t Offset = g_FSK_Buffer[1];
if (Offset >= 0x1E00) {
gErrorsDuringAirCopy++;
return;
}
const uint16_t *pData = &g_FSK_Buffer[2];
for (unsigned int i = 0; i < 8; i++) {
EEPROM_WriteBuffer(Offset, pData);
pData += 4;
Offset += 8;
}
if (Offset == 0x1E00) {
gAircopyState = AIRCOPY_COMPLETE;
}
gAirCopyBlockNumber++;
}
static void AIRCOPY_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
{
if (bKeyHeld || !bKeyPressed) {
return;
}
INPUTBOX_Append(Key);
gRequestDisplayScreen = DISPLAY_AIRCOPY;
if (gInputBoxIndex < 6) {
#ifdef ENABLE_VOICE
gAnotherVoiceID = (VOICE_ID_t)Key;
#endif
return;
}
gInputBoxIndex = 0;
uint32_t Frequency = StrToUL(INPUTBOX_GetAscii()) * 100;
for (unsigned int i = 0; i < BAND_N_ELEM; i++) {
if (Frequency < frequencyBandTable[i].lower || Frequency >= frequencyBandTable[i].upper) {
continue;
}
if (TX_freq_check(Frequency)) {
continue;
}
#ifdef ENABLE_VOICE
gAnotherVoiceID = (VOICE_ID_t)Key;
#endif
Frequency = FREQUENCY_RoundToStep(Frequency, gRxVfo->StepFrequency);
gRxVfo->Band = i;
gRxVfo->freq_config_RX.Frequency = Frequency;
gRxVfo->freq_config_TX.Frequency = Frequency;
RADIO_ConfigureSquelchAndOutputPower(gRxVfo);
gCurrentVfo = gRxVfo;
RADIO_SetupRegisters(true);
BK4819_SetupAircopy();
BK4819_ResetFSK();
return;
}
gRequestDisplayScreen = DISPLAY_AIRCOPY;
}
static void AIRCOPY_Key_EXIT(bool bKeyPressed, bool bKeyHeld)
{
if (bKeyHeld || !bKeyPressed) {
return;
}
if (gInputBoxIndex == 0) {
gFSKWriteIndex = 0;
gAirCopyBlockNumber = 0;
gInputBoxIndex = 0;
gErrorsDuringAirCopy = 0;
gAirCopyIsSendMode = 0;
BK4819_PrepareFSKReceive();
gAircopyState = AIRCOPY_TRANSFER;
} else {
gInputBox[--gInputBoxIndex] = 10;
}
gRequestDisplayScreen = DISPLAY_AIRCOPY;
}
static void AIRCOPY_Key_MENU(bool bKeyPressed, bool bKeyHeld)
{
if (bKeyHeld || !bKeyPressed) {
return;
}
gFSKWriteIndex = 0;
gAirCopyBlockNumber = 0;
gInputBoxIndex = 0;
gAirCopyIsSendMode = 1;
g_FSK_Buffer[0] = 0xABCD;
g_FSK_Buffer[1] = 0;
g_FSK_Buffer[35] = 0xDCBA;
GUI_DisplayScreen();
gAircopyState = AIRCOPY_TRANSFER;
}
void AIRCOPY_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
{
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:
AIRCOPY_Key_DIGITS(Key, bKeyPressed, bKeyHeld);
break;
case KEY_MENU:
AIRCOPY_Key_MENU(bKeyPressed, bKeyHeld);
break;
case KEY_EXIT:
AIRCOPY_Key_EXIT(bKeyPressed, bKeyHeld);
break;
default:
break;
}
}
#endif

46
app/aircopy.h Normal file
View File

@@ -0,0 +1,46 @@
/* Copyright 2023 Dual Tachyon
* https://github.com/DualTachyon
*
* 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_AIRCOPY_H
#define APP_AIRCOPY_H
#ifdef ENABLE_AIRCOPY
#include "driver/keyboard.h"
enum AIRCOPY_State_t
{
AIRCOPY_READY = 0,
AIRCOPY_TRANSFER,
AIRCOPY_COMPLETE
};
typedef enum AIRCOPY_State_t AIRCOPY_State_t;
extern AIRCOPY_State_t gAircopyState;
extern uint16_t gAirCopyBlockNumber;
extern uint16_t gErrorsDuringAirCopy;
extern uint8_t gAirCopyIsSendMode;
extern uint16_t g_FSK_Buffer[36];
bool AIRCOPY_SendMessage(void);
void AIRCOPY_StorePacket(void);
void AIRCOPY_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld);
#endif
#endif

1888
app/app.c Normal file

File diff suppressed because it is too large Load Diff

35
app/app.h Normal file
View File

@@ -0,0 +1,35 @@
/* Copyright 2023 Dual Tachyon
* https://github.com/DualTachyon
*
* 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_APP_H
#define APP_APP_H
#include <stdbool.h>
#include "functions.h"
#include "frequencies.h"
#include "radio.h"
void APP_EndTransmission(void);
void APP_StartListening(FUNCTION_Type_t function);
uint32_t APP_SetFreqByStepAndLimits(VFO_Info_t *pInfo, int8_t direction, uint32_t lower, uint32_t upper);
uint32_t APP_SetFrequencyByStep(VFO_Info_t *pInfo, int8_t direction);
void APP_Update(void);
void APP_TimeSlice10ms(void);
void APP_TimeSlice500ms(void);
#endif

53
app/boot_splash.c Normal file
View File

@@ -0,0 +1,53 @@
/* UA1ZBE Custom Firmware - Boot Splash Screen
* Displays:
* Line 1 (top, large): UA1ZBE
* Line 2 (center): POCSAG pager
* Line 3 (bottom): YYYY-MM-DD (build date)
* Then fades to VFO mode after 2 seconds.
*/
#include <string.h>
#include "driver/st7565.h"
#include "driver/system.h"
#include "ui/helper.h"
#include "font.h"
#ifndef BUILD_DATE
#define BUILD_DATE "unknown"
#endif
void BOOT_SplashShow(void)
{
/* Clear screen */
ST7565_FillScreen(0x00);
memset(gStatusLine, 0, sizeof(gStatusLine));
for (int line = 0; line < FRAME_LINES; line++)
memset(gFrameBuffer[line], 0, LCD_WIDTH);
/* Line 1: UA1ZBE — large font, centered, top */
UI_PrintString("UA1ZBE", 0, LCD_WIDTH, 0, 12);
/* Line 2: "POCSAG pager" — small font, centered */
/* Small font is 6 pixels tall, at y-offset line 3 (~24px) */
const char *line2 = "POCSAG pager";
int len2 = 0;
while (line2[len2]) len2++;
int x2 = (LCD_WIDTH - len2 * 6) / 2;
if (x2 < 0) x2 = 0;
UI_PrintStringSmallNormal(line2, x2, LCD_WIDTH, 24);
/* Line 3: BUILD_DATE — small font, centered, bottom */
const char *date_str = BUILD_DATE;
int len3 = 0;
while (date_str[len3]) len3++;
int x3 = (LCD_WIDTH - len3 * 6) / 2;
if (x3 < 0) x3 = 0;
UI_PrintStringSmallNormal(date_str, x3, LCD_WIDTH, 48);
/* Blit to screen */
ST7565_BlitStatusLine();
ST7565_BlitFullScreen();
/* Hold for 2 seconds */
SYSTEM_DelayMs(2000);
}

7
app/boot_splash.h Normal file
View File

@@ -0,0 +1,7 @@
/* UA1ZBE Custom Firmware - Boot Splash Screen */
#ifndef APP_BOOT_SPLASH_H
#define APP_BOOT_SPLASH_H
void BOOT_SplashShow(void);
#endif

273
app/chFrScanner.c Normal file
View File

@@ -0,0 +1,273 @@
#include "app/app.h"
#include "app/chFrScanner.h"
#include "functions.h"
#include "misc.h"
#include "settings.h"
int8_t gScanStateDir;
bool gScanKeepResult;
bool gScanPauseMode;
#ifdef ENABLE_SCAN_RANGES
uint32_t gScanRangeStart;
uint32_t gScanRangeStop;
#endif
typedef enum {
SCAN_NEXT_CHAN_SCANLIST1 = 0,
SCAN_NEXT_CHAN_SCANLIST2,
SCAN_NEXT_CHAN_DUAL_WATCH,
SCAN_NEXT_CHAN_MR,
SCAN_NEXT_NUM
} scan_next_chan_t;
scan_next_chan_t currentScanList;
uint32_t initialFrqOrChan;
uint8_t initialCROSS_BAND_RX_TX;
uint32_t lastFoundFrqOrChan;
static void NextFreqChannel(void);
static void NextMemChannel(void);
void CHFRSCANNER_Start(const bool storeBackupSettings, const int8_t scan_direction)
{
if (storeBackupSettings) {
initialCROSS_BAND_RX_TX = gEeprom.CROSS_BAND_RX_TX;
gEeprom.CROSS_BAND_RX_TX = CROSS_BAND_OFF;
gScanKeepResult = false;
}
RADIO_SelectVfos();
gNextMrChannel = gRxVfo->CHANNEL_SAVE;
currentScanList = SCAN_NEXT_CHAN_SCANLIST1;
gScanStateDir = scan_direction;
if (IS_MR_CHANNEL(gNextMrChannel))
{ // channel mode
if (storeBackupSettings) {
initialFrqOrChan = gRxVfo->CHANNEL_SAVE;
lastFoundFrqOrChan = initialFrqOrChan;
}
NextMemChannel();
}
else
{ // frequency mode
if (storeBackupSettings) {
initialFrqOrChan = gRxVfo->freq_config_RX.Frequency;
lastFoundFrqOrChan = initialFrqOrChan;
}
NextFreqChannel();
}
gScanPauseDelayIn_10ms = scan_pause_delay_in_2_10ms;
gScheduleScanListen = false;
gRxReceptionMode = RX_MODE_NONE;
gScanPauseMode = false;
}
void CHFRSCANNER_ContinueScanning(void)
{
if (IS_FREQ_CHANNEL(gNextMrChannel))
{
if (gCurrentFunction == FUNCTION_INCOMING)
APP_StartListening(gMonitor ? FUNCTION_MONITOR : FUNCTION_RECEIVE);
else
NextFreqChannel(); // switch to next frequency
}
else
{
if (gCurrentCodeType == CODE_TYPE_OFF && gCurrentFunction == FUNCTION_INCOMING)
APP_StartListening(gMonitor ? FUNCTION_MONITOR : FUNCTION_RECEIVE);
else
NextMemChannel(); // switch to next channel
}
gScanPauseMode = false;
gRxReceptionMode = RX_MODE_NONE;
gScheduleScanListen = false;
}
void CHFRSCANNER_Found(void)
{
switch (gEeprom.SCAN_RESUME_MODE)
{
case SCAN_RESUME_TO:
if (!gScanPauseMode)
{
gScanPauseDelayIn_10ms = scan_pause_delay_in_1_10ms;
gScheduleScanListen = false;
gScanPauseMode = true;
}
break;
case SCAN_RESUME_CO:
case SCAN_RESUME_SE:
gScanPauseDelayIn_10ms = 0;
gScheduleScanListen = false;
break;
}
if (IS_MR_CHANNEL(gRxVfo->CHANNEL_SAVE)) { //memory scan
lastFoundFrqOrChan = gRxVfo->CHANNEL_SAVE;
}
else { // frequency scan
lastFoundFrqOrChan = gRxVfo->freq_config_RX.Frequency;
}
gScanKeepResult = true;
}
void CHFRSCANNER_Stop(void)
{
if(initialCROSS_BAND_RX_TX != CROSS_BAND_OFF) {
gEeprom.CROSS_BAND_RX_TX = initialCROSS_BAND_RX_TX;
initialCROSS_BAND_RX_TX = CROSS_BAND_OFF;
}
gScanStateDir = SCAN_OFF;
const uint32_t chFr = gScanKeepResult ? lastFoundFrqOrChan : initialFrqOrChan;
const bool channelChanged = chFr != initialFrqOrChan;
if (IS_MR_CHANNEL(gNextMrChannel)) {
gEeprom.MrChannel[gEeprom.RX_VFO] = chFr;
gEeprom.ScreenChannel[gEeprom.RX_VFO] = chFr;
RADIO_ConfigureChannel(gEeprom.RX_VFO, VFO_CONFIGURE_RELOAD);
if(channelChanged) {
SETTINGS_SaveVfoIndices();
gUpdateStatus = true;
}
}
else {
gRxVfo->freq_config_RX.Frequency = chFr;
RADIO_ApplyOffset(gRxVfo);
RADIO_ConfigureSquelchAndOutputPower(gRxVfo);
if(channelChanged) {
SETTINGS_SaveChannel(gRxVfo->CHANNEL_SAVE, gEeprom.RX_VFO, gRxVfo, 1);
}
}
RADIO_SetupRegisters(true);
gUpdateDisplay = true;
}
static void NextFreqChannel(void)
{
#ifdef ENABLE_SCAN_RANGES
if(gScanRangeStart) {
gRxVfo->freq_config_RX.Frequency = APP_SetFreqByStepAndLimits(gRxVfo, gScanStateDir, gScanRangeStart, gScanRangeStop);
}
else
#endif
gRxVfo->freq_config_RX.Frequency = APP_SetFrequencyByStep(gRxVfo, gScanStateDir);
RADIO_ApplyOffset(gRxVfo);
RADIO_ConfigureSquelchAndOutputPower(gRxVfo);
RADIO_SetupRegisters(true);
#ifdef ENABLE_FASTER_CHANNEL_SCAN
gScanPauseDelayIn_10ms = 9; // 90ms
#else
gScanPauseDelayIn_10ms = scan_pause_delay_in_6_10ms;
#endif
gUpdateDisplay = true;
}
static void NextMemChannel(void)
{
static unsigned int prev_mr_chan = 0;
const bool enabled = (gEeprom.SCAN_LIST_DEFAULT < 2) ? gEeprom.SCAN_LIST_ENABLED[gEeprom.SCAN_LIST_DEFAULT] : true;
const int chan1 = (gEeprom.SCAN_LIST_DEFAULT < 2) ? gEeprom.SCANLIST_PRIORITY_CH1[gEeprom.SCAN_LIST_DEFAULT] : -1;
const int chan2 = (gEeprom.SCAN_LIST_DEFAULT < 2) ? gEeprom.SCANLIST_PRIORITY_CH2[gEeprom.SCAN_LIST_DEFAULT] : -1;
const unsigned int prev_chan = gNextMrChannel;
unsigned int chan = 0;
if (enabled)
{
switch (currentScanList)
{
case SCAN_NEXT_CHAN_SCANLIST1:
prev_mr_chan = gNextMrChannel;
if (chan1 >= 0)
{
if (RADIO_CheckValidChannel(chan1, false, 0))
{
currentScanList = SCAN_NEXT_CHAN_SCANLIST1;
gNextMrChannel = chan1;
break;
}
}
__attribute__((fallthrough));
case SCAN_NEXT_CHAN_SCANLIST2:
if (chan2 >= 0)
{
if (RADIO_CheckValidChannel(chan2, false, 0))
{
currentScanList = SCAN_NEXT_CHAN_SCANLIST2;
gNextMrChannel = chan2;
break;
}
}
__attribute__((fallthrough));
// this bit doesn't yet work if the other VFO is a frequency
case SCAN_NEXT_CHAN_DUAL_WATCH:
// dual watch is enabled - include the other VFO in the scan
// if (gEeprom.DUAL_WATCH != DUAL_WATCH_OFF)
// {
// chan = (gEeprom.RX_VFO + 1) & 1u;
// chan = gEeprom.ScreenChannel[chan];
// if (IS_MR_CHANNEL(chan))
// {
// currentScanList = SCAN_NEXT_CHAN_DUAL_WATCH;
// gNextMrChannel = chan;
// break;
// }
// }
default:
case SCAN_NEXT_CHAN_MR:
currentScanList = SCAN_NEXT_CHAN_MR;
gNextMrChannel = prev_mr_chan;
chan = 0xff;
break;
}
}
if (!enabled || chan == 0xff)
{
chan = RADIO_FindNextChannel(gNextMrChannel + gScanStateDir, gScanStateDir, (gEeprom.SCAN_LIST_DEFAULT < 2) ? true : false, gEeprom.SCAN_LIST_DEFAULT);
if (chan == 0xFF)
{ // no valid channel found
chan = MR_CHANNEL_FIRST;
}
gNextMrChannel = chan;
}
if (gNextMrChannel != prev_chan)
{
gEeprom.MrChannel[ gEeprom.RX_VFO] = gNextMrChannel;
gEeprom.ScreenChannel[gEeprom.RX_VFO] = gNextMrChannel;
RADIO_ConfigureChannel(gEeprom.RX_VFO, VFO_CONFIGURE_RELOAD);
RADIO_SetupRegisters(true);
gUpdateDisplay = true;
}
#ifdef ENABLE_FASTER_CHANNEL_SCAN
gScanPauseDelayIn_10ms = 9; // 90ms .. <= ~60ms it misses signals (squelch response and/or PLL lock time) ?
#else
gScanPauseDelayIn_10ms = scan_pause_delay_in_3_10ms;
#endif
if (enabled)
if (++currentScanList >= SCAN_NEXT_NUM)
currentScanList = SCAN_NEXT_CHAN_SCANLIST1; // back round we go
}

23
app/chFrScanner.h Normal file
View File

@@ -0,0 +1,23 @@
#ifndef APP_CHFRSCANNER_H
#define APP_CHFRSCANNER_H
#include <stdbool.h>
#include <stdint.h>
// scan direction, if not equal SCAN_OFF indicates
// that we are in a process of scanning channels/frequencies
extern int8_t gScanStateDir;
extern bool gScanKeepResult;
extern bool gScanPauseMode;
#ifdef ENABLE_SCAN_RANGES
extern uint32_t gScanRangeStart;
extern uint32_t gScanRangeStop;
#endif
void CHFRSCANNER_Found(void);
void CHFRSCANNER_Stop(void);
void CHFRSCANNER_Start(const bool storeBackupSettings, const int8_t scan_direction);
void CHFRSCANNER_ContinueScanning(void);
#endif

77
app/common.c Normal file
View File

@@ -0,0 +1,77 @@
#include "app/chFrScanner.h"
#include "audio.h"
#include "functions.h"
#include "misc.h"
#include "settings.h"
#include "ui/ui.h"
void COMMON_KeypadLockToggle()
{
if (gScreenToDisplay != DISPLAY_MENU &&
gCurrentFunction != FUNCTION_TRANSMIT)
{ // toggle the keyboad lock
#ifdef ENABLE_VOICE
gAnotherVoiceID = gEeprom.KEY_LOCK ? VOICE_ID_UNLOCK : VOICE_ID_LOCK;
#endif
gEeprom.KEY_LOCK = !gEeprom.KEY_LOCK;
gRequestSaveSettings = true;
}
}
void COMMON_SwitchVFOs()
{
#ifdef ENABLE_SCAN_RANGES
gScanRangeStart = 0;
#endif
gEeprom.TX_VFO ^= 1;
if (gEeprom.CROSS_BAND_RX_TX != CROSS_BAND_OFF)
gEeprom.CROSS_BAND_RX_TX = gEeprom.TX_VFO + 1;
if (gEeprom.DUAL_WATCH != DUAL_WATCH_OFF)
gEeprom.DUAL_WATCH = gEeprom.TX_VFO + 1;
gRequestSaveSettings = 1;
gFlagReconfigureVfos = true;
gScheduleDualWatch = true;
gRequestDisplayScreen = DISPLAY_MAIN;
}
void COMMON_SwitchVFOMode()
{
#ifdef ENABLE_NOAA
if (gEeprom.VFO_OPEN && !IS_NOAA_CHANNEL(gTxVfo->CHANNEL_SAVE))
#else
if (gEeprom.VFO_OPEN)
#endif
{
if (IS_MR_CHANNEL(gTxVfo->CHANNEL_SAVE))
{ // swap to frequency mode
gEeprom.ScreenChannel[gEeprom.TX_VFO] = gEeprom.FreqChannel[gEeprom.TX_VFO];
#ifdef ENABLE_VOICE
gAnotherVoiceID = VOICE_ID_FREQUENCY_MODE;
#endif
gRequestSaveVFO = true;
gVfoConfigureMode = VFO_CONFIGURE_RELOAD;
return;
}
uint8_t Channel = RADIO_FindNextChannel(gEeprom.MrChannel[gEeprom.TX_VFO], 1, false, 0);
if (Channel != 0xFF)
{ // swap to channel mode
gEeprom.ScreenChannel[gEeprom.TX_VFO] = Channel;
#ifdef ENABLE_VOICE
AUDIO_SetVoiceID(0, VOICE_ID_CHANNEL_MODE);
AUDIO_SetDigitVoice(1, Channel + 1);
gAnotherVoiceID = (VOICE_ID_t)0xFE;
#endif
gRequestSaveVFO = true;
gVfoConfigureMode = VFO_CONFIGURE_RELOAD;
return;
}
}
}

13
app/common.h Normal file
View File

@@ -0,0 +1,13 @@
#ifndef APP_COMMON_H
#define APP_COMMON_H
#include "functions.h"
#include "settings.h"
#include "ui/ui.h"
void COMMON_KeypadLockToggle();
void COMMON_SwitchVFOs();
void COMMON_SwitchVFOMode();
#endif

128
app/display_rssi.c Normal file
View File

@@ -0,0 +1,128 @@
/* UA1ZBE Custom Firmware - RSSI Display Implementation
*
* Reads RSSI from BK4819 REG_67 (9-bit value).
* Conversion: dBm = (raw / 2) - 160 + band_correction
*
* Displays as "S:XX" format with optional mini bar.
* Updates every ~150ms via internal counter.
*/
#include "display_rssi.h"
#include "driver/bk4819.h"
#include "driver/st7565.h"
#include "ui/helper.h"
#include "ui/main.h"
#include "misc.h"
#include "radio.h"
#include <string.h>
#include <stdio.h>
/* dBm correction table — defined in ui/main.c */
extern const int8_t dBmCorrTable[7];
/* === Internal state === */
static int16_t s_rssi_dbm;
static uint16_t s_rssi_raw;
static uint8_t s_s_level;
static uint16_t s_update_counter;
/* Update interval: ~15 (150ms at 10ms tick) */
#define RSSI_UPDATE_INTERVAL 15
/* S-meter thresholds (dBm) — simplified from U8RssiMap */
static const int8_t s_thresholds[] = {
-121, -115, -109, -103, -97, -91, -85, -79, -73, -63
};
void RSSI_Init(void)
{
s_rssi_dbm = -160;
s_rssi_raw = 0;
s_s_level = 0;
s_update_counter = 0;
}
void RSSI_Update(void)
{
s_update_counter++;
if (s_update_counter < RSSI_UPDATE_INTERVAL)
return;
s_update_counter = 0;
/* Read raw RSSI from BK4819 */
s_rssi_raw = BK4819_GetRSSI();
/* Convert to dBm: (raw / 2) - 160 + band_correction
* Avoid division: raw >> 1 */
int8_t band_corr = dBmCorrTable[gRxVfo->Band];
s_rssi_dbm = (int16_t)(s_rssi_raw >> 1) - 160 + band_corr;
/* Calculate S-level */
s_s_level = 0;
for (uint8_t i = 0; i < ARRAY_SIZE(s_thresholds); i++) {
if (s_rssi_dbm >= s_thresholds[i]) {
s_s_level = i + 1;
}
}
if (s_s_level > 9)
s_s_level = 9;
}
int16_t RSSI_GetdBm(void)
{
return s_rssi_dbm;
}
uint16_t RSSI_GetRaw(void)
{
return s_rssi_raw;
}
uint8_t RSSI_GetSLevel(void)
{
return s_s_level;
}
/*
* Draw RSSI indicator.
* Format: "S:42" or raw dBm with mini bar.
* Uses small font to fit in upper-right corner.
*/
void RSSI_Draw(int x, int y, bool with_bar)
{
static char buf[16];
/* Draw dBm value: "-87dBm" */
sprintf(buf, "%ddBm", s_rssi_dbm);
UI_PrintStringSmallNormal(buf, x, LCD_WIDTH, y);
if (with_bar) {
/* Draw mini signal bar below the text
* 5 bars, each 2px wide, 1px gap */
int bar_x = x;
int bar_y = y + 8;
uint8_t bars = (s_s_level + 1) / 2; /* 0-5 bars from S0-S9 */
if (bars > 5) bars = 5;
for (uint8_t i = 0; i < 5; i++) {
uint8_t h = 2 + i * 2; /* Bar heights: 2,4,6,8,10 */
if (i < bars) {
/* Draw filled bar */
for (uint8_t py = 0; py < h; py++) {
for (uint8_t px = 0; px < 2; px++) {
UI_DrawPixelBuffer(gFrameBuffer, bar_x + i * 3 + px, bar_y + (10 - h) + py, true);
}
}
} else {
/* Draw outline only */
UI_DrawPixelBuffer(gFrameBuffer, bar_x + i * 3, bar_y + (10 - h), false);
UI_DrawPixelBuffer(gFrameBuffer, bar_x + i * 3 + 1, bar_y + (10 - h), false);
UI_DrawPixelBuffer(gFrameBuffer, bar_x + i * 3, bar_y + 9, false);
UI_DrawPixelBuffer(gFrameBuffer, bar_x + i * 3 + 1, bar_y + 9, false);
UI_DrawPixelBuffer(gFrameBuffer, bar_x + i * 3, bar_y + (10 - h) + 1, false);
UI_DrawPixelBuffer(gFrameBuffer, bar_x + i * 3 + 1, bar_y + (10 - h) + 1, false);
}
}
}
}

36
app/display_rssi.h Normal file
View File

@@ -0,0 +1,36 @@
/* UA1ZBE Custom Firmware - RSSI Display Module
*
* Reads RSSI from BK4819 every 100-200ms, converts to dBm,
* and draws an indicator on the display.
*
* Format: "S:42" or "-87dBm" with mini bar
* Shown in upper-right corner of screen.
*/
#ifndef APP_DISPLAY_RSSI_H
#define APP_DISPLAY_RSSI_H
#include <stdbool.h>
#include <stdint.h>
/* Initialize RSSI display module */
void RSSI_Init(void);
/* Call from 10ms timeslice to update RSSI reading */
void RSSI_Update(void);
/* Draw RSSI indicator on the screen
* x, y: position on screen
* with_bar: if true, draw a mini signal bar next to the value */
void RSSI_Draw(int x, int y, bool with_bar);
/* Get current RSSI value in dBm */
int16_t RSSI_GetdBm(void);
/* Get raw RSSI value (0-511 from BK4819 REG_67) */
uint16_t RSSI_GetRaw(void);
/* Get S-meter level (0-9) */
uint8_t RSSI_GetSLevel(void);
#endif

504
app/dtmf.c Normal file
View File

@@ -0,0 +1,504 @@
/* Copyright 2023 Dual Tachyon
* https://github.com/DualTachyon
*
* 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.
*/
#include <string.h>
#include <stdio.h> // NULL
#include "app/chFrScanner.h"
#ifdef ENABLE_FMRADIO
#include "app/fm.h"
#endif
#include "app/scanner.h"
#include "bsp/dp32g030/gpio.h"
#include "audio.h"
#include "driver/bk4819.h"
#include "driver/eeprom.h"
#include "driver/gpio.h"
#include "driver/system.h"
#include "dtmf.h"
#include "external/printf/printf.h"
#include "misc.h"
#include "settings.h"
#include "ui/ui.h"
char gDTMF_String[15];
char gDTMF_InputBox[15];
uint8_t gDTMF_InputBox_Index = 0;
bool gDTMF_InputMode = false;
uint8_t gDTMF_PreviousIndex = 0;
char gDTMF_RX_live[20];
uint8_t gDTMF_RX_live_timeout = 0;
#ifdef ENABLE_DTMF_CALLING
char gDTMF_RX[17];
uint8_t gDTMF_RX_index = 0;
uint8_t gDTMF_RX_timeout = 0;
bool gDTMF_RX_pending = false;
bool gIsDtmfContactValid;
char gDTMF_ID[4];
char gDTMF_Caller[4];
char gDTMF_Callee[4];
DTMF_State_t gDTMF_State;
uint8_t gDTMF_DecodeRingCountdown_500ms;
uint8_t gDTMF_chosen_contact;
uint8_t gDTMF_auto_reset_time_500ms;
DTMF_CallState_t gDTMF_CallState;
DTMF_CallMode_t gDTMF_CallMode;
bool gDTMF_IsTx;
uint8_t gDTMF_TxStopCountdown_500ms;
bool gDTMF_IsGroupCall;
#endif
DTMF_ReplyState_t gDTMF_ReplyState;
#ifdef ENABLE_DTMF_CALLING
void DTMF_clear_RX(void)
{
gDTMF_RX_timeout = 0;
gDTMF_RX_index = 0;
gDTMF_RX_pending = false;
memset(gDTMF_RX, 0, sizeof(gDTMF_RX));
}
#endif
void DTMF_SendEndOfTransmission(void)
{
if (gCurrentVfo->DTMF_PTT_ID_TX_MODE == PTT_ID_APOLLO)
BK4819_PlaySingleTone(2475, 250, 28, gEeprom.DTMF_SIDE_TONE);
else if ((gCurrentVfo->DTMF_PTT_ID_TX_MODE == PTT_ID_TX_DOWN || gCurrentVfo->DTMF_PTT_ID_TX_MODE == PTT_ID_BOTH)
#ifdef ENABLE_DTMF_CALLING
&& gDTMF_CallState == DTMF_CALL_STATE_NONE
#endif
) { // end-of-tx
if (gEeprom.DTMF_SIDE_TONE) {
AUDIO_AudioPathOn();
gEnableSpeaker = true;
SYSTEM_DelayMs(60);
}
BK4819_EnterDTMF_TX(gEeprom.DTMF_SIDE_TONE);
BK4819_PlayDTMFString(
gEeprom.DTMF_DOWN_CODE,
0,
gEeprom.DTMF_FIRST_CODE_PERSIST_TIME,
gEeprom.DTMF_HASH_CODE_PERSIST_TIME,
gEeprom.DTMF_CODE_PERSIST_TIME,
gEeprom.DTMF_CODE_INTERVAL_TIME);
AUDIO_AudioPathOff();
gEnableSpeaker = false;
}
BK4819_ExitDTMF_TX(true);
}
bool DTMF_ValidateCodes(char *pCode, const unsigned int size)
{
unsigned int i;
if (pCode[0] == 0xFF || pCode[0] == 0)
return false;
for (i = 0; i < size; i++)
{
if (pCode[i] == 0xFF || pCode[i] == 0)
{
pCode[i] = 0;
break;
}
if ((pCode[i] < '0' || pCode[i] > '9') && (pCode[i] < 'A' || pCode[i] > 'D') && pCode[i] != '*' && pCode[i] != '#')
return false;
}
return true;
}
#ifdef ENABLE_DTMF_CALLING
bool DTMF_GetContact(const int Index, char *pContact)
{
if (Index < 0 || Index >= MAX_DTMF_CONTACTS || pContact == NULL) {
return false;
}
EEPROM_ReadBuffer(0x1C00 + (Index * 16), pContact, 16);
// check whether the first character is printable or not
return (pContact[0] >= ' ' && pContact[0] < 127);
}
bool DTMF_FindContact(const char *pContact, char *pResult)
{
pResult[0] = 0;
for (unsigned int i = 0; i < MAX_DTMF_CONTACTS; i++) {
char Contact[16];
if (!DTMF_GetContact(i, Contact)) {
return false;
}
if (memcmp(pContact, Contact + 8, 3) == 0) {
memcpy(pResult, Contact, 8);
pResult[8] = 0;
return true;
}
}
return false;
}
#endif
char DTMF_GetCharacter(const unsigned int code)
{
switch (code)
{
case KEY_0: return '0';
case KEY_1: return '1';
case KEY_2: return '2';
case KEY_3: return '3';
case KEY_4: return '4';
case KEY_5: return '5';
case KEY_6: return '6';
case KEY_7: return '7';
case KEY_8: return '8';
case KEY_9: return '9';
case KEY_MENU: return 'A';
case KEY_UP: return 'B';
case KEY_DOWN: return 'C';
case KEY_EXIT: return 'D';
case KEY_STAR: return '*';
case KEY_F: return '#';
default: return 0xff;
}
}
#ifdef ENABLE_DTMF_CALLING
static bool CompareMessage(const char *pMsg, const char *pTemplate, const unsigned int size, const bool bCheckGroup)
{
unsigned int i;
for (i = 0; i < size; i++)
{
if (pMsg[i] != pTemplate[i])
{
if (!bCheckGroup || pMsg[i] != gEeprom.DTMF_GROUP_CALL_CODE)
return false;
gDTMF_IsGroupCall = true;
}
}
return true;
}
DTMF_CallMode_t DTMF_CheckGroupCall(const char *pMsg, const unsigned int size)
{
for (unsigned int i = 0; i < size; i++)
if (pMsg[i] == gEeprom.DTMF_GROUP_CALL_CODE) {
return DTMF_CALL_MODE_GROUP;
}
return DTMF_CALL_MODE_NOT_GROUP;
}
#endif
void DTMF_clear_input_box(void)
{
memset(gDTMF_InputBox, 0, sizeof(gDTMF_InputBox));
gDTMF_InputBox_Index = 0;
gDTMF_InputMode = false;
}
void DTMF_Append(const char code)
{
if (gDTMF_InputBox_Index == 0)
{
memset(gDTMF_InputBox, '-', sizeof(gDTMF_InputBox) - 1);
gDTMF_InputBox[sizeof(gDTMF_InputBox) - 1] = 0;
}
if (gDTMF_InputBox_Index < (sizeof(gDTMF_InputBox) - 1))
gDTMF_InputBox[gDTMF_InputBox_Index++] = code;
}
#ifdef ENABLE_DTMF_CALLING
void DTMF_HandleRequest(void)
{ // proccess the RX'ed DTMF characters
char String[21];
unsigned int Offset;
if (!gDTMF_RX_pending)
return; // nothing new received
if (gScanStateDir != SCAN_OFF || gCssBackgroundScan)
{ // we're busy scanning
DTMF_clear_RX();
return;
}
if (!gRxVfo->DTMF_DECODING_ENABLE && !gSetting_KILLED)
{ // D-DCD is disabled or we're alive
DTMF_clear_RX();
return;
}
gDTMF_RX_pending = false;
if (gDTMF_RX_index >= 9)
{ // look for the KILL code
sprintf(String, "%s%c%s", gEeprom.ANI_DTMF_ID, gEeprom.DTMF_SEPARATE_CODE, gEeprom.KILL_CODE);
Offset = gDTMF_RX_index - strlen(String);
if (CompareMessage(gDTMF_RX + Offset, String, strlen(String), true))
{ // bugger
if (gEeprom.PERMIT_REMOTE_KILL)
{
gSetting_KILLED = true; // oooerr !
DTMF_clear_RX();
SETTINGS_SaveSettings();
gDTMF_ReplyState = DTMF_REPLY_AB;
#ifdef ENABLE_FMRADIO
if (gFmRadioMode)
{
FM_TurnOff();
GUI_SelectNextDisplay(DISPLAY_MAIN);
}
#endif
}
else
{
gDTMF_ReplyState = DTMF_REPLY_NONE;
}
gDTMF_CallState = DTMF_CALL_STATE_NONE;
gUpdateDisplay = true;
gUpdateStatus = true;
return;
}
}
if (gDTMF_RX_index >= 9)
{ // look for the REVIVE code
sprintf(String, "%s%c%s", gEeprom.ANI_DTMF_ID, gEeprom.DTMF_SEPARATE_CODE, gEeprom.REVIVE_CODE);
Offset = gDTMF_RX_index - strlen(String);
if (CompareMessage(gDTMF_RX + Offset, String, strlen(String), true))
{ // shit, we're back !
gSetting_KILLED = false;
DTMF_clear_RX();
SETTINGS_SaveSettings();
gDTMF_ReplyState = DTMF_REPLY_AB;
gDTMF_CallState = DTMF_CALL_STATE_NONE;
gUpdateDisplay = true;
gUpdateStatus = true;
return;
}
}
if (gDTMF_RX_index >= 2)
{ // look for ACK reply
char *pPrintStr = "AB";
Offset = gDTMF_RX_index - strlen(pPrintStr);
if (CompareMessage(gDTMF_RX + Offset, pPrintStr, strlen(pPrintStr), true)) {
// ends with "AB"
if (gDTMF_ReplyState != DTMF_REPLY_NONE) // 1of11
// if (gDTMF_CallState != DTMF_CALL_STATE_NONE) // 1of11
// if (gDTMF_CallState == DTMF_CALL_STATE_CALL_OUT) // 1of11
{
gDTMF_State = DTMF_STATE_TX_SUCC;
DTMF_clear_RX();
gUpdateDisplay = true;
return;
}
}
}
if (gDTMF_CallState == DTMF_CALL_STATE_CALL_OUT &&
gDTMF_CallMode == DTMF_CALL_MODE_NOT_GROUP &&
gDTMF_RX_index >= 9)
{ // waiting for a reply
sprintf(String, "%s%c%s", gDTMF_String, gEeprom.DTMF_SEPARATE_CODE, "AAAAA");
Offset = gDTMF_RX_index - strlen(String);
if (CompareMessage(gDTMF_RX + Offset, String, strlen(String), false))
{ // we got a response
gDTMF_State = DTMF_STATE_CALL_OUT_RSP;
DTMF_clear_RX();
gUpdateDisplay = true;
}
}
if (gSetting_KILLED || gDTMF_CallState != DTMF_CALL_STATE_NONE)
{ // we've been killed or expecting a reply
return;
}
if (gDTMF_RX_index >= 7)
{ // see if we're being called
gDTMF_IsGroupCall = false;
sprintf(String, "%s%c", gEeprom.ANI_DTMF_ID, gEeprom.DTMF_SEPARATE_CODE);
Offset = gDTMF_RX_index - strlen(String) - 3;
if (CompareMessage(gDTMF_RX + Offset, String, strlen(String), true))
{ // it's for us !
gDTMF_CallState = DTMF_CALL_STATE_RECEIVED;
memset(gDTMF_Callee, 0, sizeof(gDTMF_Callee));
memset(gDTMF_Caller, 0, sizeof(gDTMF_Caller));
memcpy(gDTMF_Callee, gDTMF_RX + Offset + 0, 3);
memcpy(gDTMF_Caller, gDTMF_RX + Offset + 4, 3);
DTMF_clear_RX();
gUpdateDisplay = true;
switch (gEeprom.DTMF_DECODE_RESPONSE)
{
case DTMF_DEC_RESPONSE_BOTH:
gDTMF_DecodeRingCountdown_500ms = DTMF_decode_ring_countdown_500ms;
__attribute__((fallthrough));
case DTMF_DEC_RESPONSE_REPLY:
gDTMF_ReplyState = DTMF_REPLY_AAAAA;
break;
case DTMF_DEC_RESPONSE_RING:
gDTMF_DecodeRingCountdown_500ms = DTMF_decode_ring_countdown_500ms;
break;
default:
case DTMF_DEC_RESPONSE_NONE:
gDTMF_DecodeRingCountdown_500ms = 0;
gDTMF_ReplyState = DTMF_REPLY_NONE;
break;
}
if (gDTMF_IsGroupCall)
gDTMF_ReplyState = DTMF_REPLY_NONE;
}
}
}
#endif
void DTMF_Reply(void)
{
uint16_t Delay;
#ifdef ENABLE_DTMF_CALLING
char String[23];
#endif
const char *pString = NULL;
switch (gDTMF_ReplyState)
{
case DTMF_REPLY_ANI:
#ifdef ENABLE_DTMF_CALLING
if (gDTMF_CallMode != DTMF_CALL_MODE_DTMF)
{ // append our ID code onto the end of the DTMF code to send
sprintf(String, "%s%c%s", gDTMF_String, gEeprom.DTMF_SEPARATE_CODE, gEeprom.ANI_DTMF_ID);
pString = String;
}
else
#endif
{
pString = gDTMF_String;
}
break;
#ifdef ENABLE_DTMF_CALLING
case DTMF_REPLY_AB:
pString = "AB";
break;
case DTMF_REPLY_AAAAA:
sprintf(String, "%s%c%s", gEeprom.ANI_DTMF_ID, gEeprom.DTMF_SEPARATE_CODE, "AAAAA");
pString = String;
break;
#endif
default:
case DTMF_REPLY_NONE:
if (
#ifdef ENABLE_DTMF_CALLING
gDTMF_CallState != DTMF_CALL_STATE_NONE ||
#endif
gCurrentVfo->DTMF_PTT_ID_TX_MODE == PTT_ID_APOLLO ||
gCurrentVfo->DTMF_PTT_ID_TX_MODE == PTT_ID_OFF ||
gCurrentVfo->DTMF_PTT_ID_TX_MODE == PTT_ID_TX_DOWN)
{
gDTMF_ReplyState = DTMF_REPLY_NONE;
return;
}
// send TX-UP DTMF
pString = gEeprom.DTMF_UP_CODE;
break;
}
gDTMF_ReplyState = DTMF_REPLY_NONE;
if (pString == NULL)
return;
Delay = (gEeprom.DTMF_PRELOAD_TIME < 200) ? 200 : gEeprom.DTMF_PRELOAD_TIME;
if (gEeprom.DTMF_SIDE_TONE)
{ // the user will also hear the transmitted tones
AUDIO_AudioPathOn();
gEnableSpeaker = true;
}
SYSTEM_DelayMs(Delay);
BK4819_EnterDTMF_TX(gEeprom.DTMF_SIDE_TONE);
BK4819_PlayDTMFString(
pString,
1,
gEeprom.DTMF_FIRST_CODE_PERSIST_TIME,
gEeprom.DTMF_HASH_CODE_PERSIST_TIME,
gEeprom.DTMF_CODE_PERSIST_TIME,
gEeprom.DTMF_CODE_INTERVAL_TIME);
AUDIO_AudioPathOff();
gEnableSpeaker = false;
BK4819_ExitDTMF_TX(false);
}

119
app/dtmf.h Normal file
View File

@@ -0,0 +1,119 @@
/* Copyright 2023 Dual Tachyon
* https://github.com/DualTachyon
*
* 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 DTMF_H
#define DTMF_H
#include <stdbool.h>
#include <stdint.h>
#define MAX_DTMF_CONTACTS 16
enum DTMF_State_t {
DTMF_STATE_0 = 0,
DTMF_STATE_TX_SUCC,
DTMF_STATE_CALL_OUT_RSP
};
typedef enum DTMF_State_t DTMF_State_t;
enum DTMF_CallState_t {
DTMF_CALL_STATE_NONE = 0,
DTMF_CALL_STATE_CALL_OUT,
DTMF_CALL_STATE_RECEIVED,
DTMF_CALL_STATE_RECEIVED_STAY
};
enum DTMF_DecodeResponse_t {
DTMF_DEC_RESPONSE_NONE = 0,
DTMF_DEC_RESPONSE_RING,
DTMF_DEC_RESPONSE_REPLY,
DTMF_DEC_RESPONSE_BOTH
};
typedef enum DTMF_CallState_t DTMF_CallState_t;
enum DTMF_ReplyState_t {
DTMF_REPLY_NONE = 0,
DTMF_REPLY_ANI,
DTMF_REPLY_AB,
DTMF_REPLY_AAAAA
};
typedef enum DTMF_ReplyState_t DTMF_ReplyState_t;
enum DTMF_CallMode_t {
DTMF_CALL_MODE_NOT_GROUP = 0,
DTMF_CALL_MODE_GROUP,
DTMF_CALL_MODE_DTMF
};
enum { // seconds
DTMF_HOLD_MIN = 5,
DTMF_HOLD_MAX = 60
};
typedef enum DTMF_CallMode_t DTMF_CallMode_t;
extern char gDTMF_String[15];
extern char gDTMF_InputBox[15];
extern uint8_t gDTMF_InputBox_Index;
extern bool gDTMF_InputMode;
extern uint8_t gDTMF_PreviousIndex;
extern char gDTMF_RX_live[20];
extern uint8_t gDTMF_RX_live_timeout;
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(void);
void DTMF_Append(const char code);
void DTMF_Reply(void);
void DTMF_SendEndOfTransmission(void);
#ifdef ENABLE_DTMF_CALLING
extern char gDTMF_RX[17];
extern uint8_t gDTMF_RX_index;
extern uint8_t gDTMF_RX_timeout;
extern bool gDTMF_RX_pending;
extern bool gIsDtmfContactValid;
extern char gDTMF_ID[4];
extern char gDTMF_Caller[4];
extern char gDTMF_Callee[4];
extern DTMF_State_t gDTMF_State;
extern uint8_t gDTMF_DecodeRingCountdown_500ms;
extern uint8_t gDTMF_chosen_contact;
extern uint8_t gDTMF_auto_reset_time_500ms;
extern DTMF_CallState_t gDTMF_CallState;
extern DTMF_CallMode_t gDTMF_CallMode;
extern bool gDTMF_IsTx;
extern uint8_t gDTMF_TxStopCountdown_500ms;
void DTMF_clear_RX(void);
DTMF_CallMode_t DTMF_CheckGroupCall(const char *pDTMF, const unsigned int size);
bool DTMF_GetContact(const int Index, char *pContact);
bool DTMF_FindContact(const char *pContact, char *pResult);
void DTMF_HandleRequest(void);
#endif
#endif

66
app/flashlight.c Normal file
View File

@@ -0,0 +1,66 @@
#ifdef ENABLE_FLASHLIGHT
#include "driver/gpio.h"
#include "bsp/dp32g030/gpio.h"
#include "flashlight.h"
enum FlashlightMode_t gFlashLightState;
void FlashlightTimeSlice()
{
if (gFlashLightState == FLASHLIGHT_BLINK && (gFlashLightBlinkCounter & 15u) == 0) {
GPIO_FlipBit(&GPIOC->DATA, GPIOC_PIN_FLASHLIGHT);
return;
}
if (gFlashLightState == FLASHLIGHT_SOS) {
const uint16_t u = 15;
static uint8_t c;
static uint16_t next;
if (gFlashLightBlinkCounter - next > 7 * u) {
c = 0;
next = gFlashLightBlinkCounter + 1;
return;
}
if (gFlashLightBlinkCounter == next) {
if (c==0) {
GPIO_ClearBit(&GPIOC->DATA, GPIOC_PIN_FLASHLIGHT);
} else {
GPIO_FlipBit(&GPIOC->DATA, GPIOC_PIN_FLASHLIGHT);
}
if (c >= 18) {
next = gFlashLightBlinkCounter + 7 * u;
c = 0;
} else if(c==7 || c==9 || c==11) {
next = gFlashLightBlinkCounter + 3 * u;
} else {
next = gFlashLightBlinkCounter + u;
}
c++;
}
}
}
void ACTION_FlashLight(void)
{
switch (gFlashLightState) {
case FLASHLIGHT_OFF:
gFlashLightState++;
GPIO_SetBit(&GPIOC->DATA, GPIOC_PIN_FLASHLIGHT);
break;
case FLASHLIGHT_ON:
case FLASHLIGHT_BLINK:
gFlashLightState++;
break;
case FLASHLIGHT_SOS:
default:
gFlashLightState = 0;
GPIO_ClearBit(&GPIOC->DATA, GPIOC_PIN_FLASHLIGHT);
}
}
#endif

23
app/flashlight.h Normal file
View File

@@ -0,0 +1,23 @@
#ifndef APP_FLASHLIGHT_H
#define APP_FLASHLIGHT_H
#ifdef ENABLE_FLASHLIGHT
#include <stdint.h>
enum FlashlightMode_t {
FLASHLIGHT_OFF = 0,
FLASHLIGHT_ON,
FLASHLIGHT_BLINK,
FLASHLIGHT_SOS
};
extern enum FlashlightMode_t gFlashLightState;
extern volatile uint16_t gFlashLightBlinkCounter;
void FlashlightTimeSlice(void);
void ACTION_FlashLight(void);
#endif
#endif

605
app/fm.c Normal file
View File

@@ -0,0 +1,605 @@
/* Copyright 2023 Dual Tachyon
* https://github.com/DualTachyon
*
* 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_FMRADIO
#include <string.h>
#include "app/action.h"
#include "app/fm.h"
#include "app/generic.h"
#include "audio.h"
#include "bsp/dp32g030/gpio.h"
#include "driver/bk1080.h"
#include "driver/eeprom.h"
#include "driver/gpio.h"
#include "functions.h"
#include "misc.h"
#include "settings.h"
#include "ui/inputbox.h"
#include "ui/ui.h"
#ifndef ARRAY_SIZE
#define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
#endif
uint16_t gFM_Channels[20];
bool gFmRadioMode;
uint8_t gFmRadioCountdown_500ms;
volatile uint16_t gFmPlayCountdown_10ms;
volatile int8_t gFM_ScanState;
bool gFM_AutoScan;
uint8_t gFM_ChannelPosition;
bool gFM_FoundFrequency;
bool gFM_AutoScan;
uint16_t gFM_RestoreCountdown_10ms;
const uint8_t BUTTON_STATE_PRESSED = 1 << 0;
const uint8_t BUTTON_STATE_HELD = 1 << 1;
const uint8_t BUTTON_EVENT_PRESSED = BUTTON_STATE_PRESSED;
const uint8_t BUTTON_EVENT_HELD = BUTTON_STATE_PRESSED | BUTTON_STATE_HELD;
const uint8_t BUTTON_EVENT_SHORT = 0;
const uint8_t BUTTON_EVENT_LONG = BUTTON_STATE_HELD;
static void Key_FUNC(KEY_Code_t Key, uint8_t state);
bool FM_CheckValidChannel(uint8_t Channel)
{
return Channel < ARRAY_SIZE(gFM_Channels) &&
gFM_Channels[Channel] >= BK1080_GetFreqLoLimit(gEeprom.FM_Band) &&
gFM_Channels[Channel] < BK1080_GetFreqHiLimit(gEeprom.FM_Band);
}
uint8_t FM_FindNextChannel(uint8_t Channel, uint8_t Direction)
{
for (unsigned i = 0; i < ARRAY_SIZE(gFM_Channels); i++) {
if (Channel == 0xFF)
Channel = ARRAY_SIZE(gFM_Channels) - 1;
else if (Channel >= ARRAY_SIZE(gFM_Channels))
Channel = 0;
if (FM_CheckValidChannel(Channel))
return Channel;
Channel += Direction;
}
return 0xFF;
}
int FM_ConfigureChannelState(void)
{
gEeprom.FM_FrequencyPlaying = gEeprom.FM_SelectedFrequency;
if (gEeprom.FM_IsMrMode) {
const uint8_t Channel = FM_FindNextChannel(gEeprom.FM_SelectedChannel, FM_CHANNEL_UP);
if (Channel == 0xFF) {
gEeprom.FM_IsMrMode = false;
return -1;
}
gEeprom.FM_SelectedChannel = Channel;
gEeprom.FM_FrequencyPlaying = gFM_Channels[Channel];
}
return 0;
}
void FM_TurnOff(void)
{
gFmRadioMode = false;
gFM_ScanState = FM_SCAN_OFF;
gFM_RestoreCountdown_10ms = 0;
AUDIO_AudioPathOff();
gEnableSpeaker = false;
BK1080_Init0();
gUpdateStatus = true;
}
void FM_EraseChannels(void)
{
uint8_t Template[8];
memset(Template, 0xFF, sizeof(Template));
for (unsigned i = 0; i < 5; i++)
EEPROM_WriteBuffer(0x0E40 + (i * 8), Template);
memset(gFM_Channels, 0xFF, sizeof(gFM_Channels));
}
void FM_Tune(uint16_t Frequency, int8_t Step, bool bFlag)
{
AUDIO_AudioPathOff();
gEnableSpeaker = false;
gFmPlayCountdown_10ms = (gFM_ScanState == FM_SCAN_OFF) ? fm_play_countdown_noscan_10ms : fm_play_countdown_scan_10ms;
gScheduleFM = false;
gFM_FoundFrequency = false;
gAskToSave = false;
gAskToDelete = false;
gEeprom.FM_FrequencyPlaying = Frequency;
if (!bFlag) {
Frequency += Step;
if (Frequency < BK1080_GetFreqLoLimit(gEeprom.FM_Band))
Frequency = BK1080_GetFreqHiLimit(gEeprom.FM_Band);
else if (Frequency > BK1080_GetFreqHiLimit(gEeprom.FM_Band))
Frequency = BK1080_GetFreqLoLimit(gEeprom.FM_Band);
gEeprom.FM_FrequencyPlaying = Frequency;
}
gFM_ScanState = Step;
BK1080_SetFrequency(gEeprom.FM_FrequencyPlaying, gEeprom.FM_Band/*, gEeprom.FM_Space*/);
}
void FM_PlayAndUpdate(void)
{
gFM_ScanState = FM_SCAN_OFF;
if (gFM_AutoScan) {
gEeprom.FM_IsMrMode = true;
gEeprom.FM_SelectedChannel = 0;
}
FM_ConfigureChannelState();
BK1080_SetFrequency(gEeprom.FM_FrequencyPlaying, gEeprom.FM_Band/*, gEeprom.FM_Space*/);
SETTINGS_SaveFM();
gFmPlayCountdown_10ms = 0;
gScheduleFM = false;
gAskToSave = false;
AUDIO_AudioPathOn();
gEnableSpeaker = true;
}
int FM_CheckFrequencyLock(uint16_t Frequency, uint16_t LowerLimit)
{
int ret = -1;
const uint16_t Test2 = BK1080_ReadRegister(BK1080_REG_07);
// This is supposed to be a signed value, but above function is unsigned
const uint16_t Deviation = BK1080_REG_07_GET_FREQD(Test2);
if (BK1080_REG_07_GET_SNR(Test2) <= 2) {
goto Bail;
}
const uint16_t Status = BK1080_ReadRegister(BK1080_REG_10);
if ((Status & BK1080_REG_10_MASK_AFCRL) != BK1080_REG_10_AFCRL_NOT_RAILED || BK1080_REG_10_GET_RSSI(Status) < 10) {
goto Bail;
}
//if (Deviation > -281 && Deviation < 280)
if (Deviation >= 280 && Deviation <= 3815) {
goto Bail;
}
// not BLE(less than or equal)
if (Frequency > LowerLimit && (Frequency - BK1080_BaseFrequency) == 1) {
if (BK1080_FrequencyDeviation & 0x800 || (BK1080_FrequencyDeviation < 20))
goto Bail;
}
// not BLT(less than)
if (Frequency >= LowerLimit && (BK1080_BaseFrequency - Frequency) == 1) {
if ((BK1080_FrequencyDeviation & 0x800) == 0 || (BK1080_FrequencyDeviation > 4075))
goto Bail;
}
ret = 0;
Bail:
BK1080_FrequencyDeviation = Deviation;
BK1080_BaseFrequency = Frequency;
return ret;
}
static void Key_DIGITS(KEY_Code_t Key, uint8_t state)
{
enum { STATE_FREQ_MODE, STATE_MR_MODE, STATE_SAVE };
if (state == BUTTON_EVENT_SHORT && !gWasFKeyPressed) {
uint8_t State;
if (gAskToDelete) {
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
return;
}
if (gAskToSave) {
State = STATE_SAVE;
}
else {
if (gFM_ScanState != FM_SCAN_OFF) {
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
return;
}
State = gEeprom.FM_IsMrMode ? STATE_MR_MODE : STATE_FREQ_MODE;
}
INPUTBOX_Append(Key);
gRequestDisplayScreen = DISPLAY_FM;
if (State == STATE_FREQ_MODE) {
if (gInputBoxIndex == 1) {
if (gInputBox[0] > 1) {
gInputBox[1] = gInputBox[0];
gInputBox[0] = 0;
gInputBoxIndex = 2;
}
}
else if (gInputBoxIndex > 3) {
uint32_t Frequency;
gInputBoxIndex = 0;
Frequency = StrToUL(INPUTBOX_GetAscii());
if (Frequency < BK1080_GetFreqLoLimit(gEeprom.FM_Band) || BK1080_GetFreqHiLimit(gEeprom.FM_Band) < Frequency) {
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
gRequestDisplayScreen = DISPLAY_FM;
return;
}
gEeprom.FM_SelectedFrequency = (uint16_t)Frequency;
#ifdef ENABLE_VOICE
gAnotherVoiceID = (VOICE_ID_t)Key;
#endif
gEeprom.FM_FrequencyPlaying = gEeprom.FM_SelectedFrequency;
BK1080_SetFrequency(gEeprom.FM_FrequencyPlaying, gEeprom.FM_Band/*, gEeprom.FM_Space*/);
gRequestSaveFM = true;
return;
}
}
else if (gInputBoxIndex == 2) {
uint8_t Channel;
gInputBoxIndex = 0;
Channel = ((gInputBox[0] * 10) + gInputBox[1]) - 1;
if (State == STATE_MR_MODE) {
if (FM_CheckValidChannel(Channel)) {
#ifdef ENABLE_VOICE
gAnotherVoiceID = (VOICE_ID_t)Key;
#endif
gEeprom.FM_SelectedChannel = Channel;
gEeprom.FM_FrequencyPlaying = gFM_Channels[Channel];
BK1080_SetFrequency(gEeprom.FM_FrequencyPlaying, gEeprom.FM_Band/*, gEeprom.FM_Space*/);
gRequestSaveFM = true;
return;
}
}
else if (Channel < 20) {
#ifdef ENABLE_VOICE
gAnotherVoiceID = (VOICE_ID_t)Key;
#endif
gRequestDisplayScreen = DISPLAY_FM;
gInputBoxIndex = 0;
gFM_ChannelPosition = Channel;
return;
}
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
return;
}
#ifdef ENABLE_VOICE
gAnotherVoiceID = (VOICE_ID_t)Key;
#endif
}
else
Key_FUNC(Key, state);
}
static void Key_FUNC(KEY_Code_t Key, uint8_t state)
{
if (state == BUTTON_EVENT_SHORT || state == BUTTON_EVENT_HELD) {
bool autoScan = gWasFKeyPressed || (state == BUTTON_EVENT_HELD);
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
gWasFKeyPressed = false;
gUpdateStatus = true;
gRequestDisplayScreen = DISPLAY_FM;
switch (Key) {
case KEY_0:
ACTION_FM();
break;
case KEY_1:
gEeprom.FM_Band++;
gRequestSaveFM = true;
break;
// case KEY_2:
// gEeprom.FM_Space = (gEeprom.FM_Space + 1) % 3;
// gRequestSaveFM = true;
// break;
case KEY_3:
gEeprom.FM_IsMrMode = !gEeprom.FM_IsMrMode;
if (!FM_ConfigureChannelState()) {
BK1080_SetFrequency(gEeprom.FM_FrequencyPlaying, gEeprom.FM_Band/*, gEeprom.FM_Space*/);
gRequestSaveFM = true;
}
else
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
break;
case KEY_STAR:
ACTION_Scan(autoScan);
break;
default:
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
break;
}
}
}
static void Key_EXIT(uint8_t state)
{
if (state != BUTTON_EVENT_SHORT)
return;
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
if (gFM_ScanState == FM_SCAN_OFF) {
if (gInputBoxIndex == 0) {
if (!gAskToSave && !gAskToDelete) {
ACTION_FM();
return;
}
gAskToSave = false;
gAskToDelete = false;
}
else {
gInputBox[--gInputBoxIndex] = 10;
if (gInputBoxIndex) {
if (gInputBoxIndex != 1) {
gRequestDisplayScreen = DISPLAY_FM;
return;
}
if (gInputBox[0] != 0) {
gRequestDisplayScreen = DISPLAY_FM;
return;
}
}
gInputBoxIndex = 0;
}
#ifdef ENABLE_VOICE
gAnotherVoiceID = VOICE_ID_CANCEL;
#endif
}
else {
FM_PlayAndUpdate();
#ifdef ENABLE_VOICE
gAnotherVoiceID = VOICE_ID_SCANNING_STOP;
#endif
}
gRequestDisplayScreen = DISPLAY_FM;
}
static void Key_MENU(uint8_t state)
{
if (state != BUTTON_EVENT_SHORT)
return;
gRequestDisplayScreen = DISPLAY_FM;
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
if (gFM_ScanState == FM_SCAN_OFF) {
if (!gEeprom.FM_IsMrMode) {
if (gAskToSave) {
gFM_Channels[gFM_ChannelPosition] = gEeprom.FM_FrequencyPlaying;
gRequestSaveFM = true;
}
gAskToSave = !gAskToSave;
}
else {
if (gAskToDelete) {
gFM_Channels[gEeprom.FM_SelectedChannel] = 0xFFFF;
FM_ConfigureChannelState();
BK1080_SetFrequency(gEeprom.FM_FrequencyPlaying, gEeprom.FM_Band/*, gEeprom.FM_Space*/);
gRequestSaveFM = true;
}
gAskToDelete = !gAskToDelete;
}
}
else {
if (gFM_AutoScan || !gFM_FoundFrequency) {
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
gInputBoxIndex = 0;
return;
}
if (gAskToSave) {
gFM_Channels[gFM_ChannelPosition] = gEeprom.FM_FrequencyPlaying;
gRequestSaveFM = true;
}
gAskToSave = !gAskToSave;
}
}
static void Key_UP_DOWN(uint8_t state, int8_t Step)
{
if (state == BUTTON_EVENT_PRESSED) {
if (gInputBoxIndex) {
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
return;
}
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
} else if (gInputBoxIndex || state!=BUTTON_EVENT_HELD) {
return;
}
if (gAskToSave) {
gRequestDisplayScreen = DISPLAY_FM;
gFM_ChannelPosition = NUMBER_AddWithWraparound(gFM_ChannelPosition, Step, 0, 19);
return;
}
if (gFM_ScanState != FM_SCAN_OFF) {
if (gFM_AutoScan) {
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
return;
}
FM_Tune(gEeprom.FM_FrequencyPlaying, Step, false);
gRequestDisplayScreen = DISPLAY_FM;
return;
}
if (gEeprom.FM_IsMrMode) {
const uint8_t Channel = FM_FindNextChannel(gEeprom.FM_SelectedChannel + Step, Step);
if (Channel == 0xFF || gEeprom.FM_SelectedChannel == Channel)
goto Bail;
gEeprom.FM_SelectedChannel = Channel;
gEeprom.FM_FrequencyPlaying = gFM_Channels[Channel];
}
else {
uint16_t Frequency = gEeprom.FM_SelectedFrequency + Step;
if (Frequency < BK1080_GetFreqLoLimit(gEeprom.FM_Band))
Frequency = BK1080_GetFreqHiLimit(gEeprom.FM_Band);
else if (Frequency > BK1080_GetFreqHiLimit(gEeprom.FM_Band))
Frequency = BK1080_GetFreqLoLimit(gEeprom.FM_Band);
gEeprom.FM_FrequencyPlaying = Frequency;
gEeprom.FM_SelectedFrequency = gEeprom.FM_FrequencyPlaying;
}
gRequestSaveFM = true;
Bail:
BK1080_SetFrequency(gEeprom.FM_FrequencyPlaying, gEeprom.FM_Band/*, gEeprom.FM_Space*/);
gRequestDisplayScreen = DISPLAY_FM;
}
void FM_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
{
uint8_t state = bKeyPressed + 2 * bKeyHeld;
switch (Key) {
case KEY_0...KEY_9:
Key_DIGITS(Key, state);
break;
case KEY_STAR:
Key_FUNC(Key, state);
break;
case KEY_MENU:
Key_MENU(state);
break;
case KEY_UP:
Key_UP_DOWN(state, 1);
break;
case KEY_DOWN:
Key_UP_DOWN(state, -1);
break;;
case KEY_EXIT:
Key_EXIT(state);
break;
case KEY_F:
GENERIC_Key_F(bKeyPressed, bKeyHeld);
break;
case KEY_PTT:
GENERIC_Key_PTT(bKeyPressed);
break;
default:
if (!bKeyHeld && bKeyPressed)
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
break;
}
}
void FM_Play(void)
{
if (!FM_CheckFrequencyLock(gEeprom.FM_FrequencyPlaying, BK1080_GetFreqLoLimit(gEeprom.FM_Band))) {
if (!gFM_AutoScan) {
gFmPlayCountdown_10ms = 0;
gFM_FoundFrequency = true;
if (!gEeprom.FM_IsMrMode)
gEeprom.FM_SelectedFrequency = gEeprom.FM_FrequencyPlaying;
AUDIO_AudioPathOn();
gEnableSpeaker = true;
GUI_SelectNextDisplay(DISPLAY_FM);
return;
}
if (gFM_ChannelPosition < 20)
gFM_Channels[gFM_ChannelPosition++] = gEeprom.FM_FrequencyPlaying;
if (gFM_ChannelPosition >= 20) {
FM_PlayAndUpdate();
GUI_SelectNextDisplay(DISPLAY_FM);
return;
}
}
if (gFM_AutoScan && gEeprom.FM_FrequencyPlaying >= BK1080_GetFreqHiLimit(1))
FM_PlayAndUpdate();
else
FM_Tune(gEeprom.FM_FrequencyPlaying, gFM_ScanState, false);
GUI_SelectNextDisplay(DISPLAY_FM);
}
void FM_Start(void)
{
gDualWatchActive = false;
gFmRadioMode = true;
gFM_ScanState = FM_SCAN_OFF;
gFM_RestoreCountdown_10ms = 0;
BK1080_Init(gEeprom.FM_FrequencyPlaying, gEeprom.FM_Band/*, gEeprom.FM_Space*/);
AUDIO_AudioPathOn();
gEnableSpeaker = true;
gUpdateStatus = true;
}
#endif

61
app/fm.h Normal file
View File

@@ -0,0 +1,61 @@
/* Copyright 2023 Dual Tachyon
* https://github.com/DualTachyon
*
* 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_FM_H
#define APP_FM_H
#ifdef ENABLE_FMRADIO
#include "driver/keyboard.h"
#define FM_CHANNEL_UP 0x01
#define FM_CHANNEL_DOWN 0xFF
enum {
FM_SCAN_OFF = 0U,
};
extern uint16_t gFM_Channels[20];
extern bool gFmRadioMode;
extern uint8_t gFmRadioCountdown_500ms;
extern volatile uint16_t gFmPlayCountdown_10ms;
extern volatile int8_t gFM_ScanState;
extern bool gFM_AutoScan;
extern uint8_t gFM_ChannelPosition;
// Doubts about whether this should be signed or not
extern uint16_t gFM_FrequencyDeviation;
extern bool gFM_FoundFrequency;
extern uint16_t gFM_RestoreCountdown_10ms;
bool FM_CheckValidChannel(uint8_t Channel);
// returns first valid channel starting at Channel
uint8_t FM_FindNextChannel(uint8_t Channel, uint8_t Direction);
int FM_ConfigureChannelState(void);
void FM_TurnOff(void);
void FM_EraseChannels(void);
void FM_Tune(uint16_t Frequency, int8_t Step, bool bFlag);
void FM_PlayAndUpdate(void);
int FM_CheckFrequencyLock(uint16_t Frequency, uint16_t LowerLimit);
void FM_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld);
void FM_Play(void);
void FM_Start(void);
#endif
#endif

225
app/generic.c Normal file
View File

@@ -0,0 +1,225 @@
/* Copyright 2023 Dual Tachyon
* https://github.com/DualTachyon
*
* 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.
*/
#include <string.h>
#include "app/app.h"
#include "app/chFrScanner.h"
#include "app/common.h"
#ifdef ENABLE_FMRADIO
#include "app/fm.h"
#endif
#include "app/generic.h"
#include "app/menu.h"
#include "app/scanner.h"
#include "audio.h"
#include "driver/keyboard.h"
#include "dtmf.h"
#include "external/printf/printf.h"
#include "functions.h"
#include "misc.h"
#include "settings.h"
#include "ui/inputbox.h"
#include "ui/ui.h"
void GENERIC_Key_F(bool bKeyPressed, bool bKeyHeld)
{
if (gInputBoxIndex > 0) {
if (!bKeyHeld && bKeyPressed) // short pressed
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
return;
}
if (bKeyHeld || !bKeyPressed) { // held or released
if (bKeyHeld || bKeyPressed) { // held or pressed (cannot be held and not pressed I guess, so it checks only if HELD?)
if (!bKeyHeld) // won't ever pass
return;
if (!bKeyPressed) // won't ever pass
return;
COMMON_KeypadLockToggle();
}
else { // released
#ifdef ENABLE_FMRADIO
if ((gFmRadioMode || gScreenToDisplay != DISPLAY_MAIN) && gScreenToDisplay != DISPLAY_FM)
return;
#else
if (gScreenToDisplay != DISPLAY_MAIN)
return;
#endif
gWasFKeyPressed = !gWasFKeyPressed; // toggle F function
if (gWasFKeyPressed)
gKeyInputCountdown = key_input_timeout_500ms;
#ifdef ENABLE_VOICE
if (!gWasFKeyPressed)
gAnotherVoiceID = VOICE_ID_CANCEL;
#endif
gUpdateStatus = true;
}
}
else { // short pressed
#ifdef ENABLE_FMRADIO
if (gScreenToDisplay != DISPLAY_FM)
#endif
{
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
return;
}
#ifdef ENABLE_FMRADIO
if (gFM_ScanState == FM_SCAN_OFF) { // not scanning
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
return;
}
#endif
gBeepToPlay = BEEP_440HZ_500MS;
gPttWasReleased = true;
}
}
void GENERIC_Key_PTT(bool bKeyPressed)
{
gInputBoxIndex = 0;
if (!bKeyPressed || SerialConfigInProgress())
{ // PTT released
if (gCurrentFunction == FUNCTION_TRANSMIT) {
// we are transmitting .. stop
if (gFlagEndTransmission) {
FUNCTION_Select(FUNCTION_FOREGROUND);
}
else {
APP_EndTransmission();
if (gEeprom.REPEATER_TAIL_TONE_ELIMINATION == 0)
FUNCTION_Select(FUNCTION_FOREGROUND);
else
gRTTECountdown_10ms = gEeprom.REPEATER_TAIL_TONE_ELIMINATION * 10;
}
gFlagEndTransmission = false;
#ifdef ENABLE_VOX
gVOX_NoiseDetected = false;
#endif
RADIO_SetVfoState(VFO_STATE_NORMAL);
if (gScreenToDisplay != DISPLAY_MENU) // 1of11 .. don't close the menu
gRequestDisplayScreen = DISPLAY_MAIN;
}
return;
}
// PTT pressed
if (SCANNER_IsScanning()) {
SCANNER_Stop(); // CTCSS/CDCSS scanning .. stop
goto cancel_tx;
}
if (gScanStateDir != SCAN_OFF) {
CHFRSCANNER_Stop(); // frequency/channel scanning . .stop
goto cancel_tx;
}
#ifdef ENABLE_FMRADIO
if (gFM_ScanState != FM_SCAN_OFF) { // FM radio is scanning .. stop
FM_PlayAndUpdate();
#ifdef ENABLE_VOICE
gAnotherVoiceID = VOICE_ID_SCANNING_STOP;
#endif
gRequestDisplayScreen = DISPLAY_FM;
goto cancel_tx;
}
#endif
#ifdef ENABLE_FMRADIO
if (gScreenToDisplay == DISPLAY_FM)
goto start_tx; // listening to the FM radio .. start TX'ing
#endif
if (gCurrentFunction == FUNCTION_TRANSMIT && gRTTECountdown_10ms == 0) {// already transmitting
gInputBoxIndex = 0;
return;
}
if (gScreenToDisplay != DISPLAY_MENU) // 1of11 .. don't close the menu
gRequestDisplayScreen = DISPLAY_MAIN;
if (!gDTMF_InputMode && gDTMF_InputBox_Index == 0)
goto start_tx; // wasn't entering a DTMF code .. start TX'ing (maybe)
// was entering a DTMF string
if (gDTMF_InputBox_Index > 0 || gDTMF_PreviousIndex > 0) { // going to transmit a DTMF string
if (gDTMF_InputBox_Index == 0 && gDTMF_PreviousIndex > 0)
gDTMF_InputBox_Index = gDTMF_PreviousIndex; // use the previous DTMF string
if (gDTMF_InputBox_Index < sizeof(gDTMF_InputBox))
gDTMF_InputBox[gDTMF_InputBox_Index] = 0; // NULL term the string
#ifdef ENABLE_DTMF_CALLING
// append our DTMF ID to the inputted DTMF code -
// IF the user inputted code is exactly 3 digits long and D-DCD is enabled
if (gDTMF_InputBox_Index == 3 && gTxVfo->DTMF_DECODING_ENABLE > 0)
gDTMF_CallMode = DTMF_CheckGroupCall(gDTMF_InputBox, 3);
else
gDTMF_CallMode = DTMF_CALL_MODE_DTMF;
gDTMF_State = DTMF_STATE_0;
#endif
// remember the DTMF string
gDTMF_PreviousIndex = gDTMF_InputBox_Index;
strcpy(gDTMF_String, gDTMF_InputBox);
gDTMF_ReplyState = DTMF_REPLY_ANI;
}
DTMF_clear_input_box();
start_tx:
// request start TX
gFlagPrepareTX = true;
goto done;
cancel_tx:
if (gPttIsPressed) {
gPttWasPressed = true;
}
done:
gPttDebounceCounter = 0;
if (gScreenToDisplay != DISPLAY_MENU
#ifdef ENABLE_FMRADIO
&& gRequestDisplayScreen != DISPLAY_FM
#endif
) {
// 1of11 .. don't close the menu
gRequestDisplayScreen = DISPLAY_MAIN;
}
gUpdateStatus = true;
gUpdateDisplay = true;
}

26
app/generic.h Normal file
View File

@@ -0,0 +1,26 @@
/* Copyright 2023 Dual Tachyon
* https://github.com/DualTachyon
*
* 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_GENERIC_H
#define APP_GENERIC_H
#include <stdbool.h>
void GENERIC_Key_F(bool bKeyPressed, bool bKeyHeld);
void GENERIC_Key_PTT(bool bKeyPressed);
#endif

743
app/main.c Normal file
View File

@@ -0,0 +1,743 @@
/* Copyright 2023 Dual Tachyon
* https://github.com/DualTachyon
*
* 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.
*/
#include <string.h>
#include "app/action.h"
#include "app/app.h"
#include "app/chFrScanner.h"
#include "app/common.h"
#ifdef ENABLE_FMRADIO
#include "app/fm.h"
#endif
#include "app/generic.h"
#include "app/main.h"
#include "app/scanner.h"
#ifdef ENABLE_SPECTRUM
#include "app/spectrum.h"
#endif
#include "audio.h"
#include "board.h"
#include "driver/bk4819.h"
#include "dtmf.h"
#include "frequencies.h"
#include "misc.h"
#include "radio.h"
#include "settings.h"
#include "ui/inputbox.h"
#include "ui/ui.h"
#include <stdlib.h>
void toggle_chan_scanlist(void)
{ // toggle the selected channels scanlist setting
if (SCANNER_IsScanning())
return;
if(!IS_MR_CHANNEL(gTxVfo->CHANNEL_SAVE)) {
#ifdef ENABLE_SCAN_RANGES
gScanRangeStart = gScanRangeStart ? 0 : gTxVfo->pRX->Frequency;
gScanRangeStop = gEeprom.VfoInfo[!gEeprom.TX_VFO].freq_config_RX.Frequency;
if(gScanRangeStart > gScanRangeStop)
SWAP(gScanRangeStart, gScanRangeStop);
#endif
return;
}
if (gTxVfo->SCANLIST1_PARTICIPATION ^ gTxVfo->SCANLIST2_PARTICIPATION){
gTxVfo->SCANLIST2_PARTICIPATION = gTxVfo->SCANLIST1_PARTICIPATION;
} else {
gTxVfo->SCANLIST1_PARTICIPATION = !gTxVfo->SCANLIST1_PARTICIPATION;
}
SETTINGS_UpdateChannel(gTxVfo->CHANNEL_SAVE, gTxVfo, true);
gVfoConfigureMode = VFO_CONFIGURE;
gFlagResetVfos = true;
}
static void processFKeyFunction(const KEY_Code_t Key, const bool beep)
{
uint8_t Vfo = gEeprom.TX_VFO;
if (gScreenToDisplay == DISPLAY_MENU) {
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
return;
}
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
switch (Key) {
case KEY_0:
#ifdef ENABLE_FMRADIO
ACTION_FM();
#endif
break;
case KEY_1:
if (!IS_FREQ_CHANNEL(gTxVfo->CHANNEL_SAVE)) {
gWasFKeyPressed = false;
gUpdateStatus = true;
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
#ifdef ENABLE_COPY_CHAN_TO_VFO
if (!gEeprom.VFO_OPEN || gCssBackgroundScan) {
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
return;
}
if (gScanStateDir != SCAN_OFF) {
if (gCurrentFunction != FUNCTION_INCOMING ||
gRxReceptionMode == RX_MODE_NONE ||
gScanPauseDelayIn_10ms == 0)
{ // scan is running (not paused)
return;
}
}
const uint8_t vfo = gEeprom.TX_VFO;
if (IS_MR_CHANNEL(gEeprom.ScreenChannel[vfo]))
{ // copy channel to VFO, then swap to the VFO
gEeprom.ScreenChannel[vfo] = FREQ_CHANNEL_FIRST + gEeprom.VfoInfo[vfo].Band;
gEeprom.VfoInfo[vfo].CHANNEL_SAVE = gEeprom.ScreenChannel[vfo];
RADIO_SelectVfos();
RADIO_ApplyOffset(gRxVfo);
RADIO_ConfigureSquelchAndOutputPower(gRxVfo);
RADIO_SetupRegisters(true);
//SETTINGS_SaveChannel(channel, gEeprom.RX_VFO, gRxVfo, 1);
gUpdateDisplay = true;
}
#endif
return;
}
#ifdef ENABLE_WIDE_RX
if(gTxVfo->Band == BAND7_470MHz && gTxVfo->pRX->Frequency < _1GHz_in_KHz) {
gTxVfo->pRX->Frequency = _1GHz_in_KHz;
return;
}
#endif
gTxVfo->Band += 1;
if (gTxVfo->Band == BAND5_350MHz && !gSetting_350EN) {
// skip if not enabled
gTxVfo->Band += 1;
} else if (gTxVfo->Band >= BAND_N_ELEM){
// go arround if overflowed
gTxVfo->Band = BAND1_50MHz;
}
gEeprom.ScreenChannel[Vfo] = FREQ_CHANNEL_FIRST + gTxVfo->Band;
gEeprom.FreqChannel[Vfo] = FREQ_CHANNEL_FIRST + gTxVfo->Band;
gRequestSaveVFO = true;
gVfoConfigureMode = VFO_CONFIGURE_RELOAD;
gRequestDisplayScreen = DISPLAY_MAIN;
if (beep)
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
break;
case KEY_2:
COMMON_SwitchVFOs();
if (beep)
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
break;
case KEY_3:
COMMON_SwitchVFOMode();
if (beep)
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
break;
case KEY_4:
gWasFKeyPressed = false;
gBackup_CROSS_BAND_RX_TX = gEeprom.CROSS_BAND_RX_TX;
gEeprom.CROSS_BAND_RX_TX = CROSS_BAND_OFF;
gUpdateStatus = true;
if (beep)
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
SCANNER_Start(false);
gRequestDisplayScreen = DISPLAY_SCANNER;
break;
case KEY_5:
if(beep) {
#ifdef ENABLE_NOAA
if (!IS_NOAA_CHANNEL(gTxVfo->CHANNEL_SAVE)) {
gEeprom.ScreenChannel[Vfo] = gEeprom.NoaaChannel[gEeprom.TX_VFO];
}
else {
gEeprom.ScreenChannel[Vfo] = gEeprom.FreqChannel[gEeprom.TX_VFO];
#ifdef ENABLE_VOICE
gAnotherVoiceID = VOICE_ID_FREQUENCY_MODE;
#endif
}
gRequestSaveVFO = true;
gVfoConfigureMode = VFO_CONFIGURE_RELOAD;
#elif defined(ENABLE_SPECTRUM)
APP_RunSpectrum();
gRequestDisplayScreen = DISPLAY_MAIN;
#endif
}
else {
#ifdef ENABLE_VOX
toggle_chan_scanlist();
#endif
}
break;
case KEY_6:
ACTION_Power();
break;
case KEY_7:
#ifdef ENABLE_VOX
ACTION_Vox();
#else
toggle_chan_scanlist();
#endif
break;
case KEY_8:
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;
#ifdef ENABLE_VOICE
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;
}
if (beep)
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
break;
default:
gUpdateStatus = true;
gWasFKeyPressed = false;
if (beep)
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
break;
}
}
static void MAIN_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
{
if (bKeyHeld) { // key held down
if (bKeyPressed) {
if (gScreenToDisplay == DISPLAY_MAIN) {
if (gInputBoxIndex > 0) { // delete any inputted chars
gInputBoxIndex = 0;
gRequestDisplayScreen = DISPLAY_MAIN;
}
gWasFKeyPressed = false;
gUpdateStatus = true;
processFKeyFunction(Key, false);
}
}
return;
}
if (bKeyPressed)
{ // key is pressed
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL; // beep when key is pressed
return; // don't use the key till it's released
}
if (!gWasFKeyPressed) { // F-key wasn't pressed
const uint8_t Vfo = gEeprom.TX_VFO;
gKeyInputCountdown = key_input_timeout_500ms;
INPUTBOX_Append(Key);
gRequestDisplayScreen = DISPLAY_MAIN;
if (IS_MR_CHANNEL(gTxVfo->CHANNEL_SAVE)) { // user is entering channel number
if (gInputBoxIndex != 3) {
#ifdef ENABLE_VOICE
gAnotherVoiceID = (VOICE_ID_t)Key;
#endif
gRequestDisplayScreen = DISPLAY_MAIN;
return;
}
gInputBoxIndex = 0;
const uint16_t Channel = ((gInputBox[0] * 100) + (gInputBox[1] * 10) + gInputBox[2]) - 1;
if (!RADIO_CheckValidChannel(Channel, false, 0)) {
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
return;
}
#ifdef ENABLE_VOICE
gAnotherVoiceID = (VOICE_ID_t)Key;
#endif
gEeprom.MrChannel[Vfo] = (uint8_t)Channel;
gEeprom.ScreenChannel[Vfo] = (uint8_t)Channel;
gRequestSaveVFO = true;
gVfoConfigureMode = VFO_CONFIGURE_RELOAD;
return;
}
// #ifdef ENABLE_NOAA
// if (!IS_NOAA_CHANNEL(gTxVfo->CHANNEL_SAVE))
// #endif
if (IS_FREQ_CHANNEL(gTxVfo->CHANNEL_SAVE))
{ // user is entering a frequency
#ifdef ENABLE_VOICE
gAnotherVoiceID = (VOICE_ID_t)Key;
#endif
bool isGigaF = gTxVfo->pRX->Frequency >= _1GHz_in_KHz;
if (gInputBoxIndex < 6 + isGigaF) {
return;
}
gInputBoxIndex = 0;
uint32_t Frequency = StrToUL(INPUTBOX_GetAscii()) * 100;
// clamp the frequency entered to some valid value
if (Frequency < frequencyBandTable[0].lower) {
Frequency = frequencyBandTable[0].lower;
}
else if (Frequency >= BX4819_band1.upper && Frequency < BX4819_band2.lower) {
const uint32_t center = (BX4819_band1.upper + BX4819_band2.lower) / 2;
Frequency = (Frequency < center) ? BX4819_band1.upper : BX4819_band2.lower;
}
else if (Frequency > frequencyBandTable[BAND_N_ELEM - 1].upper) {
Frequency = frequencyBandTable[BAND_N_ELEM - 1].upper;
}
const FREQUENCY_Band_t band = FREQUENCY_GetBand(Frequency);
if (gTxVfo->Band != band) {
gTxVfo->Band = band;
gEeprom.ScreenChannel[Vfo] = band + FREQ_CHANNEL_FIRST;
gEeprom.FreqChannel[Vfo] = band + FREQ_CHANNEL_FIRST;
SETTINGS_SaveVfoIndices();
RADIO_ConfigureChannel(Vfo, VFO_CONFIGURE_RELOAD);
}
Frequency = FREQUENCY_RoundToStep(Frequency, gTxVfo->StepFrequency);
if (Frequency >= BX4819_band1.upper && Frequency < BX4819_band2.lower)
{ // clamp the frequency to the limit
const uint32_t center = (BX4819_band1.upper + BX4819_band2.lower) / 2;
Frequency = (Frequency < center) ? BX4819_band1.upper - gTxVfo->StepFrequency : BX4819_band2.lower;
}
gTxVfo->freq_config_RX.Frequency = Frequency;
gRequestSaveChannel = 1;
return;
}
#ifdef ENABLE_NOAA
else
if (IS_NOAA_CHANNEL(gTxVfo->CHANNEL_SAVE))
{ // user is entering NOAA channel
if (gInputBoxIndex != 2) {
#ifdef ENABLE_VOICE
gAnotherVoiceID = (VOICE_ID_t)Key;
#endif
gRequestDisplayScreen = DISPLAY_MAIN;
return;
}
gInputBoxIndex = 0;
uint8_t Channel = (gInputBox[0] * 10) + gInputBox[1];
if (Channel >= 1 && Channel <= ARRAY_SIZE(NoaaFrequencyTable)) {
Channel += NOAA_CHANNEL_FIRST;
#ifdef ENABLE_VOICE
gAnotherVoiceID = (VOICE_ID_t)Key;
#endif
gEeprom.NoaaChannel[Vfo] = Channel;
gEeprom.ScreenChannel[Vfo] = Channel;
gRequestSaveVFO = true;
gVfoConfigureMode = VFO_CONFIGURE_RELOAD;
return;
}
}
#endif
gRequestDisplayScreen = DISPLAY_MAIN;
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
return;
}
gWasFKeyPressed = false;
gUpdateStatus = true;
processFKeyFunction(Key, true);
}
static void MAIN_Key_EXIT(bool bKeyPressed, bool bKeyHeld)
{
if (!bKeyHeld && bKeyPressed) { // exit key pressed
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
#ifdef ENABLE_DTMF_CALLING
if (gDTMF_CallState != DTMF_CALL_STATE_NONE && gCurrentFunction != FUNCTION_TRANSMIT)
{ // clear CALL mode being displayed
gDTMF_CallState = DTMF_CALL_STATE_NONE;
gUpdateDisplay = true;
return;
}
#endif
#ifdef ENABLE_FMRADIO
if (!gFmRadioMode)
#endif
{
if (gScanStateDir == SCAN_OFF) {
if (gInputBoxIndex == 0)
return;
gInputBox[--gInputBoxIndex] = 10;
gKeyInputCountdown = key_input_timeout_500ms;
#ifdef ENABLE_VOICE
if (gInputBoxIndex == 0)
gAnotherVoiceID = VOICE_ID_CANCEL;
#endif
}
else {
gScanKeepResult = false;
CHFRSCANNER_Stop();
#ifdef ENABLE_VOICE
gAnotherVoiceID = VOICE_ID_SCANNING_STOP;
#endif
}
gRequestDisplayScreen = DISPLAY_MAIN;
return;
}
#ifdef ENABLE_FMRADIO
ACTION_FM();
#endif
return;
}
if (bKeyHeld && bKeyPressed) { // exit key held down
if (gInputBoxIndex > 0 || gDTMF_InputBox_Index > 0 || gDTMF_InputMode)
{ // cancel key input mode (channel/frequency entry)
gDTMF_InputMode = false;
gDTMF_InputBox_Index = 0;
memset(gDTMF_String, 0, sizeof(gDTMF_String));
gInputBoxIndex = 0;
gRequestDisplayScreen = DISPLAY_MAIN;
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
}
}
}
static void MAIN_Key_MENU(const bool bKeyPressed, const bool bKeyHeld)
{
if (bKeyPressed && !bKeyHeld) // menu key pressed
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
if (bKeyHeld) { // menu key held down (long press)
if (bKeyPressed) { // long press MENU key
gWasFKeyPressed = false;
if (gScreenToDisplay == DISPLAY_MAIN) {
if (gInputBoxIndex > 0) { // delete any inputted chars
gInputBoxIndex = 0;
gRequestDisplayScreen = DISPLAY_MAIN;
}
gWasFKeyPressed = false;
gUpdateStatus = true;
ACTION_Handle(KEY_MENU, bKeyPressed, bKeyHeld);
}
}
return;
}
if (!bKeyPressed && !gDTMF_InputMode) { // menu key released
const bool bFlag = !gInputBoxIndex;
gInputBoxIndex = 0;
if (bFlag) {
if (gScanStateDir != SCAN_OFF) {
CHFRSCANNER_Stop();
return;
}
gFlagRefreshSetting = true;
gRequestDisplayScreen = DISPLAY_MENU;
#ifdef ENABLE_VOICE
gAnotherVoiceID = VOICE_ID_MENU;
#endif
}
else {
gRequestDisplayScreen = DISPLAY_MAIN;
}
}
}
static void MAIN_Key_STAR(bool bKeyPressed, bool bKeyHeld)
{
if (gCurrentFunction == FUNCTION_TRANSMIT)
return;
if (gInputBoxIndex) {
if (!bKeyHeld && bKeyPressed)
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
return;
}
if (bKeyHeld && !gWasFKeyPressed){ // long press
if (!bKeyPressed) // released
return;
ACTION_Scan(false);// toggle scanning
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
return;
}
if (bKeyPressed) { // just pressed
return;
}
// just released
if (!gWasFKeyPressed) // pressed without the F-key
{
if (gScanStateDir == SCAN_OFF
#ifdef ENABLE_NOAA
&& !IS_NOAA_CHANNEL(gTxVfo->CHANNEL_SAVE)
#endif
#ifdef ENABLE_SCAN_RANGES
&& gScanRangeStart == 0
#endif
)
{ // start entering a DTMF string
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
memcpy(gDTMF_InputBox, gDTMF_String, MIN(sizeof(gDTMF_InputBox), sizeof(gDTMF_String) - 1));
gDTMF_InputBox_Index = 0;
gDTMF_InputMode = true;
gKeyInputCountdown = key_input_timeout_500ms;
gRequestDisplayScreen = DISPLAY_MAIN;
}
else
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
}
else
{ // with the F-key
gWasFKeyPressed = false;
#ifdef ENABLE_NOAA
if (IS_NOAA_CHANNEL(gTxVfo->CHANNEL_SAVE)) {
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
return;
}
#endif
// scan the CTCSS/DCS code
gBackup_CROSS_BAND_RX_TX = gEeprom.CROSS_BAND_RX_TX;
gEeprom.CROSS_BAND_RX_TX = CROSS_BAND_OFF;
SCANNER_Start(true);
gRequestDisplayScreen = DISPLAY_SCANNER;
}
gPttWasReleased = true;
gUpdateStatus = true;
}
static void MAIN_Key_UP_DOWN(bool bKeyPressed, bool bKeyHeld, int8_t Direction)
{
uint8_t Channel = gEeprom.ScreenChannel[gEeprom.TX_VFO];
if (bKeyHeld || !bKeyPressed) { // key held or released
if (gInputBoxIndex > 0)
return; // leave if input box active
if (!bKeyPressed) {
if (!bKeyHeld || IS_FREQ_CHANNEL(Channel))
return;
// if released long button press and not in freq mode
#ifdef ENABLE_VOICE
AUDIO_SetDigitVoice(0, gTxVfo->CHANNEL_SAVE + 1); // say channel number
gAnotherVoiceID = (VOICE_ID_t)0xFE;
#endif
return;
}
}
else { // short pressed
if (gInputBoxIndex > 0) {
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
return;
}
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
}
if (gScanStateDir == SCAN_OFF) {
#ifdef ENABLE_NOAA
if (!IS_NOAA_CHANNEL(Channel))
#endif
{
uint8_t Next;
if (IS_FREQ_CHANNEL(Channel)) { // step/down in frequency
const uint32_t frequency = APP_SetFrequencyByStep(gTxVfo, Direction);
if (RX_freq_check(frequency) < 0) { // frequency not allowed
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
return;
}
gTxVfo->freq_config_RX.Frequency = frequency;
BK4819_SetFrequency(frequency);
BK4819_RX_TurnOn();
gRequestSaveChannel = 1;
return;
}
Next = RADIO_FindNextChannel(Channel + Direction, Direction, false, 0);
if (Next == 0xFF)
return;
if (Channel == Next)
return;
gEeprom.MrChannel[gEeprom.TX_VFO] = Next;
gEeprom.ScreenChannel[gEeprom.TX_VFO] = Next;
if (!bKeyHeld) {
#ifdef ENABLE_VOICE
AUDIO_SetDigitVoice(0, Next + 1);
gAnotherVoiceID = (VOICE_ID_t)0xFE;
#endif
}
}
#ifdef ENABLE_NOAA
else {
Channel = NOAA_CHANNEL_FIRST + NUMBER_AddWithWraparound(gEeprom.ScreenChannel[gEeprom.TX_VFO] - NOAA_CHANNEL_FIRST, Direction, 0, 9);
gEeprom.NoaaChannel[gEeprom.TX_VFO] = Channel;
gEeprom.ScreenChannel[gEeprom.TX_VFO] = Channel;
}
#endif
gRequestSaveVFO = true;
gVfoConfigureMode = VFO_CONFIGURE_RELOAD;
return;
}
// jump to the next channel
CHFRSCANNER_Start(false, Direction);
gScanPauseDelayIn_10ms = 1;
gScheduleScanListen = false;
gPttWasReleased = true;
}
void MAIN_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
{
#ifdef ENABLE_FMRADIO
if (gFmRadioMode && Key != KEY_PTT && Key != KEY_EXIT) {
if (!bKeyHeld && bKeyPressed)
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
return;
}
#endif
if (gDTMF_InputMode && bKeyPressed && !bKeyHeld) {
const char Character = DTMF_GetCharacter(Key);
if (Character != 0xFF)
{ // add key to DTMF string
DTMF_Append(Character);
gKeyInputCountdown = key_input_timeout_500ms;
gRequestDisplayScreen = DISPLAY_MAIN;
gPttWasReleased = true;
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
return;
}
}
// TODO: ???
// if (Key > KEY_PTT)
// {
// Key = KEY_SIDE2; // what's this doing ???
// }
switch (Key) {
case KEY_0...KEY_9:
MAIN_Key_DIGITS(Key, bKeyPressed, bKeyHeld);
break;
case KEY_MENU:
MAIN_Key_MENU(bKeyPressed, bKeyHeld);
break;
case KEY_UP:
MAIN_Key_UP_DOWN(bKeyPressed, bKeyHeld, 1);
break;
case KEY_DOWN:
MAIN_Key_UP_DOWN(bKeyPressed, bKeyHeld, -1);
break;
case KEY_EXIT:
MAIN_Key_EXIT(bKeyPressed, bKeyHeld);
break;
case KEY_STAR:
MAIN_Key_STAR(bKeyPressed, bKeyHeld);
break;
case KEY_F:
GENERIC_Key_F(bKeyPressed, bKeyHeld);
break;
case KEY_PTT:
GENERIC_Key_PTT(bKeyPressed);
break;
default:
if (!bKeyHeld && bKeyPressed)
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
break;
}
}

25
app/main.h Normal file
View File

@@ -0,0 +1,25 @@
/* Copyright 2023 Dual Tachyon
* https://github.com/DualTachyon
*
* 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_MAIN_H
#define APP_MAIN_H
#include "driver/keyboard.h"
void MAIN_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld);
#endif

1739
app/menu.c Normal file

File diff suppressed because it is too large Load Diff

38
app/menu.h Normal file
View File

@@ -0,0 +1,38 @@
/* Copyright 2023 Dual Tachyon
* https://github.com/DualTachyon
*
* 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_MENU_H
#define APP_MENU_H
#include "driver/keyboard.h"
#ifdef ENABLE_F_CAL_MENU
void writeXtalFreqCal(const int32_t value, const bool update_eeprom);
#endif
extern uint8_t gUnlockAllTxConfCnt;
int MENU_GetLimits(uint8_t menu_id, int32_t *pMin, int32_t *pMax);
void MENU_AcceptSetting(void);
void MENU_ShowCurrentSetting(void);
void MENU_StartCssScan(void);
void MENU_CssScanFound(void);
void MENU_StopCssScan(void);
void MENU_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld);
#endif

333
app/mode.c Normal file
View File

@@ -0,0 +1,333 @@
/* UA1ZBE Custom Firmware - Mode Dispatcher Implementation
*
* Central dispatcher for VFO, POCSAG, Spectrum, and FM modes.
*
* Architecture:
* - VFO mode: use full APP_TimeSlice infrastructure
* - POCSAG mode: use APP_TimeSlice for interrupts but intercept keys
* - Spectrum: blocking loop, returns to VFO
* - FM: uses FM radio subsystem
*/
#include "mode.h"
#include "app/app.h"
#include "app/common.h"
#include "app/display_rssi.h"
#include "driver/bk4819.h"
#include "driver/keyboard.h"
#include "driver/st7565.h"
#include "driver/system.h"
#include "ui/helper.h"
#include "ui/main.h"
#include "misc.h"
#include "radio.h"
#include "settings.h"
#include "functions.h"
#include <string.h>
#include <stdio.h>
#ifdef ENABLE_POCSAG
#include "pocsag/pocsag.h"
#endif
#ifdef ENABLE_SPECTRUM
#include "app/spectrum.h"
#endif
#ifdef ENABLE_FMRADIO
#include "app/fm.h"
#endif
/* === State === */
static op_mode_t s_current_mode = MODE_VFO;
static GUI_DisplayType_t s_saved_display_type;
/* === Mode entry/exit === */
static void mode_enter_vfo(void)
{
RADIO_SetupRegisters(true);
gScreenToDisplay = s_saved_display_type;
gUpdateDisplay = true;
}
static void mode_enter_pocsag(void)
{
#ifdef ENABLE_POCSAG
POCSAG_ConfigureRadio(gRxVfo->freq_config_RX.Frequency);
POCSAG_Init(POCSAG_BAUD_1200);
/* Save current display type and set to INVALID to skip standard key handling */
s_saved_display_type = gScreenToDisplay;
gScreenToDisplay = DISPLAY_INVALID;
ST7565_FillScreen(0x00);
for (int line = 0; line < FRAME_LINES; line++)
memset(gFrameBuffer[line], 0, LCD_WIDTH);
memset(gStatusLine, 0, sizeof(gStatusLine));
#endif
}
static void mode_enter_fm(void)
{
#ifdef ENABLE_FMRADIO
s_saved_display_type = gScreenToDisplay;
gFmRadioMode = false;
#endif
}
static void mode_exit_current(void)
{
switch (s_current_mode) {
case MODE_POCSAG:
#ifdef ENABLE_POCSAG
POCSAG_Stop();
gScreenToDisplay = s_saved_display_type;
#endif
break;
case MODE_FM:
#ifdef ENABLE_FMRADIO
FM_TurnOff();
gFmRadioMode = false;
gScreenToDisplay = s_saved_display_type;
#endif
break;
default:
break;
}
}
/* === Mode switching === */
void MODE_Switch(op_mode_t mode)
{
if (mode == s_current_mode || mode >= MODE_COUNT)
return;
mode_exit_current();
s_current_mode = mode;
switch (mode) {
case MODE_VFO: mode_enter_vfo(); break;
case MODE_POCSAG: mode_enter_pocsag(); break;
case MODE_SPECTRUM: /* Spectrum is blocking */ break;
case MODE_FM: mode_enter_fm(); break;
default: break;
}
gUpdateDisplay = true;
}
op_mode_t MODE_GetCurrent(void)
{
return s_current_mode;
}
void MODE_Init(void)
{
s_current_mode = MODE_VFO;
s_saved_display_type = DISPLAY_MAIN;
mode_enter_vfo();
}
/* === POCSAG display === */
static void pocsag_draw(void)
{
#ifdef ENABLE_POCSAG
ST7565_FillScreen(0x00);
for (int line = 0; line < FRAME_LINES; line++)
memset(gFrameBuffer[line], 0, LCD_WIDTH);
memset(gStatusLine, 0, sizeof(gStatusLine));
char buf[32];
/* Header */
uint32_t baud = POCSAG_GetBaud();
sprintf(buf, "POCSAG %lu", baud);
UI_PrintString(buf, 0, LCD_WIDTH, 0, 10);
/* Status */
const char *state_str = POCSAG_StateString(POCSAG_GetState());
UI_PrintStringSmallNormal(state_str, 0, LCD_WIDTH, 16);
if (POCSAG_SignalDetected())
UI_PrintStringSmallNormal("SIG", 100, LCD_WIDTH, 16);
/* Message count and stats */
sprintf(buf, "Msgs:%d W:%lu", gPocsag.msg_count, gPocsag.total_words);
UI_PrintStringSmallNormal(buf, 0, LCD_WIDTH, 28);
sprintf(buf, "Corr:%lu", gPocsag.corrected_errors);
UI_PrintStringSmallNormal(buf, 0, LCD_WIDTH, 38);
/* RSSI */
sprintf(buf, "%ddBm", RSSI_GetdBm());
UI_PrintStringSmallNormal(buf, 0, LCD_WIDTH, 48);
/* Help */
UI_PrintStringSmallNormal("F=512/1200 X=VFO", 0, LCD_WIDTH, 56);
ST7565_BlitStatusLine();
ST7565_BlitFullScreen();
#endif
}
/* === Key debounce for POCSAG === */
static KEY_Code_t s_last_key = KEY_INVALID;
static uint16_t s_key_debounce = 0;
static KEY_Code_t read_key_debounced(void)
{
KEY_Code_t key = KEYBOARD_Poll();
if (key == s_last_key) {
if (s_key_debounce < 3) {
s_key_debounce++;
return KEY_INVALID;
}
} else {
s_last_key = key;
s_key_debounce = 0;
return KEY_INVALID;
}
return key;
}
/* === Timeslice handlers === */
void MODE_TimeSlice10ms(void)
{
/* Always call standard app processing for interrupts, etc. */
APP_TimeSlice10ms();
/* Mode-specific processing */
switch (s_current_mode) {
case MODE_VFO:
/* VFO display already handled by APP_TimeSlice10ms.
* Add RSSI overlay on top. */
if (gScreenToDisplay == DISPLAY_MAIN) {
RSSI_Draw(95, 0, false);
ST7565_BlitFullScreen();
}
break;
case MODE_POCSAG:
#ifdef ENABLE_POCSAG
/* Feed audio samples to POCSAG decoder */
{
uint16_t audio = BK4819_GetVoiceAmplitudeOut();
POCSAG_FeedSample(audio);
}
POCSAG_Process();
/* Check for messages */
if (POCSAG_MessageAvailable()) {
pocsag_msg_t msg;
if (POCSAG_GetMessage(&msg)) {
/* New message received */
}
}
/* Process POCSAG-specific keys (gScreenToDisplay is INVALID) */
{
KEY_Code_t Key = read_key_debounced();
if (Key != KEY_INVALID) {
if (Key == KEY_EXIT) {
MODE_Switch(MODE_VFO);
} else if (Key == KEY_F || Key == KEY_STAR) {
uint32_t cur = POCSAG_GetBaud();
POCSAG_SwitchBaud(cur == POCSAG_BAUD_1200
? POCSAG_BAUD_512
: POCSAG_BAUD_1200);
}
}
}
/* Redraw POCSAG display periodically */
{
static uint16_t rc = 0;
if (++rc >= 5) {
rc = 0;
pocsag_draw();
}
}
#endif
break;
case MODE_SPECTRUM:
/* Spectrum has returned — go back to VFO */
MODE_Switch(MODE_VFO);
break;
case MODE_FM:
#ifdef ENABLE_FMRADIO
if (!gFmRadioMode)
MODE_Switch(MODE_VFO);
#endif
break;
default:
break;
}
}
void MODE_TimeSlice500ms(void)
{
switch (s_current_mode) {
case MODE_VFO:
APP_TimeSlice500ms();
break;
default:
break;
}
}
/* === External key processing hook === */
bool MODE_ProcessKey(int key, bool pressed, bool held)
{
KEY_Code_t k = (KEY_Code_t)key;
if (k == KEY_EXIT && pressed && !held) {
if (s_current_mode != MODE_VFO) {
MODE_Switch(MODE_VFO);
return true;
}
return false;
}
if (pressed && !held) {
switch (s_current_mode) {
case MODE_VFO:
if (k == KEY_SIDE2) {
#ifdef ENABLE_POCSAG
MODE_Switch(MODE_POCSAG);
return true;
#endif
}
if (k == KEY_SIDE1) {
#ifdef ENABLE_SPECTRUM
MODE_Switch(MODE_SPECTRUM);
return true;
#endif
}
if (k == KEY_0) {
#ifdef ENABLE_FMRADIO
MODE_Switch(MODE_FM);
FM_Start();
return true;
#endif
}
return false;
default:
return false;
}
}
return false;
}

50
app/mode.h Normal file
View File

@@ -0,0 +1,50 @@
/* UA1ZBE Custom Firmware - Mode Dispatcher
*
* Manages switching between operating modes:
* MODE_VFO — Main VFO mode (default)
* MODE_POCSAG — POCSAG decoder
* MODE_SPECTRUM — Spectrum analyzer
* MODE_FM — FM radio receiver
*
* Key mappings:
* KEY_SIDE2 (SK2) → POCSAG mode
* KEY_SIDE1 (SK1) → Spectrum mode
* KEY_EXIT → Return to VFO
* KEY_0 (short) → FM mode
*/
#ifndef APP_MODE_H
#define APP_MODE_H
#include <stdint.h>
#include <stdbool.h>
/* Operating modes */
typedef enum {
MODE_VFO = 0,
MODE_POCSAG,
MODE_SPECTRUM,
MODE_FM,
MODE_COUNT
} op_mode_t;
/* Get current mode */
op_mode_t MODE_GetCurrent(void);
/* Switch to a mode */
void MODE_Switch(op_mode_t mode);
/* Initialize mode dispatcher (call once at boot) */
void MODE_Init(void);
/* Call from 10ms timeslice */
void MODE_TimeSlice10ms(void);
/* Call from 500ms timeslice */
void MODE_TimeSlice500ms(void);
/* Process key press for mode switching.
* Returns true if the key was consumed by mode switch. */
bool MODE_ProcessKey(int key, bool pressed, bool held);
#endif

228
app/pocsag/bch31.c Normal file
View File

@@ -0,0 +1,228 @@
/* UA1ZBE Custom Firmware - BCH(31,21) Decoder for POCSAG
*
* POCSAG uses a shortened BCH(31,21) code:
* - 31-bit codeword (n=31)
* - 21 data bits (k=21)
* - 10 parity/check bits (n-k=10)
* - Can correct up to 2 bit errors per codeword
*
* Generator polynomial: g(x) = x^10 + x^9 + x^8 + x^6 + x^5 + x^3 + 1
* = 0x72D (binary: 111 0010 1101)
*
* The full 32-bit word includes:
* - Bit 31: even parity bit (bit 0 of the word after inversion)
* - Bits 30-0: 31-bit BCH codeword
*
* This implementation avoids hardware division (Cortex-M0 has no DIV unit).
* All operations use shifts and XOR (GF(2) arithmetic).
*/
#include "pocsag.h"
#include <stdint.h>
#include <stdbool.h>
/* Generator polynomial for BCH(31,21): x^10 + x^9 + x^8 + x^6 + x^5 + x^3 + 1 */
#define BCH31_GEN_POLY 0x72DU /* 11100101101 binary */
/* POCSAG sync word (32-bit) */
#define POCSAG_SYNC_WORD 0x7CD21538U
/* POCSAG idle word */
#define POCSAG_IDLE_WORD 0x7A89C197U
/* Parity check mask for even parity */
#define BCH31_PARITY_MASK 0x80000000U
/*
* Calculate syndrome of a 31-bit BCH codeword.
* The syndrome is the remainder of dividing the received word by g(x).
* If syndrome == 0, the word is valid (no errors or undetectable errors).
*
* word: 31-bit codeword (bits 30:0, parity bit excluded)
* Returns: 10-bit syndrome value
*/
static uint16_t bch31_syndrome(uint32_t word)
{
uint32_t reg = word & 0x7FFFFFFFU; /* Mask to 31 bits */
int i;
/* Polynomial division in GF(2) using shift-and-XOR */
/* We process from MSB to LSB, XORing with generator when MSB is 1 */
for (i = 30; i >= 10; i--) {
if (reg & ((uint32_t)1 << i)) {
reg ^= (BCH31_GEN_POLY << (i - 10));
}
}
/* The remainder is in the lower 10 bits */
return (uint16_t)(reg & 0x03FFU);
}
/*
* Check even parity of a 32-bit word.
* Returns true if parity is correct (even number of 1-bits).
*/
static bool bch31_check_parity(uint32_t word)
{
/* Count set bits using a lookup-free method (no division needed) */
uint32_t v = word;
v = v - ((v >> 1) & 0x55555555U);
v = (v & 0x33333333U) + ((v >> 2) & 0x33333333U);
v = (v + (v >> 4)) & 0x0F0F0F0FU;
v = (v * 0x01010101U) >> 24; /* Sum of all bytes */
return (v & 1U) == 0; /* Even parity = even number of 1-bits */
}
/*
* Find error position from syndrome.
* For single-bit errors, the syndrome directly maps to the error position.
* For double-bit errors, we need more complex correction.
*
* syndrome: 10-bit syndrome value
* Returns: bit position (0-30) if single error, 0 if no error,
* or a special value for double errors.
*/
static int bch31_find_single_error(uint16_t syndrome)
{
if (syndrome == 0)
return -1; /* No error */
/* For single-bit errors, syndrome = x^i mod g(x) for error at position i.
* We try each position by computing the expected syndrome.
* This avoids division — just shift and XOR. */
uint32_t test_syn = 1; /* Start with x^0 mod g(x) = 1 */
for (int i = 0; i < 31; i++) {
if (test_syn == syndrome)
return i; /* Error at position i */
/* Multiply by x in GF(2^10 / g(x)):
* Shift left; if bit 10 is set, XOR with generator */
test_syn <<= 1;
if (test_syn & 0x0400U) { /* Bit 10 set */
test_syn ^= BCH31_GEN_POLY;
}
test_syn &= 0x03FFU; /* Keep 10 bits */
}
return -2; /* Not a single-bit error */
}
/*
* Attempt to correct double-bit errors using syndrome decoding.
* For a (31,21) BCH code with d_min=5, we can correct up to 2 errors.
*
* This uses a simplified approach: try all pairs of error positions.
* For performance on Cortex-M0, we use a precomputed approach.
*
* word: pointer to the 32-bit word (will be modified in place if corrected)
* Returns: 0 = no error, 1 = single error corrected,
* 2 = double error corrected, -1 = uncorrectable
*/
int bch31_correct(uint32_t *word)
{
uint32_t data = *word;
/* Step 1: Check parity */
bool parity_ok = bch31_check_parity(data);
/* Extract 31-bit codeword (strip parity bit 31) */
uint32_t codeword = data & 0x7FFFFFFFU;
/* Step 2: Calculate syndrome */
uint16_t syn = bch31_syndrome(codeword);
if (syn == 0 && parity_ok) {
/* No errors detected */
return 0;
}
/* Step 3: Try single-bit error correction */
int err_pos = bch31_find_single_error(syn);
if (err_pos >= 0) {
/* Single-bit error at position err_pos */
codeword ^= ((uint32_t)1 << err_pos);
/* Fix parity bit too */
*word = codeword | ((uint32_t)bch31_check_parity(codeword) << 31);
return 1;
}
/* Step 4: Try double-bit error correction
*
* For double errors at positions i and j:
* syndrome S = x^i + x^j (mod g(x))
*
* We use a brute-force search over all pairs (i, j) where i > j.
* For 31 bits, this is 31*30/2 = 465 pairs — acceptable.
*/
if (err_pos == -2) {
/* Precompute all single-error syndromes */
uint16_t single_syn[31];
uint32_t test = 1;
for (int i = 0; i < 31; i++) {
single_syn[i] = (uint16_t)test;
test <<= 1;
if (test & 0x0400U)
test ^= BCH31_GEN_POLY;
test &= 0x03FFU;
}
/* Search for pair (i, j) where syn_i XOR syn_j == syn */
for (int i = 1; i < 31; i++) {
for (int j = 0; j < i; j++) {
if ((single_syn[i] ^ single_syn[j]) == syn) {
/* Found double error at positions i and j */
codeword ^= ((uint32_t)1 << i);
codeword ^= ((uint32_t)1 << j);
*word = codeword | ((uint32_t)bch31_check_parity(codeword) << 31);
return 2;
}
}
}
}
/* Step 5: If syndrome != 0 but we couldn't find error positions,
* check if it might still be valid (parity might catch it).
* POCSAG spec says words with uncorrectable errors should be discarded.
*/
return -1; /* Uncorrectable */
}
/*
* Extract 21 data bits from a corrected 32-bit POCSAG word.
* Returns the 21-bit data value.
*/
uint32_t bch31_get_data(uint32_t word)
{
/* Data bits are in positions 30:10 of the 31-bit codeword
* (bit 31 is parity, bits 9:0 are check bits)
* So data = bits [30:10] = (word >> 10) & 0x1FFFFF */
return (word >> 10) & 0x001FFFFFU;
}
/*
* Get function bits from a POCSAG address word.
* Address words have function code in bits 11:10 of the data portion.
*/
uint8_t bch31_get_func(uint32_t word)
{
/* Function bits are data bits [11:10] = bits [21:20] of full word */
return (uint8_t)((word >> 20) & 0x03U);
}
/*
* Check if a word is a sync word.
*/
bool bch31_is_sync(uint32_t word)
{
return word == POCSAG_SYNC_WORD;
}
/*
* Check if a word is an idle word.
*/
bool bch31_is_idle(uint32_t word)
{
return word == POCSAG_IDLE_WORD;
}

25
app/pocsag/bch31.h Normal file
View File

@@ -0,0 +1,25 @@
/* UA1ZBE Custom Firmware - BCH(31,21) header */
#ifndef APP_POCSAG_BCH31_H
#define APP_POCSAG_BCH31_H
#include <stdint.h>
#include <stdbool.h>
/* Correct errors in a POCSAG 32-bit word in place.
* Returns: 0 = no error, 1 = single corrected,
* 2 = double corrected, -1 = uncorrectable */
int bch31_correct(uint32_t *word);
/* Extract 21 data bits from corrected word */
uint32_t bch31_get_data(uint32_t word);
/* Get 2-bit function code */
uint8_t bch31_get_func(uint32_t word);
/* Check if word is sync word */
bool bch31_is_sync(uint32_t word);
/* Check if word is idle word */
bool bch31_is_idle(uint32_t word);
#endif

542
app/pocsag/pocsag.c Normal file
View File

@@ -0,0 +1,542 @@
/* UA1ZBE Custom Firmware - POCSAG Decoder Implementation
*
* Implements POCSAG decoding from FM discriminator audio samples.
* Uses zero-crossing detection for bit slicing.
*
* Signal chain:
* BK4819 FM demod -> AF output -> ADC sampling
* -> zero-crossing detection -> bit stream -> POCSAG decode
*
* Cortex-M0 optimized: no hardware division in hot path.
* All memory statically allocated.
*/
#include "pocsag.h"
#include "bch31.h"
#include "driver/bk4819.h"
#include "driver/system.h"
#include "driver/st7565.h"
#include "misc.h"
#include "radio.h"
#include <string.h>
/* === Configuration === */
/* Sampling rate for audio (Hz) — must be much higher than max baud rate */
#define POCSAG_SAMPLE_RATE 8000 /* 8 kHz — sufficient for 1200 baud */
/* Threshold for zero-crossing detection (ADC counts) */
#define POCSAG_ZERO_THRESHOLD 2048 /* Midpoint of 12-bit ADC range (0-4095) */
/* Minimum preamble bits to detect (POCSAG spec: 576 bits of 0x55 + 1) */
#define POCSAG_PREAMBLE_MIN_BITS 576
/* BPSK bit extraction: compare sample against threshold */
#define IS_BIT_ONE(sample) ((sample) > POCSAG_ZERO_THRESHOLD)
/* === Global decoder instance === */
pocsag_decoder_t gPocsag;
/* === Internal state for bit slicing === */
/* Samples since last bit decision */
static uint16_t s_sample_counter;
/* Samples per bit for current baud rate */
static uint16_t s_samples_per_bit;
/* Accumulated samples for current bit decision */
static uint32_t s_bit_accumulator;
static uint8_t s_bit_sample_count;
/* Preamble detection: count alternating bits */
static uint16_t s_preamble_alt_count;
static uint8_t s_last_bit;
/* Sync detection shift register (holds last 32 bits) */
static uint32_t s_sync_reg;
/* Bit buffer for assembling 32-bit words */
static uint32_t s_word_reg;
static uint8_t s_word_bits;
/* Batch tracking */
static uint8_t s_batch_words;
/* Address/message assembly */
static uint32_t s_msg_address;
static uint8_t s_msg_func;
static uint8_t s_msg_buf[POCSAG_MSG_MAX_LEN];
static uint8_t s_msg_len;
static bool s_msg_is_alpha;
/* Alpha decoder state */
static uint32_t s_alpha_bits;
static uint8_t s_alpha_bit_count;
/* State */
static pocsag_state_t s_state;
/* Previous save state for restoration */
static uint16_t s_saved_reg_30;
static uint16_t s_saved_reg_47;
static uint16_t s_saved_reg_33;
/* Timing counter — increments each time FeedSample is called (every ~10ms) */
static uint32_t s_tick_counter;
/* Last sync timestamp */
static uint32_t s_last_sync_tick;
/* === Baud rate lookup === */
/* Samples per bit for supported baud rates at 8kHz sample rate */
static uint16_t get_samples_per_bit(uint32_t baud)
{
/* Avoid division: use precomputed values
* 8000 / 512 = 15.625 -> 16
* 8000 / 1200 = 6.667 -> 7 */
if (baud == POCSAG_BAUD_512)
return 16;
if (baud == POCSAG_BAUD_1200)
return 7;
return 7; /* Default to 1200 */
}
/* === Message ring buffer === */
static void msg_push(const pocsag_msg_t *msg)
{
if (gPocsag.msg_count >= POCSAG_MSG_POOL_SIZE) {
/* Buffer full — drop oldest */
gPocsag.msg_read_idx = (gPocsag.msg_read_idx + 1) % POCSAG_MSG_POOL_SIZE;
gPocsag.msg_count--;
}
gPocsag.messages[gPocsag.msg_write_idx] = *msg;
gPocsag.msg_write_idx = (gPocsag.msg_write_idx + 1) % POCSAG_MSG_POOL_SIZE;
gPocsag.msg_count++;
gPocsag.msg_available = true;
}
/* === POCSAG word processing === */
static void process_pocsag_word(uint32_t word)
{
/* Check parity first — quick reject using bit count */
uint32_t v = word;
v = v - ((v >> 1) & 0x55555555U);
v = (v & 0x33333333U) + ((v >> 2) & 0x33333333U);
v = (v + (v >> 4)) & 0x0F0F0F0FU;
v = (v * 0x01010101U) >> 24;
bool parity_ok = (v & 1U) == 0;
/* Try BCH correction */
int corr = bch31_correct(&word);
if (corr < 0 && !parity_ok) {
/* Uncorrectable — discard */
gPocsag.uncorrectable_errors++;
if (s_state == POCSAG_STATE_DATA) {
s_state = POCSAG_STATE_SYNC;
s_batch_words = 0;
}
return;
}
if (corr > 0)
gPocsag.corrected_errors++;
gPocsag.total_words++;
/* Check for sync word */
if (bch31_is_sync(word)) {
s_batch_words = 0;
s_last_sync_tick = s_tick_counter;
return;
}
/* Check for idle word */
if (bch31_is_idle(word)) {
return; /* Skip idle words */
}
/* In DATA state, process the word */
if (s_state != POCSAG_STATE_DATA) {
s_state = POCSAG_STATE_DATA;
s_batch_words = 0;
}
/* Even/odd position in batch determines address vs data */
if (s_batch_words % 2 == 0) {
/* Address word */
s_msg_address = bch31_get_data(word);
s_msg_func = bch31_get_func(word);
s_msg_len = 0;
s_msg_is_alpha = false;
s_msg_buf[0] = '\0';
/* Reset alpha decoder state */
s_alpha_bits = 0;
s_alpha_bit_count = 0;
} else {
/* Data word */
uint32_t data = bch31_get_data(word);
if (s_msg_func == 0) {
/* Numeric pager — data is BCD digits */
uint32_t num = data & 0x000FFFFFU;
char digits[12];
int idx = 0;
if (num == 0) {
digits[idx++] = '0';
} else {
/* Extract digits using precomputed powers of 10 (no division) */
static const uint32_t powers[] = {
1000000000U, 100000000U, 10000000U, 1000000U,
100000U, 10000U, 1000U, 100U, 10U, 1U
};
bool started = false;
for (int p = 0; p < 10; p++) {
uint32_t pwr = powers[p];
uint8_t digit = 0;
while (num >= pwr) {
num -= pwr;
digit++;
}
if (digit > 0 || started || p == 9) {
digits[idx++] = '0' + digit;
started = true;
}
}
}
digits[idx] = '\0';
/* Push message */
if (s_msg_len == 0) {
pocsag_msg_t msg;
memset(&msg, 0, sizeof(msg));
msg.address = s_msg_address;
msg.func = s_msg_func;
msg.is_alpha = false;
msg.timestamp = s_tick_counter;
int copy_len = idx;
if (copy_len > POCSAG_MSG_MAX_LEN - 1)
copy_len = POCSAG_MSG_MAX_LEN - 1;
memcpy(msg.text, digits, copy_len);
msg.text[copy_len] = '\0';
msg.text_len = copy_len;
msg_push(&msg);
}
} else if (s_msg_func == 2) {
/* Alpha pager — 7-bit ASCII, 20 bits per word */
uint32_t bits20 = data & 0x000FFFFFU;
s_alpha_bits = (s_alpha_bits << 20) | bits20;
s_alpha_bit_count += 20;
while (s_alpha_bit_count >= 7 && s_msg_len < POCSAG_MSG_MAX_LEN - 1) {
s_alpha_bit_count -= 7;
uint8_t ch = (s_alpha_bits >> s_alpha_bit_count) & 0x7F;
if (ch == 0x03) {
/* ETX — end of message */
s_msg_buf[s_msg_len] = '\0';
pocsag_msg_t msg;
memset(&msg, 0, sizeof(msg));
msg.address = s_msg_address;
msg.func = s_msg_func;
msg.is_alpha = true;
msg.timestamp = s_tick_counter;
memcpy(msg.text, s_msg_buf, s_msg_len);
msg.text[s_msg_len] = '\0';
msg.text_len = s_msg_len;
msg_push(&msg);
s_msg_len = 0;
s_alpha_bits = 0;
s_alpha_bit_count = 0;
break;
}
s_msg_buf[s_msg_len++] = ch;
}
s_msg_is_alpha = true;
}
}
s_batch_words++;
/* After 32 words, expect new sync */
if (s_batch_words >= 32) {
s_state = POCSAG_STATE_SYNC;
s_batch_words = 0;
}
}
/* === Bit processing === */
static void process_bit(bool bit)
{
gPocsag.total_bits++;
switch (s_state) {
case POCSAG_STATE_IDLE:
case POCSAG_STATE_PREAMBLE:
s_state = POCSAG_STATE_PREAMBLE;
/* Look for alternating pattern: 01010101... (0x55) ending with 1 (0xAB) */
if (s_preamble_alt_count == 0) {
s_last_bit = bit ? 1 : 0;
s_preamble_alt_count = 1;
} else if ((bit ? 1 : 0) != s_last_bit) {
s_preamble_alt_count++;
s_last_bit = bit ? 1 : 0;
} else {
s_preamble_alt_count = 1;
s_last_bit = bit ? 1 : 0;
}
if (s_preamble_alt_count >= POCSAG_PREAMBLE_MIN_BITS) {
/* Preamble detected — look for sync word */
s_state = POCSAG_STATE_SYNC;
s_sync_reg = 0;
s_word_bits = 0;
s_preamble_alt_count = 0;
s_last_sync_tick = s_tick_counter;
}
break;
case POCSAG_STATE_SYNC:
/* Collect bits into 32-bit sync register */
s_sync_reg = (s_sync_reg << 1) | (bit ? 1 : 0);
if (s_sync_reg == 0x7CD21538U) {
/* Sync word found! */
s_state = POCSAG_STATE_DATA;
s_word_reg = 0;
s_word_bits = 0;
s_batch_words = 0;
gPocsag.signal_detected = true;
s_last_sync_tick = s_tick_counter;
}
/* Timeout: ~1000 bits at 1200 baud ≈ 833ms ≈ 83 calls */
if ((s_tick_counter - s_last_sync_tick) > 100) {
s_state = POCSAG_STATE_IDLE;
s_preamble_alt_count = 0;
gPocsag.signal_detected = false;
}
break;
case POCSAG_STATE_DATA:
/* Collect 32-bit words */
s_word_reg = (s_word_reg << 1) | (bit ? 1 : 0);
s_word_bits++;
if (s_word_bits >= 32) {
process_pocsag_word(s_word_reg);
s_word_reg = 0;
s_word_bits = 0;
}
/* Sync loss check */
if ((s_tick_counter - s_last_sync_tick) > 300) {
s_state = POCSAG_STATE_SYNC;
s_sync_reg = 0;
gPocsag.signal_detected = false;
}
break;
}
}
/* === Sample processing (hot path) === */
bool POCSAG_FeedSample(uint16_t audio_sample)
{
s_tick_counter++;
s_sample_counter++;
/* Accumulate samples for bit decision */
s_bit_accumulator += audio_sample;
s_bit_sample_count++;
if (s_bit_sample_count >= s_samples_per_bit) {
/* Make bit decision from averaged sample.
* Avoid division: compare accumulated value against threshold * count.
*
* For 1200 baud (7 samples): threshold * 7 = 2048 * 7 = 14336
* For 512 baud (16 samples): threshold * 16 = 2048 * 16 = 32768 */
uint32_t thresh;
if (gPocsag.baud_rate == POCSAG_BAUD_512)
thresh = 32768;
else
thresh = 14336;
bool bit = (s_bit_accumulator >= thresh);
s_bit_accumulator = 0;
s_bit_sample_count = 0;
/* Process the bit */
process_bit(bit);
return true;
}
return false;
}
void POCSAG_FeedBit(bool bit)
{
process_bit(bit);
}
/* === Public API === */
void POCSAG_Init(uint32_t baud_rate)
{
memset(&gPocsag, 0, sizeof(gPocsag));
gPocsag.baud_rate = baud_rate;
gPocsag.state = POCSAG_STATE_IDLE;
gPocsag.msg_read_idx = 0;
gPocsag.msg_write_idx = 0;
gPocsag.msg_count = 0;
gPocsag.msg_available = false;
s_sample_counter = 0;
s_samples_per_bit = get_samples_per_bit(baud_rate);
s_bit_accumulator = 0;
s_bit_sample_count = 0;
s_preamble_alt_count = 0;
s_last_bit = 0;
s_sync_reg = 0;
s_word_reg = 0;
s_word_bits = 0;
s_batch_words = 0;
s_msg_address = 0;
s_msg_func = 0;
s_msg_len = 0;
s_msg_is_alpha = false;
s_alpha_bits = 0;
s_alpha_bit_count = 0;
s_state = POCSAG_STATE_IDLE;
s_tick_counter = 0;
s_last_sync_tick = 0;
}
void POCSAG_SwitchBaud(uint32_t baud_rate)
{
gPocsag.baud_rate = baud_rate;
s_samples_per_bit = get_samples_per_bit(baud_rate);
s_state = POCSAG_STATE_IDLE;
s_preamble_alt_count = 0;
s_sync_reg = 0;
s_word_reg = 0;
s_word_bits = 0;
s_batch_words = 0;
gPocsag.signal_detected = false;
}
uint32_t POCSAG_GetBaud(void)
{
return gPocsag.baud_rate;
}
bool POCSAG_MessageAvailable(void)
{
return gPocsag.msg_count > 0;
}
bool POCSAG_GetMessage(pocsag_msg_t *msg)
{
if (gPocsag.msg_count == 0)
return false;
*msg = gPocsag.messages[gPocsag.msg_read_idx];
gPocsag.msg_read_idx = (gPocsag.msg_read_idx + 1) % POCSAG_MSG_POOL_SIZE;
gPocsag.msg_count--;
if (gPocsag.msg_count == 0)
gPocsag.msg_available = false;
return true;
}
pocsag_state_t POCSAG_GetState(void)
{
return s_state;
}
bool POCSAG_SignalDetected(void)
{
return gPocsag.signal_detected;
}
void POCSAG_Reset(void)
{
uint32_t baud = gPocsag.baud_rate;
POCSAG_Init(baud);
}
void POCSAG_Process(void)
{
gPocsag.state = s_state;
}
/* === Radio configuration === */
void POCSAG_ConfigureRadio(uint32_t frequency_hz)
{
/* Save current radio state */
s_saved_reg_30 = BK4819_ReadRegister(BK4819_REG_30);
s_saved_reg_47 = BK4819_ReadRegister(BK4819_REG_47);
s_saved_reg_33 = BK4819_ReadRegister(BK4819_REG_33);
/* Set frequency */
BK4819_SetFrequency(frequency_hz);
BK4819_PickRXFilterPathBasedOnFrequency(frequency_hz);
/* Configure for narrow FM (POCSAG is typically narrow FM) */
BK4819_SetFilterBandwidth(BK4819_FILTER_BW_NARROW, true);
/* Turn on RX */
BK4819_RX_TurnOn();
/* Set AF output to FM (demodulated audio) for sampling */
BK4819_SetAF(BK4819_AF_FM);
/* Disable squelch for continuous audio monitoring */
BK4819_SetupSquelch(0, 0, 0, 0, 255, 0);
/* Enable RX link, AF DAC, disc mode */
BK4819_WriteRegister(BK4819_REG_30,
BK4819_REG_30_ENABLE_VCO_CALIB |
BK4819_REG_30_ENABLE_RX_LINK |
BK4819_REG_30_ENABLE_AF_DAC |
BK4819_REG_30_ENABLE_DISC_MODE |
BK4819_REG_30_ENABLE_PLL_VCO |
BK4819_REG_30_ENABLE_RX_DSP);
/* Enable green LED */
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, true);
}
void POCSAG_Stop(void)
{
BK4819_WriteRegister(BK4819_REG_30, s_saved_reg_30);
BK4819_WriteRegister(BK4819_REG_47, s_saved_reg_47);
BK4819_WriteRegister(BK4819_REG_33, s_saved_reg_33);
gPocsag.signal_detected = false;
s_state = POCSAG_STATE_IDLE;
}
/* === State description strings === */
const char *POCSAG_StateString(pocsag_state_t state)
{
static const char *const strs[] = { "IDLE", "SEARCH", "SYNC", "DATA" };
if ((unsigned int)state > 3u) return "???";
return strs[(unsigned int)state];
}

150
app/pocsag/pocsag.h Normal file
View File

@@ -0,0 +1,150 @@
/* UA1ZBE Custom Firmware - POCSAG Decoder
*
* Decodes POCSAG paging signals from FM discriminator output.
* Supports 512 and 1200 baud rates with dynamic switching.
*
* POCSAG Protocol:
* - Preamble: 0xAAAAAAAB (at least 576 bits of alternating 1010... ending with 1)
* - Sync word: 0x7CD21538
* - Data words: 32-bit (21 data + 10 BCH + 1 parity)
* - BCH(31,21) code, corrects up to 2 errors per word
*
* Architecture:
* - Samples audio from BK4819 discriminator (REG_69 AF output or FSK FIFO)
* - Zero-crossing detection for bit slicing
* - State machine: IDLE -> PREAMBLE -> SYNC -> DATA
* - Ring buffer for decoded messages
* - Zero malloc — all static allocation
*/
#ifndef APP_POCSAG_POCSAG_H
#define APP_POCSAG_POCSAG_H
#include <stdbool.h>
#include <stdint.h>
/* POCSAG baud rates */
#define POCSAG_BAUD_512 512
#define POCSAG_BAUD_1200 1200
/* Maximum message length in bytes (arbitrary, fits in static buffer) */
#define POCSAG_MSG_MAX_LEN 64
/* Maximum messages in ring buffer */
#define POCSAG_MSG_POOL_SIZE 8
/* Decoded message structure */
typedef struct {
uint32_t address; /* Address (21-bit frame address) */
uint8_t func; /* Function code (2 bits) */
char text[POCSAG_MSG_MAX_LEN]; /* Decoded text (alpha pager) */
uint8_t text_len; /* Length of decoded text */
bool is_alpha; /* true = alpha pager, false = numeric */
uint32_t timestamp; /* System tick when received */
} pocsag_msg_t;
/* Decoder state machine states */
typedef enum {
POCSAG_STATE_IDLE = 0,
POCSAG_STATE_PREAMBLE,
POCSAG_STATE_SYNC,
POCSAG_STATE_DATA
} pocsag_state_t;
/* Decoder context — all static, no malloc */
typedef struct {
/* Configuration */
uint32_t baud_rate; /* Current baud rate: 512 or 1200 */
/* State machine */
pocsag_state_t state;
/* Bit accumulation */
uint32_t shift_reg; /* Shift register for incoming bits */
uint8_t bit_count; /* Number of bits collected in shift_reg */
uint8_t preamble_count; /* Consecutive alternating bits in preamble */
/* Sync detection */
uint8_t sync_word_count; /* Number of sync words found in a row */
/* Word collection */
uint8_t batch_count; /* Words in current batch (0-16) */
uint8_t word_in_batch; /* Current word position in batch */
/* Message assembly */
uint32_t current_address;
uint8_t current_func;
uint8_t msg_data[POCSAG_MSG_MAX_LEN];
uint8_t msg_len;
bool msg_is_alpha;
/* Ring buffer of decoded messages */
pocsag_msg_t messages[POCSAG_MSG_POOL_SIZE];
volatile uint8_t msg_read_idx;
volatile uint8_t msg_write_idx;
volatile uint8_t msg_count;
/* Statistics */
uint32_t total_bits;
uint32_t total_words;
uint32_t corrected_errors;
uint32_t uncorrectable_errors;
/* Flag for new message available */
volatile bool msg_available;
/* Signal active flag */
bool signal_detected;
} pocsag_decoder_t;
/* Global decoder instance */
extern pocsag_decoder_t gPocsag;
/* === API === */
/* Initialize decoder, set baud rate */
void POCSAG_Init(uint32_t baud_rate);
/* Switch baud rate dynamically (512 <-> 1200) */
void POCSAG_SwitchBaud(uint32_t baud_rate);
/* Get current baud rate */
uint32_t POCSAG_GetBaud(void);
/* Feed audio sample (12-bit ADC value from discriminator)
* Call this from the 10ms timeslice or a dedicated sampling timer.
* Returns true if a bit was detected. */
bool POCSAG_FeedSample(uint16_t audio_sample);
/* Feed a single bit (after external bit-slicing) */
void POCSAG_FeedBit(bool bit);
/* Check if a decoded message is available (non-blocking) */
bool POCSAG_MessageAvailable(void);
/* Pop the next decoded message from the ring buffer */
bool POCSAG_GetMessage(pocsag_msg_t *msg);
/* Get decoder state (for display) */
pocsag_state_t POCSAG_GetState(void);
/* Get signal detection status */
bool POCSAG_SignalDetected(void);
/* Reset decoder to idle state */
void POCSAG_Reset(void);
/* Process decoder state machine (call from main loop) */
void POCSAG_Process(void);
/* Configure BK4819 for POCSAG reception on given frequency */
void POCSAG_ConfigureRadio(uint32_t frequency_hz);
/* Stop POCSAG reception, restore normal RX */
void POCSAG_Stop(void);
/* Get state description string */
const char *POCSAG_StateString(pocsag_state_t state);
#endif /* APP_POCSAG_POCSAG_H */

528
app/scanner.c Normal file
View File

@@ -0,0 +1,528 @@
/* Copyright 2023 Dual Tachyon
* https://github.com/DualTachyon
*
* 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.
*/
#include "app/app.h"
#include "app/dtmf.h"
#include "app/generic.h"
#include "app/menu.h"
#include "app/scanner.h"
#include "audio.h"
#include "driver/bk4819.h"
#include "frequencies.h"
#include "misc.h"
#include "radio.h"
#include "settings.h"
#include "ui/inputbox.h"
#include "ui/ui.h"
DCS_CodeType_t gScanCssResultType;
uint8_t gScanCssResultCode;
bool gScanSingleFrequency; // scan CTCSS/DCS codes for current frequency
SCAN_SaveState_t gScannerSaveState;
uint8_t gScanChannel;
uint32_t gScanFrequency;
SCAN_CssState_t gScanCssState;
uint8_t gScanProgressIndicator;
bool gScanUseCssResult;
STEP_Setting_t stepSetting;
uint8_t scanHitCount;
static void SCANNER_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
{
if (!bKeyHeld && bKeyPressed)
{
if (gScannerSaveState == SCAN_SAVE_CHAN_SEL) {
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
INPUTBOX_Append(Key);
gRequestDisplayScreen = DISPLAY_SCANNER;
if (gInputBoxIndex < 3) {
#ifdef ENABLE_VOICE
gAnotherVoiceID = (VOICE_ID_t)Key;
#endif
return;
}
gInputBoxIndex = 0;
uint16_t chan = ((gInputBox[0] * 100) + (gInputBox[1] * 10) + gInputBox[2]) - 1;
if (IS_MR_CHANNEL(chan)) {
#ifdef ENABLE_VOICE
gAnotherVoiceID = (VOICE_ID_t)Key;
#endif
gShowChPrefix = RADIO_CheckValidChannel(chan, false, 0);
gScanChannel = (uint8_t)chan;
return;
}
}
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
}
}
static void SCANNER_Key_EXIT(bool bKeyPressed, bool bKeyHeld)
{
if (!bKeyHeld && bKeyPressed) { // short pressed
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
switch (gScannerSaveState) {
case SCAN_SAVE_NO_PROMPT:
SCANNER_Stop();
gRequestDisplayScreen = DISPLAY_MAIN;
break;
case SCAN_SAVE_CHAN_SEL:
if (gInputBoxIndex > 0) {
gInputBox[--gInputBoxIndex] = 10;
gRequestDisplayScreen = DISPLAY_SCANNER;
break;
}
// Fallthrough
case SCAN_SAVE_CHANNEL:
gScannerSaveState = SCAN_SAVE_NO_PROMPT;
#ifdef ENABLE_VOICE
gAnotherVoiceID = VOICE_ID_CANCEL;
#endif
gRequestDisplayScreen = DISPLAY_SCANNER;
break;
}
}
}
static void SCANNER_Key_MENU(bool bKeyPressed, bool bKeyHeld)
{
if (bKeyHeld || !bKeyPressed) // ignore long press or release button events
return;
if (gScanCssState == SCAN_CSS_STATE_OFF && !gScanSingleFrequency) {
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
return;
}
if (gScanCssState == SCAN_CSS_STATE_SCANNING && gScanSingleFrequency) {
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
return;
}
if (gScanCssState == SCAN_CSS_STATE_FAILED) {
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
return;
}
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
switch (gScannerSaveState) {
case SCAN_SAVE_NO_PROMPT:
if (!gScanSingleFrequency)
{
uint32_t freq250 = FREQUENCY_RoundToStep(gScanFrequency, 250);
uint32_t freq625 = FREQUENCY_RoundToStep(gScanFrequency, 625);
uint32_t diff250 = gScanFrequency > freq250 ? gScanFrequency - freq250 : freq250 - gScanFrequency;
uint32_t diff625 = gScanFrequency > freq625 ? gScanFrequency - freq625 : freq625 - gScanFrequency;
if(diff250 > diff625) {
stepSetting = STEP_6_25kHz;
gScanFrequency = freq625;
}
else {
stepSetting = STEP_2_5kHz;
gScanFrequency = freq250;
}
}
if (IS_MR_CHANNEL(gTxVfo->CHANNEL_SAVE)) {
gScannerSaveState = SCAN_SAVE_CHAN_SEL;
gScanChannel = gTxVfo->CHANNEL_SAVE;
gShowChPrefix = RADIO_CheckValidChannel(gTxVfo->CHANNEL_SAVE, false, 0);
}
else {
gScannerSaveState = SCAN_SAVE_CHANNEL;
}
gScanCssState = SCAN_CSS_STATE_FOUND;
#ifdef ENABLE_VOICE
gAnotherVoiceID = VOICE_ID_MEMORY_CHANNEL;
#endif
gRequestDisplayScreen = DISPLAY_SCANNER;
gUpdateStatus = true;
break;
case SCAN_SAVE_CHAN_SEL:
if (gInputBoxIndex == 0) {
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
gRequestDisplayScreen = DISPLAY_SCANNER;
gScannerSaveState = SCAN_SAVE_CHANNEL;
}
break;
case SCAN_SAVE_CHANNEL:
if (!gScanSingleFrequency) {
RADIO_InitInfo(gTxVfo, gTxVfo->CHANNEL_SAVE, gScanFrequency);
if (gScanUseCssResult) {
gTxVfo->freq_config_RX.CodeType = gScanCssResultType;
gTxVfo->freq_config_RX.Code = gScanCssResultCode;
}
gTxVfo->freq_config_TX = gTxVfo->freq_config_RX;
gTxVfo->STEP_SETTING = stepSetting;
}
else {
RADIO_ConfigureChannel(0, VFO_CONFIGURE_RELOAD);
RADIO_ConfigureChannel(1, VFO_CONFIGURE_RELOAD);
gTxVfo->freq_config_RX.CodeType = gScanCssResultType;
gTxVfo->freq_config_RX.Code = gScanCssResultCode;
gTxVfo->freq_config_TX.CodeType = gScanCssResultType;
gTxVfo->freq_config_TX.Code = gScanCssResultCode;
}
uint8_t chan;
if (IS_MR_CHANNEL(gTxVfo->CHANNEL_SAVE)) {
chan = gScanChannel;
gEeprom.MrChannel[gEeprom.TX_VFO] = chan;
}
else {
chan = gTxVfo->Band + FREQ_CHANNEL_FIRST;
gEeprom.FreqChannel[gEeprom.TX_VFO] = chan;
}
gTxVfo->CHANNEL_SAVE = chan;
gEeprom.ScreenChannel[gEeprom.TX_VFO] = chan;
#ifdef ENABLE_VOICE
gAnotherVoiceID = VOICE_ID_CONFIRM;
#endif
gRequestDisplayScreen = DISPLAY_SCANNER;
gRequestSaveChannel = 2;
gScannerSaveState = SCAN_SAVE_NO_PROMPT;
break;
default:
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
break;
}
}
static void SCANNER_Key_STAR(bool bKeyPressed, bool bKeyHeld)
{
if (!bKeyHeld && bKeyPressed) {
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
SCANNER_Start(gScanSingleFrequency);
}
return;
}
static void SCANNER_Key_UP_DOWN(bool bKeyPressed, bool pKeyHeld, int8_t Direction)
{
if (pKeyHeld) {
if (!bKeyPressed)
return;
}
else {
if (!bKeyPressed)
return;
gInputBoxIndex = 0;
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
}
if (gScannerSaveState == SCAN_SAVE_CHAN_SEL) {
gScanChannel = NUMBER_AddWithWraparound(gScanChannel, Direction, 0, MR_CHANNEL_LAST);
gShowChPrefix = RADIO_CheckValidChannel(gScanChannel, false, 0);
gRequestDisplayScreen = DISPLAY_SCANNER;
}
else
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
}
void SCANNER_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
{
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:
SCANNER_Key_DIGITS(Key, bKeyPressed, bKeyHeld);
break;
case KEY_MENU:
SCANNER_Key_MENU(bKeyPressed, bKeyHeld);
break;
case KEY_UP:
SCANNER_Key_UP_DOWN(bKeyPressed, bKeyHeld, 1);
break;
case KEY_DOWN:
SCANNER_Key_UP_DOWN(bKeyPressed, bKeyHeld, -1);
break;
case KEY_EXIT:
SCANNER_Key_EXIT(bKeyPressed, bKeyHeld);
break;
case KEY_STAR:
SCANNER_Key_STAR(bKeyPressed, bKeyHeld);
break;
case KEY_PTT:
GENERIC_Key_PTT(bKeyPressed);
break;
default:
if (!bKeyHeld && bKeyPressed)
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
break;
}
}
void SCANNER_Start(bool singleFreq)
{
gScanSingleFrequency = singleFreq;
gMonitor = false;
#ifdef ENABLE_VOICE
gAnotherVoiceID = VOICE_ID_SCANNING_BEGIN;
#endif
BK4819_StopScan();
RADIO_SelectVfos();
#ifdef ENABLE_NOAA
if (IS_NOAA_CHANNEL(gRxVfo->CHANNEL_SAVE))
gRxVfo->CHANNEL_SAVE = FREQ_CHANNEL_FIRST + BAND6_400MHz;
#endif
uint8_t backupStep = gRxVfo->STEP_SETTING;
uint16_t backupFrequency = gRxVfo->StepFrequency;
RADIO_InitInfo(gRxVfo, gRxVfo->CHANNEL_SAVE, gRxVfo->pRX->Frequency);
gRxVfo->STEP_SETTING = backupStep;
gRxVfo->StepFrequency = backupFrequency;
RADIO_SetupRegisters(true);
#ifdef ENABLE_NOAA
gIsNoaaMode = false;
#endif
if (gScanSingleFrequency) {
gScanCssState = SCAN_CSS_STATE_SCANNING;
gScanFrequency = gRxVfo->pRX->Frequency;
stepSetting = gRxVfo->STEP_SETTING;
BK4819_PickRXFilterPathBasedOnFrequency(gScanFrequency);
BK4819_SetScanFrequency(gScanFrequency);
gUpdateStatus = true;
}
else {
gScanCssState = SCAN_CSS_STATE_OFF;
gScanFrequency = 0xFFFFFFFF;
BK4819_PickRXFilterPathBasedOnFrequency(gScanFrequency);
BK4819_EnableFrequencyScan();
gUpdateStatus = true;
}
#ifdef ENABLE_DTMF_CALLING
DTMF_clear_RX();
#endif
gScanDelay_10ms = scan_delay_10ms;
gScanCssResultCode = 0xFF;
gScanCssResultType = 0xFF;
scanHitCount = 0;
gScanUseCssResult = false;
g_CxCSS_TAIL_Found = false;
g_CDCSS_Lost = false;
gCDCSSCodeType = 0;
g_CTCSS_Lost = false;
#ifdef ENABLE_VOX
g_VOX_Lost = false;
#endif
g_SquelchLost = false;
gScannerSaveState = SCAN_SAVE_NO_PROMPT;
gScanProgressIndicator = 0;
}
void SCANNER_Stop(void)
{
if(SCANNER_IsScanning()) {
gEeprom.CROSS_BAND_RX_TX = gBackup_CROSS_BAND_RX_TX;
gVfoConfigureMode = VFO_CONFIGURE_RELOAD;
gFlagResetVfos = true;
gUpdateStatus = true;
gCssBackgroundScan = false;
gScanUseCssResult = false;
#ifdef ENABLE_VOICE
gAnotherVoiceID = VOICE_ID_CANCEL;
#endif
BK4819_StopScan();
}
}
void SCANNER_TimeSlice10ms(void)
{
if (!SCANNER_IsScanning())
return;
if (gScanDelay_10ms > 0) {
gScanDelay_10ms--;
return;
}
if (gScannerSaveState != SCAN_SAVE_NO_PROMPT) {
return;
}
switch (gScanCssState) {
case SCAN_CSS_STATE_OFF: {
// must be RF frequency scanning if we're here ?
uint32_t result;
if (!BK4819_GetFrequencyScanResult(&result))
break;
int32_t delta = result - gScanFrequency;
gScanFrequency = result;
if (delta < 0)
delta = -delta;
if (delta < 100)
scanHitCount++;
else
scanHitCount = 0;
BK4819_DisableFrequencyScan();
if (scanHitCount < 3) {
BK4819_EnableFrequencyScan();
}
else {
BK4819_SetScanFrequency(gScanFrequency);
gScanCssResultCode = 0xFF;
gScanCssResultType = 0xFF;
scanHitCount = 0;
gScanUseCssResult = false;
gScanProgressIndicator = 0;
gScanCssState = SCAN_CSS_STATE_SCANNING;
if(!gCssBackgroundScan)
GUI_SelectNextDisplay(DISPLAY_SCANNER);
gUpdateStatus = true;
}
gScanDelay_10ms = scan_delay_10ms;
//gScanDelay_10ms = 1; // 10ms
break;
}
case SCAN_CSS_STATE_SCANNING: {
uint32_t cdcssFreq;
uint16_t ctcssFreq;
BK4819_CssScanResult_t scanResult = BK4819_GetCxCSSScanResult(&cdcssFreq, &ctcssFreq);
if (scanResult == BK4819_CSS_RESULT_NOT_FOUND)
break;
BK4819_Disable();
if (scanResult == BK4819_CSS_RESULT_CDCSS) {
const uint8_t Code = DCS_GetCdcssCode(cdcssFreq);
if (Code != 0xFF)
{
gScanCssResultCode = Code;
gScanCssResultType = CODE_TYPE_DIGITAL;
gScanCssState = SCAN_CSS_STATE_FOUND;
gScanUseCssResult = true;
gUpdateStatus = true;
}
}
else if (scanResult == BK4819_CSS_RESULT_CTCSS) {
const uint8_t Code = DCS_GetCtcssCode(ctcssFreq);
if (Code != 0xFF) {
if (Code == gScanCssResultCode && gScanCssResultType == CODE_TYPE_CONTINUOUS_TONE) {
if (++scanHitCount >= 2) {
gScanCssState = SCAN_CSS_STATE_FOUND;
gScanUseCssResult = true;
gUpdateStatus = true;
}
}
else
scanHitCount = 0;
gScanCssResultType = CODE_TYPE_CONTINUOUS_TONE;
gScanCssResultCode = Code;
}
}
if (gScanCssState < SCAN_CSS_STATE_FOUND) { // scanning or off
BK4819_SetScanFrequency(gScanFrequency);
gScanDelay_10ms = scan_delay_10ms;
break;
}
if(gCssBackgroundScan) {
gCssBackgroundScan = false;
if(gScanUseCssResult)
MENU_CssScanFound();
}
else
GUI_SelectNextDisplay(DISPLAY_SCANNER);
break;
}
default:
gCssBackgroundScan = false;
break;
}
}
void SCANNER_TimeSlice500ms(void)
{
if (SCANNER_IsScanning() && gScannerSaveState == SCAN_SAVE_NO_PROMPT && gScanCssState < SCAN_CSS_STATE_FOUND) {
gScanProgressIndicator++;
#ifndef ENABLE_NO_CODE_SCAN_TIMEOUT
if (gScanProgressIndicator > 32) {
if (gScanCssState == SCAN_CSS_STATE_SCANNING && !gScanSingleFrequency)
gScanCssState = SCAN_CSS_STATE_FOUND;
else
gScanCssState = SCAN_CSS_STATE_FAILED;
gUpdateStatus = true;
}
#endif
gUpdateDisplay = true;
}
else if(gCssBackgroundScan) {
gUpdateDisplay = true;
}
}
bool SCANNER_IsScanning(void)
{
return gCssBackgroundScan || (gScreenToDisplay == DISPLAY_SCANNER);
}

57
app/scanner.h Normal file
View File

@@ -0,0 +1,57 @@
/* Copyright 2023 Dual Tachyon
* https://github.com/DualTachyon
*
* 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_SCANNER_H
#define APP_SCANNER_H
#include "dcs.h"
#include "driver/keyboard.h"
typedef enum
{
SCAN_CSS_STATE_OFF,
SCAN_CSS_STATE_SCANNING,
SCAN_CSS_STATE_FOUND,
SCAN_CSS_STATE_FAILED
} SCAN_CssState_t;
typedef enum
{
SCAN_SAVE_NO_PROMPT, // saving process not initiated
SCAN_SAVE_CHAN_SEL, // "SAVE: ", channel select prompt, actives only in channel mode
SCAN_SAVE_CHANNEL, // "SAVE?" prompt, waits for confirmation to save settings to channel, or current VFO
} SCAN_SaveState_t;
extern DCS_CodeType_t gScanCssResultType;
extern uint8_t gScanCssResultCode;
extern bool gScanSingleFrequency;
extern SCAN_SaveState_t gScannerSaveState;
extern uint8_t gScanChannel;
extern uint32_t gScanFrequency;
extern SCAN_CssState_t gScanCssState;
extern uint8_t gScanProgressIndicator;
extern bool gScanUseCssResult;
void SCANNER_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld);
void SCANNER_Start(bool singleFreq);
void SCANNER_Stop(void);
void SCANNER_TimeSlice10ms(void);
void SCANNER_TimeSlice500ms(void);
bool SCANNER_IsScanning(void);
#endif

1310
app/spectrum.c Normal file

File diff suppressed because it is too large Load Diff

158
app/spectrum.h Normal file
View File

@@ -0,0 +1,158 @@
/* Copyright 2023 fagci
* https://github.com/fagci
*
* 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 SPECTRUM_H
#define SPECTRUM_H
#include "../bitmaps.h"
#include "../board.h"
#include "../bsp/dp32g030/gpio.h"
#include "../driver/bk4819-regs.h"
#include "../driver/bk4819.h"
#include "../driver/gpio.h"
#include "../driver/keyboard.h"
#include "../driver/st7565.h"
#include "../driver/system.h"
#include "../driver/systick.h"
#include "../external/printf/printf.h"
#include "../font.h"
#include "../helper/battery.h"
#include "../misc.h"
#include "../radio.h"
#include "../settings.h"
#include "../ui/helper.h"
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
static const uint8_t DrawingEndY = 40;
static const uint8_t U8RssiMap[] = {
121, 115, 109, 103, 97, 91, 85, 79, 73, 63,
};
static const uint16_t scanStepValues[] = {
1, 10, 50, 100, 250, 500, 625, 833,
1000, 1250, 1500, 2000, 2500, 5000, 10000,
};
static const uint16_t scanStepBWRegValues[] = {
// RX RXw TX BW
// 0b0 000 000 001 01 1000
// 1
0b0000000001011000, // 6.25
// 10
0b0000000001011000, // 6.25
// 50
0b0000000001011000, // 6.25
// 100
0b0000000001011000, // 6.25
// 250
0b0000000001011000, // 6.25
// 500
0b0010010001011000, // 6.25
// 625
0b0100100001011000, // 6.25
// 833
0b0110110001001000, // 6.25
// 1000
0b0110110001001000, // 6.25
// 1250
0b0111111100001000, // 6.25
// 2500
0b0011011000101000, // 25
// 10000
0b0011011000101000, // 25
};
static const uint16_t listenBWRegValues[] = {
0b0011011000101000, // 25
0b0111111100001000, // 12.5
0b0100100001011000, // 6.25
};
typedef enum State {
SPECTRUM,
FREQ_INPUT,
STILL,
} State;
typedef enum StepsCount {
STEPS_128,
STEPS_64,
STEPS_32,
STEPS_16,
} StepsCount;
typedef enum ScanStep {
S_STEP_0_01kHz,
S_STEP_0_1kHz,
S_STEP_0_5kHz,
S_STEP_1_0kHz,
S_STEP_2_5kHz,
S_STEP_5_0kHz,
S_STEP_6_25kHz,
S_STEP_8_33kHz,
S_STEP_10_0kHz,
S_STEP_12_5kHz,
S_STEP_15_0kHz,
S_STEP_20_0kHz,
S_STEP_25_0kHz,
S_STEP_50_0kHz,
S_STEP_100_0kHz,
} ScanStep;
typedef struct SpectrumSettings {
uint32_t frequencyChangeStep;
StepsCount stepsCount;
ScanStep scanStepIndex;
uint16_t scanDelay;
uint16_t rssiTriggerLevel;
BK4819_FilterBandwidth_t bw;
BK4819_FilterBandwidth_t listenBw;
int dbMin;
int dbMax;
ModulationMode_t modulationType;
bool backlightState;
} SpectrumSettings;
typedef struct KeyboardState {
KEY_Code_t current;
KEY_Code_t prev;
uint8_t counter;
} KeyboardState;
typedef struct ScanInfo {
uint16_t rssi, rssiMin, rssiMax;
uint16_t i, iPeak;
uint32_t f, fPeak;
uint16_t scanStep;
uint16_t measurementsCount;
} ScanInfo;
typedef struct PeakInfo {
uint16_t t;
uint16_t rssi;
uint32_t f;
uint16_t i;
} PeakInfo;
void APP_RunSpectrum(void);
#endif /* ifndef SPECTRUM_H */
// vim: ft=c

624
app/uart.c Normal file
View File

@@ -0,0 +1,624 @@
/* Copyright 2023 Dual Tachyon
* https://github.com/DualTachyon
*
* 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.
*/
#include <string.h>
#if !defined(ENABLE_OVERLAY)
#include "ARMCM0.h"
#endif
#ifdef ENABLE_FMRADIO
#include "app/fm.h"
#endif
#include "app/uart.h"
#include "board.h"
#include "bsp/dp32g030/dma.h"
#include "bsp/dp32g030/gpio.h"
#include "driver/aes.h"
#include "driver/backlight.h"
#include "driver/bk4819.h"
#include "driver/crc.h"
#include "driver/eeprom.h"
#include "driver/gpio.h"
#include "driver/uart.h"
#include "functions.h"
#include "misc.h"
#include "settings.h"
#include "version.h"
#if defined(ENABLE_OVERLAY)
#include "sram-overlay.h"
#endif
#define DMA_INDEX(x, y) (((x) + (y)) % sizeof(UART_DMA_Buffer))
typedef struct {
uint16_t ID;
uint16_t Size;
} Header_t;
typedef struct {
uint8_t Padding[2];
uint16_t ID;
} Footer_t;
typedef struct {
Header_t Header;
uint32_t Timestamp;
} CMD_0514_t;
typedef struct {
Header_t Header;
struct {
char Version[16];
bool bHasCustomAesKey;
bool bIsInLockScreen;
uint8_t Padding[2];
uint32_t Challenge[4];
} Data;
} REPLY_0514_t;
typedef struct {
Header_t Header;
uint16_t Offset;
uint8_t Size;
uint8_t Padding;
uint32_t Timestamp;
} CMD_051B_t;
typedef struct {
Header_t Header;
struct {
uint16_t Offset;
uint8_t Size;
uint8_t Padding;
uint8_t Data[128];
} Data;
} REPLY_051B_t;
typedef struct {
Header_t Header;
uint16_t Offset;
uint8_t Size;
bool bAllowPassword;
uint32_t Timestamp;
uint8_t Data[0];
} CMD_051D_t;
typedef struct {
Header_t Header;
struct {
uint16_t Offset;
} Data;
} REPLY_051D_t;
typedef struct {
Header_t Header;
struct {
uint16_t RSSI;
uint8_t ExNoiseIndicator;
uint8_t GlitchIndicator;
} Data;
} REPLY_0527_t;
typedef struct {
Header_t Header;
struct {
uint16_t Voltage;
uint16_t Current;
} Data;
} REPLY_0529_t;
typedef struct {
Header_t Header;
uint32_t Response[4];
} CMD_052D_t;
typedef struct {
Header_t Header;
struct {
bool bIsLocked;
uint8_t Padding[3];
} Data;
} REPLY_052D_t;
typedef struct {
Header_t Header;
uint32_t Timestamp;
} CMD_052F_t;
static const uint8_t Obfuscation[16] =
{
0x16, 0x6C, 0x14, 0xE6, 0x2E, 0x91, 0x0D, 0x40, 0x21, 0x35, 0xD5, 0x40, 0x13, 0x03, 0xE9, 0x80
};
static union
{
uint8_t Buffer[256];
struct
{
Header_t Header;
uint8_t Data[252];
};
} UART_Command;
static uint32_t Timestamp;
static uint16_t gUART_WriteIndex;
static bool bIsEncrypted = true;
static void SendReply(void *pReply, uint16_t Size)
{
Header_t Header;
Footer_t Footer;
if (bIsEncrypted)
{
uint8_t *pBytes = (uint8_t *)pReply;
unsigned int i;
for (i = 0; i < Size; i++)
pBytes[i] ^= Obfuscation[i % 16];
}
Header.ID = 0xCDAB;
Header.Size = Size;
UART_Send(&Header, sizeof(Header));
UART_Send(pReply, Size);
if (bIsEncrypted)
{
Footer.Padding[0] = Obfuscation[(Size + 0) % 16] ^ 0xFF;
Footer.Padding[1] = Obfuscation[(Size + 1) % 16] ^ 0xFF;
}
else
{
Footer.Padding[0] = 0xFF;
Footer.Padding[1] = 0xFF;
}
Footer.ID = 0xBADC;
UART_Send(&Footer, sizeof(Footer));
}
static void SendVersion(void)
{
REPLY_0514_t Reply;
Reply.Header.ID = 0x0515;
Reply.Header.Size = sizeof(Reply.Data);
strcpy(Reply.Data.Version, Version);
Reply.Data.bHasCustomAesKey = bHasCustomAesKey;
Reply.Data.bIsInLockScreen = bIsInLockScreen;
Reply.Data.Challenge[0] = gChallenge[0];
Reply.Data.Challenge[1] = gChallenge[1];
Reply.Data.Challenge[2] = gChallenge[2];
Reply.Data.Challenge[3] = gChallenge[3];
SendReply(&Reply, sizeof(Reply));
}
static bool IsBadChallenge(const uint32_t *pKey, const uint32_t *pIn, const uint32_t *pResponse)
{
unsigned int i;
uint32_t IV[4];
IV[0] = 0;
IV[1] = 0;
IV[2] = 0;
IV[3] = 0;
AES_Encrypt(pKey, IV, pIn, IV, true);
for (i = 0; i < 4; i++)
if (IV[i] != pResponse[i])
return true;
return false;
}
// session init, sends back version info and state
// timestamp is a session id really
static void CMD_0514(const uint8_t *pBuffer)
{
const CMD_0514_t *pCmd = (const CMD_0514_t *)pBuffer;
Timestamp = pCmd->Timestamp;
#ifdef ENABLE_FMRADIO
gFmRadioCountdown_500ms = fm_radio_countdown_500ms;
#endif
gSerialConfigCountDown_500ms = 12; // 6 sec
// turn the LCD backlight off
BACKLIGHT_TurnOff();
SendVersion();
}
// read eeprom
static void CMD_051B(const uint8_t *pBuffer)
{
const CMD_051B_t *pCmd = (const CMD_051B_t *)pBuffer;
REPLY_051B_t Reply;
bool bLocked = false;
if (pCmd->Timestamp != Timestamp)
return;
gSerialConfigCountDown_500ms = 12; // 6 sec
#ifdef ENABLE_FMRADIO
gFmRadioCountdown_500ms = fm_radio_countdown_500ms;
#endif
memset(&Reply, 0, sizeof(Reply));
Reply.Header.ID = 0x051C;
Reply.Header.Size = pCmd->Size + 4;
Reply.Data.Offset = pCmd->Offset;
Reply.Data.Size = pCmd->Size;
if (bHasCustomAesKey)
bLocked = gIsLocked;
if (!bLocked)
EEPROM_ReadBuffer(pCmd->Offset, Reply.Data.Data, pCmd->Size);
SendReply(&Reply, pCmd->Size + 8);
}
// write eeprom
static void CMD_051D(const uint8_t *pBuffer)
{
const CMD_051D_t *pCmd = (const CMD_051D_t *)pBuffer;
REPLY_051D_t Reply;
bool bReloadEeprom;
bool bIsLocked;
if (pCmd->Timestamp != Timestamp)
return;
gSerialConfigCountDown_500ms = 12; // 6 sec
bReloadEeprom = false;
#ifdef ENABLE_FMRADIO
gFmRadioCountdown_500ms = fm_radio_countdown_500ms;
#endif
Reply.Header.ID = 0x051E;
Reply.Header.Size = sizeof(Reply.Data);
Reply.Data.Offset = pCmd->Offset;
bIsLocked = bHasCustomAesKey ? gIsLocked : bHasCustomAesKey;
if (!bIsLocked)
{
unsigned int i;
for (i = 0; i < (pCmd->Size / 8); i++)
{
const uint16_t Offset = pCmd->Offset + (i * 8U);
if (Offset >= 0x0F30 && Offset < 0x0F40)
if (!gIsLocked)
bReloadEeprom = true;
if ((Offset < 0x0E98 || Offset >= 0x0EA0) || !bIsInLockScreen || pCmd->bAllowPassword)
EEPROM_WriteBuffer(Offset, &pCmd->Data[i * 8U]);
}
if (bReloadEeprom)
SETTINGS_InitEEPROM();
}
SendReply(&Reply, sizeof(Reply));
}
// read RSSI
static void CMD_0527(void)
{
REPLY_0527_t Reply;
Reply.Header.ID = 0x0528;
Reply.Header.Size = sizeof(Reply.Data);
Reply.Data.RSSI = BK4819_ReadRegister(BK4819_REG_67) & 0x01FF;
Reply.Data.ExNoiseIndicator = BK4819_ReadRegister(BK4819_REG_65) & 0x007F;
Reply.Data.GlitchIndicator = BK4819_ReadRegister(BK4819_REG_63);
SendReply(&Reply, sizeof(Reply));
}
// read ADC
static void CMD_0529(void)
{
REPLY_0529_t Reply;
Reply.Header.ID = 0x52A;
Reply.Header.Size = sizeof(Reply.Data);
// Original doesn't actually send current!
BOARD_ADC_GetBatteryInfo(&Reply.Data.Voltage, &Reply.Data.Current);
SendReply(&Reply, sizeof(Reply));
}
static void CMD_052D(const uint8_t *pBuffer)
{
const CMD_052D_t *pCmd = (const CMD_052D_t *)pBuffer;
REPLY_052D_t Reply;
bool bIsLocked;
#ifdef ENABLE_FMRADIO
gFmRadioCountdown_500ms = fm_radio_countdown_500ms;
#endif
Reply.Header.ID = 0x052E;
Reply.Header.Size = sizeof(Reply.Data);
bIsLocked = bHasCustomAesKey;
if (!bIsLocked)
bIsLocked = IsBadChallenge(gCustomAesKey, gChallenge, pCmd->Response);
if (!bIsLocked)
{
bIsLocked = IsBadChallenge(gDefaultAesKey, gChallenge, pCmd->Response);
if (bIsLocked)
gTryCount++;
}
if (gTryCount < 3)
{
if (!bIsLocked)
gTryCount = 0;
}
else
{
gTryCount = 3;
bIsLocked = true;
}
gIsLocked = bIsLocked;
Reply.Data.bIsLocked = bIsLocked;
SendReply(&Reply, sizeof(Reply));
}
// session init, sends back version info and state
// timestamp is a session id really
// this command also disables dual watch, crossband,
// DTMF side tones, freq reverse, PTT ID, DTMF decoding, frequency offset
// exits power save, sets main VFO to upper,
static void CMD_052F(const uint8_t *pBuffer)
{
const CMD_052F_t *pCmd = (const CMD_052F_t *)pBuffer;
gEeprom.DUAL_WATCH = DUAL_WATCH_OFF;
gEeprom.CROSS_BAND_RX_TX = CROSS_BAND_OFF;
gEeprom.RX_VFO = 0;
gEeprom.DTMF_SIDE_TONE = false;
gEeprom.VfoInfo[0].FrequencyReverse = false;
gEeprom.VfoInfo[0].pRX = &gEeprom.VfoInfo[0].freq_config_RX;
gEeprom.VfoInfo[0].pTX = &gEeprom.VfoInfo[0].freq_config_TX;
gEeprom.VfoInfo[0].TX_OFFSET_FREQUENCY_DIRECTION = TX_OFFSET_FREQUENCY_DIRECTION_OFF;
gEeprom.VfoInfo[0].DTMF_PTT_ID_TX_MODE = PTT_ID_OFF;
#ifdef ENABLE_DTMF_CALLING
gEeprom.VfoInfo[0].DTMF_DECODING_ENABLE = false;
#endif
#ifdef ENABLE_NOAA
gIsNoaaMode = false;
#endif
if (gCurrentFunction == FUNCTION_POWER_SAVE)
FUNCTION_Select(FUNCTION_FOREGROUND);
gSerialConfigCountDown_500ms = 12; // 6 sec
Timestamp = pCmd->Timestamp;
// turn the LCD backlight off
BACKLIGHT_TurnOff();
SendVersion();
}
#ifdef ENABLE_UART_RW_BK_REGS
static void CMD_0601_ReadBK4819Reg(const uint8_t *pBuffer)
{
typedef struct __attribute__((__packed__)) {
Header_t header;
uint8_t reg;
} CMD_0601_t;
CMD_0601_t *cmd = (CMD_0601_t*) pBuffer;
struct __attribute__((__packed__)) {
Header_t header;
struct __attribute__((__packed__)) {
uint8_t reg;
uint16_t value;
} data;
} reply;
reply.header.ID = 0x0601;
reply.header.Size = sizeof(reply.data);
reply.data.reg = cmd->reg;
reply.data.value = BK4819_ReadRegister(cmd->reg);
SendReply(&reply, sizeof(reply));
}
static void CMD_0602_WriteBK4819Reg(const uint8_t *pBuffer)
{
typedef struct __attribute__((__packed__)) {
Header_t header;
uint8_t reg;
uint16_t value;
} CMD_0602_t;
CMD_0602_t *cmd = (CMD_0602_t*) pBuffer;
BK4819_WriteRegister(cmd->reg, cmd->value);
}
#endif
bool UART_IsCommandAvailable(void)
{
uint16_t Index;
uint16_t TailIndex;
uint16_t Size;
uint16_t CRC;
uint16_t CommandLength;
uint16_t DmaLength = DMA_CH0->ST & 0xFFFU;
while (1)
{
if (gUART_WriteIndex == DmaLength)
return false;
while (gUART_WriteIndex != DmaLength && UART_DMA_Buffer[gUART_WriteIndex] != 0xABU)
gUART_WriteIndex = DMA_INDEX(gUART_WriteIndex, 1);
if (gUART_WriteIndex == DmaLength)
return false;
if (gUART_WriteIndex < DmaLength)
CommandLength = DmaLength - gUART_WriteIndex;
else
CommandLength = (DmaLength + sizeof(UART_DMA_Buffer)) - gUART_WriteIndex;
if (CommandLength < 8)
return 0;
if (UART_DMA_Buffer[DMA_INDEX(gUART_WriteIndex, 1)] == 0xCD)
break;
gUART_WriteIndex = DMA_INDEX(gUART_WriteIndex, 1);
}
Index = DMA_INDEX(gUART_WriteIndex, 2);
Size = (UART_DMA_Buffer[DMA_INDEX(Index, 1)] << 8) | UART_DMA_Buffer[Index];
if ((Size + 8u) > sizeof(UART_DMA_Buffer))
{
gUART_WriteIndex = DmaLength;
return false;
}
if (CommandLength < (Size + 8))
return false;
Index = DMA_INDEX(Index, 2);
TailIndex = DMA_INDEX(Index, Size + 2);
if (UART_DMA_Buffer[TailIndex] != 0xDC || UART_DMA_Buffer[DMA_INDEX(TailIndex, 1)] != 0xBA)
{
gUART_WriteIndex = DmaLength;
return false;
}
if (TailIndex < Index)
{
const uint16_t ChunkSize = sizeof(UART_DMA_Buffer) - Index;
memcpy(UART_Command.Buffer, UART_DMA_Buffer + Index, ChunkSize);
memcpy(UART_Command.Buffer + ChunkSize, UART_DMA_Buffer, TailIndex);
}
else
memcpy(UART_Command.Buffer, UART_DMA_Buffer + Index, TailIndex - Index);
TailIndex = DMA_INDEX(TailIndex, 2);
if (TailIndex < gUART_WriteIndex)
{
memset(UART_DMA_Buffer + gUART_WriteIndex, 0, sizeof(UART_DMA_Buffer) - gUART_WriteIndex);
memset(UART_DMA_Buffer, 0, TailIndex);
}
else
memset(UART_DMA_Buffer + gUART_WriteIndex, 0, TailIndex - gUART_WriteIndex);
gUART_WriteIndex = TailIndex;
if (UART_Command.Header.ID == 0x0514)
bIsEncrypted = false;
if (UART_Command.Header.ID == 0x6902)
bIsEncrypted = true;
if (bIsEncrypted)
{
unsigned int i;
for (i = 0; i < (Size + 2u); i++)
UART_Command.Buffer[i] ^= Obfuscation[i % 16];
}
CRC = UART_Command.Buffer[Size] | (UART_Command.Buffer[Size + 1] << 8);
return (CRC_Calculate(UART_Command.Buffer, Size) != CRC) ? false : true;
}
void UART_HandleCommand(void)
{
switch (UART_Command.Header.ID)
{
case 0x0514:
CMD_0514(UART_Command.Buffer);
break;
case 0x051B:
CMD_051B(UART_Command.Buffer);
break;
case 0x051D:
CMD_051D(UART_Command.Buffer);
break;
case 0x051F: // Not implementing non-authentic command
break;
case 0x0521: // Not implementing non-authentic command
break;
case 0x0527:
CMD_0527();
break;
case 0x0529:
CMD_0529();
break;
case 0x052D:
CMD_052D(UART_Command.Buffer);
break;
case 0x052F:
CMD_052F(UART_Command.Buffer);
break;
case 0x05DD: // reset
#if defined(ENABLE_OVERLAY)
overlay_FLASH_RebootToBootloader();
#else
NVIC_SystemReset();
#endif
break;
#ifdef ENABLE_UART_RW_BK_REGS
case 0x0601:
CMD_0601_ReadBK4819Reg(UART_Command.Buffer);
break;
case 0x0602:
CMD_0602_WriteBK4819Reg(UART_Command.Buffer);
break;
#endif
}
}

26
app/uart.h Normal file
View File

@@ -0,0 +1,26 @@
/* Copyright 2023 Dual Tachyon
* https://github.com/DualTachyon
*
* 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_UART_H
#define APP_UART_H
#include <stdbool.h>
bool UART_IsCommandAvailable(void);
void UART_HandleCommand(void);
#endif