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:
453
app/action.c
Normal file
453
app/action.c
Normal 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
40
app/action.h
Normal 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
242
app/aircopy.c
Normal 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
46
app/aircopy.h
Normal 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
|
||||
35
app/app.h
Normal file
35
app/app.h
Normal 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
53
app/boot_splash.c
Normal 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
7
app/boot_splash.h
Normal 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
273
app/chFrScanner.c
Normal 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
23
app/chFrScanner.h
Normal 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
77
app/common.c
Normal 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
13
app/common.h
Normal 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
128
app/display_rssi.c
Normal 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
36
app/display_rssi.h
Normal 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
504
app/dtmf.c
Normal 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
119
app/dtmf.h
Normal 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
66
app/flashlight.c
Normal 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
23
app/flashlight.h
Normal 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
605
app/fm.c
Normal 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
61
app/fm.h
Normal 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
225
app/generic.c
Normal 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
26
app/generic.h
Normal 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
743
app/main.c
Normal 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
25
app/main.h
Normal 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
1739
app/menu.c
Normal file
File diff suppressed because it is too large
Load Diff
38
app/menu.h
Normal file
38
app/menu.h
Normal 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
333
app/mode.c
Normal 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
50
app/mode.h
Normal 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
228
app/pocsag/bch31.c
Normal 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
25
app/pocsag/bch31.h
Normal 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
542
app/pocsag/pocsag.c
Normal 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
150
app/pocsag/pocsag.h
Normal 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
528
app/scanner.c
Normal 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
57
app/scanner.h
Normal 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
1310
app/spectrum.c
Normal file
File diff suppressed because it is too large
Load Diff
158
app/spectrum.h
Normal file
158
app/spectrum.h
Normal 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
624
app/uart.c
Normal 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
26
app/uart.h
Normal 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
|
||||
|
||||
Reference in New Issue
Block a user