mirror of
https://github.com/armel/uv-k1-k5v3-firmware-custom.git
synced 2026-10-02 03:15:37 +00:00
Add TripleVFO, the king of clickbait...
This commit is contained in:
1 parent
b86da168e9
commit
8c78d28f0d
5 files changed
+748
-4
No files matched your search
+54
-4
@@ -36,11 +36,11 @@
|
|||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
||||||
/* ABI 3: the table below (v1 core + the fields once labelled "v2 additions") is a
|
/* The table below is a single versioned layout. Any change to app_api_t - a
|
||||||
* single versioned layout. Any change to app_api_t - a reorder, a removal, or an
|
* reorder, a removal, or an append - MUST bump this. Keep in sync with the
|
||||||
* append - MUST bump this. Keep in sync with ABI_VERSION in pack_app.py, which
|
* value read by pack_app.py, which
|
||||||
* stamps the blob the loader checks against. */
|
* stamps the blob the loader checks against. */
|
||||||
#define APP_ABI_VERSION 3u
|
#define APP_ABI_VERSION 4u
|
||||||
|
|
||||||
/* KEY codes mirrored from driver/keyboard.h (enum KEY_Code_e). Kept in sync by
|
/* KEY codes mirrored from driver/keyboard.h (enum KEY_Code_e). Kept in sync by
|
||||||
* value so the app stays independent of the firmware headers. */
|
* value so the app stays independent of the firmware headers. */
|
||||||
@@ -61,6 +61,7 @@ enum {
|
|||||||
APP_KEY_EXIT = 13,
|
APP_KEY_EXIT = 13,
|
||||||
APP_KEY_STAR = 14,
|
APP_KEY_STAR = 14,
|
||||||
APP_KEY_F = 15,
|
APP_KEY_F = 15,
|
||||||
|
APP_KEY_PTT = 16,
|
||||||
APP_KEY_INVALID = 19,
|
APP_KEY_INVALID = 19,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -78,6 +79,43 @@ typedef struct {
|
|||||||
uint8_t sel_ch; /* selected memory channel 0..47 */
|
uint8_t sel_ch; /* selected memory channel 0..47 */
|
||||||
} app_fm_state_t;
|
} app_fm_state_t;
|
||||||
|
|
||||||
|
/* Compact, pointer-free description of one receiver in the resident triple-VFO
|
||||||
|
* service. Overlay apps must never see VFO_Info_t directly: its layout varies
|
||||||
|
* with firmware features and contains resident pointers. */
|
||||||
|
typedef struct {
|
||||||
|
uint32_t frequency; /* RX frequency, x10 Hz */
|
||||||
|
uint16_t channel; /* memory channel, zero based */
|
||||||
|
uint16_t step; /* step, 10 Hz units */
|
||||||
|
uint16_t code_value; /* CTCSS x0.1 Hz or DCS octal source value */
|
||||||
|
int16_t rssi_dbm; /* last/current corrected RSSI */
|
||||||
|
uint8_t modulation;
|
||||||
|
uint8_t power;
|
||||||
|
uint8_t bandwidth;
|
||||||
|
uint8_t code_type;
|
||||||
|
uint8_t code;
|
||||||
|
uint8_t offset_direction;
|
||||||
|
uint8_t reverse;
|
||||||
|
uint8_t squelch;
|
||||||
|
uint8_t flags; /* APP_TRIVFO_* below */
|
||||||
|
char name[11]; /* channel name, trimmed and NUL terminated */
|
||||||
|
} app_trivfo_info_t;
|
||||||
|
|
||||||
|
enum {
|
||||||
|
APP_TRIVFO_SELECTED = 1u << 0,
|
||||||
|
APP_TRIVFO_TUNED = 1u << 1,
|
||||||
|
APP_TRIVFO_RECEIVING = 1u << 2,
|
||||||
|
APP_TRIVFO_TX = 1u << 3,
|
||||||
|
APP_TRIVFO_USER_POWER = 1u << 4,
|
||||||
|
APP_TRIVFO_AUDIO_BAR = 1u << 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum {
|
||||||
|
APP_TRIVFO_SCAN = 0,
|
||||||
|
APP_TRIVFO_RX = 1,
|
||||||
|
APP_TRIVFO_HOLD = 2,
|
||||||
|
APP_TRIVFO_TX_STATE = 3,
|
||||||
|
};
|
||||||
|
|
||||||
typedef struct app_api {
|
typedef struct app_api {
|
||||||
uint8_t abi_version; /* == APP_ABI_VERSION */
|
uint8_t abi_version; /* == APP_ABI_VERSION */
|
||||||
|
|
||||||
@@ -174,6 +212,18 @@ typedef struct app_api {
|
|||||||
* UV-K1 LEFT/RIGHT -> -1/+1
|
* UV-K1 LEFT/RIGHT -> -1/+1
|
||||||
* Returns 0 for any other key. Keep get_key() raw for spatial controls. */
|
* Returns 0 for any other key. Keep get_key() raw for spatial controls. */
|
||||||
int8_t (*nav_dir)(uint8_t key);
|
int8_t (*nav_dir)(uint8_t key);
|
||||||
|
|
||||||
|
/* ---- triple VFO (ABI 4) ----
|
||||||
|
* A and B are the live Main Display VFOs. C is a resident temporary VFO
|
||||||
|
* loaded from c_channel (or the first valid memory after B when invalid).
|
||||||
|
* tick is called every 20 ms by the app and returns APP_TRIVFO_* state. */
|
||||||
|
uint16_t (*trivfo_enter)(uint16_t c_channel);
|
||||||
|
void (*trivfo_leave)(void);
|
||||||
|
void (*trivfo_get)(uint8_t vfo, app_trivfo_info_t *info);
|
||||||
|
void (*trivfo_select)(uint8_t vfo);
|
||||||
|
uint16_t (*trivfo_step)(uint8_t vfo, int8_t direction);
|
||||||
|
uint8_t (*trivfo_tick)(void);
|
||||||
|
uint8_t (*trivfo_ptt)(bool pressed); /* 0 OK; non-zero TX denial/timeout */
|
||||||
} app_api_t;
|
} app_api_t;
|
||||||
|
|
||||||
/* BK4819 AF modes for set_af (mirror driver/bk4819.h values). */
|
/* BK4819 AF modes for set_af (mirror driver/bk4819.h values). */
|
||||||
|
|||||||
@@ -32,10 +32,14 @@
|
|||||||
#include "driver/st7565.h"
|
#include "driver/st7565.h"
|
||||||
#include "driver/system.h"
|
#include "driver/system.h"
|
||||||
#include "driver/backlight.h"
|
#include "driver/backlight.h"
|
||||||
|
#include "app/app.h"
|
||||||
#include "ui/helper.h"
|
#include "ui/helper.h"
|
||||||
#include "ui/status.h"
|
#include "ui/status.h"
|
||||||
#include "board.h"
|
#include "board.h"
|
||||||
#include "audio.h"
|
#include "audio.h"
|
||||||
|
#include "dcs.h"
|
||||||
|
#include "functions.h"
|
||||||
|
#include "frequencies.h"
|
||||||
#include "radio.h"
|
#include "radio.h"
|
||||||
#include "helper/battery.h"
|
#include "helper/battery.h"
|
||||||
#include "settings.h"
|
#include "settings.h"
|
||||||
@@ -91,6 +95,382 @@ static uint32_t app_make_seed(void)
|
|||||||
* gEeprom.VfoInfo[0].pRX->Frequency;
|
* gEeprom.VfoInfo[0].pRX->Frequency;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- ABI 4: resident triple-VFO engine ---------------------------------
|
||||||
|
* The overlay owns the UI and key timing, while this resident engine owns all
|
||||||
|
* radio details. Keeping VFO_Info_t and BK4819 sequencing on this side makes
|
||||||
|
* the app independent of feature-dependent firmware layouts. */
|
||||||
|
#define APP_TRIVFO_COUNT 3u
|
||||||
|
#define APP_TRIVFO_TUNE_TICKS 5u /* 100 ms at one tick / 20 ms */
|
||||||
|
#define APP_TRIVFO_TX_HOLD_TICKS 125u /* keep the existing 2.5 s TX return */
|
||||||
|
|
||||||
|
static VFO_Info_t app_trivfo_c;
|
||||||
|
static VFO_Info_t *app_trivfo_saved_rx;
|
||||||
|
static VFO_Info_t *app_trivfo_saved_tx;
|
||||||
|
static VFO_Info_t *app_trivfo_saved_current;
|
||||||
|
static uint8_t app_trivfo_selected;
|
||||||
|
static uint8_t app_trivfo_pre_rx_selected;
|
||||||
|
static uint8_t app_trivfo_current;
|
||||||
|
static uint8_t app_trivfo_settle;
|
||||||
|
static uint16_t app_trivfo_hold;
|
||||||
|
static uint8_t app_trivfo_candidate_wait;
|
||||||
|
static uint32_t app_trivfo_tx_ticks;
|
||||||
|
static bool app_trivfo_running;
|
||||||
|
static bool app_trivfo_receiving;
|
||||||
|
static bool app_trivfo_transmitting;
|
||||||
|
static bool app_trivfo_sql_open;
|
||||||
|
static bool app_trivfo_ctcss_ok;
|
||||||
|
static bool app_trivfo_cdcss_ok;
|
||||||
|
static bool app_trivfo_ab_dirty;
|
||||||
|
static bool app_trivfo_restore_selection;
|
||||||
|
static uint8_t app_trivfo_freq_dirty;
|
||||||
|
|
||||||
|
static VFO_Info_t *app_trivfo_vfo(uint8_t vfo)
|
||||||
|
{
|
||||||
|
return vfo < 2u ? &gEeprom.VfoInfo[vfo] : &app_trivfo_c;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool app_trivfo_load_memory(VFO_Info_t *vfo, uint16_t channel)
|
||||||
|
{
|
||||||
|
ChannelScanDisplayInfo_t info;
|
||||||
|
if (!IS_MR_CHANNEL(channel) ||
|
||||||
|
!SETTINGS_FetchChannelScanDisplayInfo(channel, &info))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
RADIO_InitInfo(vfo, channel, info.rx.Frequency);
|
||||||
|
vfo->freq_config_RX = info.rx;
|
||||||
|
vfo->freq_config_TX = info.tx;
|
||||||
|
vfo->TX_OFFSET_FREQUENCY = info.offset;
|
||||||
|
vfo->StepFrequency = info.stepFrequency;
|
||||||
|
vfo->STEP_SETTING = info.stepSetting;
|
||||||
|
vfo->Modulation = info.modulation;
|
||||||
|
vfo->TX_OFFSET_FREQUENCY_DIRECTION = info.txOffsetFrequencyDirection;
|
||||||
|
vfo->OUTPUT_POWER = info.outputPower;
|
||||||
|
vfo->FrequencyReverse = info.frequencyReverse;
|
||||||
|
vfo->CHANNEL_BANDWIDTH = info.channelBandwidth;
|
||||||
|
vfo->BUSY_CHANNEL_LOCK = info.busyChannelLock;
|
||||||
|
vfo->TX_LOCK = info.txLock;
|
||||||
|
#ifdef ENABLE_DTMF_CALLING
|
||||||
|
vfo->DTMF_DECODING_ENABLE = info.dtmfDecodingEnable;
|
||||||
|
#endif
|
||||||
|
vfo->DTMF_PTT_ID_TX_MODE = info.dtmfPttIdTxMode;
|
||||||
|
vfo->Band = FREQUENCY_GetBand(vfo->freq_config_RX.Frequency);
|
||||||
|
vfo->Compander = MR_GetChannelAttributes(channel)->compander;
|
||||||
|
SETTINGS_FetchChannelName(vfo->Name, channel);
|
||||||
|
vfo->pRX = vfo->FrequencyReverse ? &vfo->freq_config_TX : &vfo->freq_config_RX;
|
||||||
|
vfo->pTX = vfo->FrequencyReverse ? &vfo->freq_config_RX : &vfo->freq_config_TX;
|
||||||
|
RADIO_ConfigureSquelchAndOutputPower(vfo);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint16_t app_trivfo_next_channel(uint16_t channel, int8_t direction, uint8_t vfo)
|
||||||
|
{
|
||||||
|
if (direction == 0)
|
||||||
|
direction = 1;
|
||||||
|
channel = RADIO_FindNextChannel((uint16_t)(channel + direction), direction,
|
||||||
|
false, vfo < 2u ? vfo : 0u);
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void app_trivfo_tune(uint8_t vfo)
|
||||||
|
{
|
||||||
|
app_trivfo_current = vfo % APP_TRIVFO_COUNT;
|
||||||
|
gRxVfo = app_trivfo_vfo(app_trivfo_current);
|
||||||
|
gCurrentVfo = gRxVfo;
|
||||||
|
app_trivfo_receiving = false;
|
||||||
|
app_trivfo_sql_open = false;
|
||||||
|
app_trivfo_ctcss_ok = false;
|
||||||
|
app_trivfo_cdcss_ok = false;
|
||||||
|
app_trivfo_candidate_wait = 0;
|
||||||
|
app_trivfo_settle = APP_TRIVFO_TUNE_TICKS;
|
||||||
|
AUDIO_AudioPathOff();
|
||||||
|
gEnableSpeaker = false;
|
||||||
|
RADIO_SetupRegisters(false);
|
||||||
|
FUNCTION_Init();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void app_trivfo_poll_irq(void)
|
||||||
|
{
|
||||||
|
while (BK4819_ReadRegister(BK4819_REG_0C) & 1u) {
|
||||||
|
BK4819_WriteRegister(BK4819_REG_02, 0);
|
||||||
|
const uint16_t irq = BK4819_ReadRegister(BK4819_REG_02);
|
||||||
|
if (irq & BK4819_REG_02_SQUELCH_LOST) app_trivfo_sql_open = true;
|
||||||
|
if (irq & BK4819_REG_02_SQUELCH_FOUND) {
|
||||||
|
app_trivfo_sql_open = false;
|
||||||
|
app_trivfo_ctcss_ok = false;
|
||||||
|
app_trivfo_cdcss_ok = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A BK4819 CSS interrupt is a transition, not a persistent level.
|
||||||
|
* Depending on the silicon revision and the configured polarity, the
|
||||||
|
* first transition can be reported as FOUND or LOST. MAIN keeps the
|
||||||
|
* decoder state across both transitions; do the same here and use any
|
||||||
|
* CSS transition as proof that the configured decoder has acquired the
|
||||||
|
* signal. A wrong CTCSS/DCS does not generate either transition. */
|
||||||
|
if (irq & (BK4819_REG_02_CTCSS_LOST | BK4819_REG_02_CTCSS_FOUND))
|
||||||
|
app_trivfo_ctcss_ok = true;
|
||||||
|
if (irq & (BK4819_REG_02_CDCSS_LOST | BK4819_REG_02_CDCSS_FOUND))
|
||||||
|
app_trivfo_cdcss_ok = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool app_trivfo_qualified(void)
|
||||||
|
{
|
||||||
|
const VFO_Info_t *vfo = app_trivfo_vfo(app_trivfo_current);
|
||||||
|
if (!app_trivfo_sql_open)
|
||||||
|
return false;
|
||||||
|
if (vfo->Modulation != MODULATION_FM || vfo->pRX->CodeType == CODE_TYPE_OFF)
|
||||||
|
return true;
|
||||||
|
if (vfo->pRX->CodeType == CODE_TYPE_CONTINUOUS_TONE)
|
||||||
|
return app_trivfo_ctcss_ok;
|
||||||
|
return app_trivfo_cdcss_ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint16_t app_trivfo_enter(uint16_t c_channel)
|
||||||
|
{
|
||||||
|
app_trivfo_saved_rx = gRxVfo;
|
||||||
|
app_trivfo_saved_tx = gTxVfo;
|
||||||
|
app_trivfo_saved_current = gCurrentVfo;
|
||||||
|
|
||||||
|
if (!IS_MR_CHANNEL(c_channel) ||
|
||||||
|
!SETTINGS_FetchChannelScanInfo(c_channel, NULL, NULL)) {
|
||||||
|
uint16_t start = IS_MR_CHANNEL(gEeprom.ScreenChannel[1])
|
||||||
|
? gEeprom.ScreenChannel[1] : gEeprom.MrChannel[1];
|
||||||
|
c_channel = app_trivfo_next_channel(start, 1, 2);
|
||||||
|
}
|
||||||
|
if (c_channel == 0xFFFFu || !app_trivfo_load_memory(&app_trivfo_c, c_channel)) {
|
||||||
|
c_channel = RADIO_FindNextChannel(MR_CHANNEL_FIRST, RADIO_CHANNEL_UP, false, 0);
|
||||||
|
if (c_channel != 0xFFFFu)
|
||||||
|
app_trivfo_load_memory(&app_trivfo_c, c_channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
app_trivfo_selected = 0;
|
||||||
|
app_trivfo_pre_rx_selected = 0;
|
||||||
|
app_trivfo_restore_selection = false;
|
||||||
|
app_trivfo_hold = 0;
|
||||||
|
app_trivfo_running = true;
|
||||||
|
app_trivfo_transmitting = false;
|
||||||
|
app_trivfo_ab_dirty = false;
|
||||||
|
app_trivfo_freq_dirty = 0;
|
||||||
|
app_trivfo_tune(0);
|
||||||
|
return c_channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void app_trivfo_leave(void)
|
||||||
|
{
|
||||||
|
if (!app_trivfo_running)
|
||||||
|
return;
|
||||||
|
if (app_trivfo_transmitting) {
|
||||||
|
BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, false);
|
||||||
|
BK4819_ToggleGpioOut(BK4819_GPIO5_PIN1_RED, false);
|
||||||
|
}
|
||||||
|
AUDIO_AudioPathOff();
|
||||||
|
gEnableSpeaker = false;
|
||||||
|
app_trivfo_running = false;
|
||||||
|
app_trivfo_transmitting = false;
|
||||||
|
gRxVfo = app_trivfo_saved_rx;
|
||||||
|
gTxVfo = app_trivfo_saved_tx;
|
||||||
|
gCurrentVfo = app_trivfo_saved_current;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void app_trivfo_get(uint8_t index, app_trivfo_info_t *info)
|
||||||
|
{
|
||||||
|
if (info == NULL || index >= APP_TRIVFO_COUNT)
|
||||||
|
return;
|
||||||
|
const VFO_Info_t *vfo = app_trivfo_vfo(index);
|
||||||
|
memset(info, 0, sizeof(*info));
|
||||||
|
info->frequency = vfo->pRX->Frequency;
|
||||||
|
info->channel = vfo->CHANNEL_SAVE;
|
||||||
|
info->step = vfo->StepFrequency;
|
||||||
|
if (vfo->pRX->CodeType == CODE_TYPE_CONTINUOUS_TONE)
|
||||||
|
info->code_value = CTCSS_Options[vfo->pRX->Code];
|
||||||
|
else if (vfo->pRX->CodeType == CODE_TYPE_DIGITAL ||
|
||||||
|
vfo->pRX->CodeType == CODE_TYPE_REVERSE_DIGITAL)
|
||||||
|
info->code_value = DCS_Options[vfo->pRX->Code];
|
||||||
|
info->rssi_dbm = (index == app_trivfo_current)
|
||||||
|
? BK4819_GetRSSI_dBm() + dBmCorrTable[vfo->Band] : -160;
|
||||||
|
info->modulation = vfo->Modulation;
|
||||||
|
info->power = vfo->OUTPUT_POWER == OUTPUT_POWER_USER
|
||||||
|
? (uint8_t)(gSetting_set_pwr + 1u) : vfo->OUTPUT_POWER;
|
||||||
|
info->bandwidth = vfo->CHANNEL_BANDWIDTH;
|
||||||
|
#ifdef ENABLE_FEAT_F4HWN_NARROWER
|
||||||
|
if (info->bandwidth == BANDWIDTH_NARROW && gSetting_set_nfm == 1)
|
||||||
|
info->bandwidth++;
|
||||||
|
#endif
|
||||||
|
info->code_type = vfo->pRX->CodeType;
|
||||||
|
info->code = vfo->pRX->Code;
|
||||||
|
info->offset_direction = vfo->TX_OFFSET_FREQUENCY_DIRECTION;
|
||||||
|
info->reverse = vfo->FrequencyReverse;
|
||||||
|
info->squelch = gEeprom.SQUELCH_LEVEL;
|
||||||
|
if (index == app_trivfo_selected) info->flags |= APP_TRIVFO_SELECTED;
|
||||||
|
if (index == app_trivfo_current) info->flags |= APP_TRIVFO_TUNED;
|
||||||
|
if (index == app_trivfo_current && app_trivfo_receiving) info->flags |= APP_TRIVFO_RECEIVING;
|
||||||
|
if (index == app_trivfo_selected && app_trivfo_transmitting) info->flags |= APP_TRIVFO_TX;
|
||||||
|
if (vfo->OUTPUT_POWER == OUTPUT_POWER_USER) info->flags |= APP_TRIVFO_USER_POWER;
|
||||||
|
#ifdef ENABLE_AUDIO_BAR
|
||||||
|
if (gSetting_mic_bar) info->flags |= APP_TRIVFO_AUDIO_BAR;
|
||||||
|
#endif
|
||||||
|
if (IS_MR_CHANNEL(vfo->CHANNEL_SAVE))
|
||||||
|
memcpy(info->name, vfo->Name, sizeof(info->name) - 1u);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void app_trivfo_select(uint8_t vfo)
|
||||||
|
{
|
||||||
|
if (vfo < APP_TRIVFO_COUNT)
|
||||||
|
app_trivfo_selected = vfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint16_t app_trivfo_step(uint8_t index, int8_t direction)
|
||||||
|
{
|
||||||
|
if (index >= APP_TRIVFO_COUNT || app_trivfo_transmitting)
|
||||||
|
return 0xFFFFu;
|
||||||
|
VFO_Info_t *vfo = app_trivfo_vfo(index);
|
||||||
|
|
||||||
|
if (IS_FREQ_CHANNEL(vfo->CHANNEL_SAVE)) {
|
||||||
|
if (direction == 0)
|
||||||
|
direction = 1;
|
||||||
|
const uint32_t frequency = APP_SetFrequencyByStep(vfo, direction);
|
||||||
|
if (RX_freq_check(frequency) < 0)
|
||||||
|
return 0xFFFFu;
|
||||||
|
|
||||||
|
vfo->freq_config_RX.Frequency = frequency;
|
||||||
|
RADIO_ApplyOffset(vfo);
|
||||||
|
RADIO_ConfigureSquelchAndOutputPower(vfo);
|
||||||
|
if (index < 2u)
|
||||||
|
app_trivfo_freq_dirty |= (uint8_t)(1u << index);
|
||||||
|
app_trivfo_hold = 0;
|
||||||
|
app_trivfo_tune(index);
|
||||||
|
return vfo->CHANNEL_SAVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t base = IS_MR_CHANNEL(vfo->CHANNEL_SAVE) ? vfo->CHANNEL_SAVE
|
||||||
|
: (index < 2u ? gEeprom.MrChannel[index] : gEeprom.MrChannel[1]);
|
||||||
|
const uint16_t channel = app_trivfo_next_channel(base, direction, index);
|
||||||
|
if (channel == 0xFFFFu)
|
||||||
|
return channel;
|
||||||
|
if (index < 2u) {
|
||||||
|
gEeprom.ScreenChannel[index] = channel;
|
||||||
|
gEeprom.MrChannel[index] = channel;
|
||||||
|
RADIO_ConfigureChannel(index, VFO_CONFIGURE_RELOAD);
|
||||||
|
app_trivfo_ab_dirty = true;
|
||||||
|
} else {
|
||||||
|
app_trivfo_load_memory(&app_trivfo_c, channel);
|
||||||
|
}
|
||||||
|
app_trivfo_hold = 0;
|
||||||
|
app_trivfo_tune(index);
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t app_trivfo_ptt(bool pressed);
|
||||||
|
|
||||||
|
static uint8_t app_trivfo_tick(void)
|
||||||
|
{
|
||||||
|
if (!app_trivfo_running)
|
||||||
|
return APP_TRIVFO_SCAN;
|
||||||
|
if (app_trivfo_transmitting) {
|
||||||
|
const uint32_t timeout = ((uint32_t)gEeprom.TX_TIMEOUT_TIMER + 1u) * 250u;
|
||||||
|
if (++app_trivfo_tx_ticks >= timeout) {
|
||||||
|
app_trivfo_ptt(false);
|
||||||
|
return APP_TRIVFO_HOLD;
|
||||||
|
}
|
||||||
|
return APP_TRIVFO_TX_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
app_trivfo_poll_irq();
|
||||||
|
if (app_trivfo_settle > 0) {
|
||||||
|
app_trivfo_settle--;
|
||||||
|
return APP_TRIVFO_SCAN;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool qualified = app_trivfo_qualified();
|
||||||
|
if (qualified) {
|
||||||
|
app_trivfo_hold = 0;
|
||||||
|
if (!app_trivfo_receiving) {
|
||||||
|
if (app_trivfo_selected != app_trivfo_current) {
|
||||||
|
app_trivfo_pre_rx_selected = app_trivfo_selected;
|
||||||
|
app_trivfo_restore_selection = true;
|
||||||
|
app_trivfo_selected = app_trivfo_current;
|
||||||
|
}
|
||||||
|
app_trivfo_receiving = true;
|
||||||
|
AUDIO_AudioPathOn();
|
||||||
|
gEnableSpeaker = true;
|
||||||
|
BK4819_SetRxAudioGain();
|
||||||
|
/* BK4819_SetupSquelch() ends by selecting AF_MUTE. Mirror
|
||||||
|
* APP_StartListening(): restore the channel demodulator only once
|
||||||
|
* the carrier/CSS has qualified. */
|
||||||
|
RADIO_SetModulation(gRxVfo->Modulation);
|
||||||
|
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, true);
|
||||||
|
}
|
||||||
|
return APP_TRIVFO_RX;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (app_trivfo_receiving) {
|
||||||
|
app_trivfo_receiving = false;
|
||||||
|
AUDIO_AudioPathOff();
|
||||||
|
gEnableSpeaker = false;
|
||||||
|
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, false);
|
||||||
|
/* Use the same receive-response dwell as resident DWR. The resident
|
||||||
|
* value is expressed in 10 ms ticks; Triple VFO ticks every 20 ms. */
|
||||||
|
app_trivfo_hold = (dual_watch_count_after_2_10ms + 1u) / 2u;
|
||||||
|
app_trivfo_candidate_wait = 50u;
|
||||||
|
}
|
||||||
|
if (app_trivfo_hold > 0) {
|
||||||
|
app_trivfo_hold--;
|
||||||
|
if (app_trivfo_hold == 0 && app_trivfo_restore_selection) {
|
||||||
|
app_trivfo_selected = app_trivfo_pre_rx_selected;
|
||||||
|
app_trivfo_restore_selection = false;
|
||||||
|
}
|
||||||
|
return APP_TRIVFO_HOLD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Like resident dual watch, give a coded carrier time to acquire its CSS.
|
||||||
|
* A wrong tone/code must not monopolise the receiver indefinitely. */
|
||||||
|
if (app_trivfo_sql_open && app_trivfo_candidate_wait < 50u) {
|
||||||
|
app_trivfo_candidate_wait++;
|
||||||
|
return APP_TRIVFO_HOLD;
|
||||||
|
}
|
||||||
|
|
||||||
|
app_trivfo_tune((uint8_t)((app_trivfo_current + 1u) % APP_TRIVFO_COUNT));
|
||||||
|
return APP_TRIVFO_SCAN;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t app_trivfo_ptt(bool pressed)
|
||||||
|
{
|
||||||
|
if (!app_trivfo_running)
|
||||||
|
return 1;
|
||||||
|
if (!pressed) {
|
||||||
|
if (!app_trivfo_transmitting)
|
||||||
|
return 0;
|
||||||
|
RADIO_SendEndOfTransmission();
|
||||||
|
app_trivfo_transmitting = false;
|
||||||
|
BK4819_ToggleGpioOut(BK4819_GPIO5_PIN1_RED, false);
|
||||||
|
app_trivfo_hold = APP_TRIVFO_TX_HOLD_TICKS;
|
||||||
|
app_trivfo_tune(app_trivfo_selected);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (app_trivfo_transmitting)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
VFO_Info_t *vfo = app_trivfo_vfo(app_trivfo_selected);
|
||||||
|
if ((TX_freq_check(vfo->pTX->Frequency) != 0 && vfo->TX_LOCK) ||
|
||||||
|
vfo->Modulation != MODULATION_FM ||
|
||||||
|
(vfo->BUSY_CHANNEL_LOCK && app_trivfo_receiving) ||
|
||||||
|
gBatteryDisplayLevel == 0 || gBatteryDisplayLevel > 6)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
AUDIO_AudioPathOff();
|
||||||
|
gEnableSpeaker = false;
|
||||||
|
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, false);
|
||||||
|
gRxVfo = gTxVfo = gCurrentVfo = vfo;
|
||||||
|
RADIO_SetTxParameters();
|
||||||
|
BK4819_ToggleGpioOut(BK4819_GPIO5_PIN1_RED, true);
|
||||||
|
BK4819_DisableScramble();
|
||||||
|
app_trivfo_current = app_trivfo_selected;
|
||||||
|
app_trivfo_receiving = false;
|
||||||
|
app_trivfo_transmitting = true;
|
||||||
|
app_trivfo_tx_ticks = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- v2 radio wrappers ---- */
|
/* ---- v2 radio wrappers ---- */
|
||||||
static int16_t app_rssi_dbm(void) { return BK4819_GetRSSI_dBm() + dBmCorrTable[gRxVfo->Band]; }
|
static int16_t app_rssi_dbm(void) { return BK4819_GetRSSI_dBm() + dBmCorrTable[gRxVfo->Band]; }
|
||||||
static uint16_t app_bk_read(uint8_t r) { return BK4819_ReadRegister((BK4819_REGISTER_t)r); }
|
static uint16_t app_bk_read(uint8_t r) { return BK4819_ReadRegister((BK4819_REGISTER_t)r); }
|
||||||
@@ -133,6 +513,12 @@ static void app_draw_battery(void)
|
|||||||
}
|
}
|
||||||
static void app_battery_sample(void)
|
static void app_battery_sample(void)
|
||||||
{
|
{
|
||||||
|
/* The resident scheduler deliberately skips ADC battery updates while the
|
||||||
|
* PA is keyed. Do the same for Triple VFO: sampling the loaded voltage as
|
||||||
|
* capacity made an 80% pack appear to fall immediately to about 16%. */
|
||||||
|
if (app_trivfo_transmitting)
|
||||||
|
return;
|
||||||
|
|
||||||
BOARD_ADC_GetBatteryInfo(&gBatteryVoltages[gBatteryVoltageIndex++], &gBatteryCurrent);
|
BOARD_ADC_GetBatteryInfo(&gBatteryVoltages[gBatteryVoltageIndex++], &gBatteryCurrent);
|
||||||
if (gBatteryVoltageIndex > 3)
|
if (gBatteryVoltageIndex > 3)
|
||||||
gBatteryVoltageIndex = 0;
|
gBatteryVoltageIndex = 0;
|
||||||
@@ -334,6 +720,13 @@ uint8_t APP_LaunchOverlay(uint8_t slot)
|
|||||||
.fm_commit = app_fm_commit,
|
.fm_commit = app_fm_commit,
|
||||||
#endif
|
#endif
|
||||||
.nav_dir = app_nav_dir,
|
.nav_dir = app_nav_dir,
|
||||||
|
.trivfo_enter = app_trivfo_enter,
|
||||||
|
.trivfo_leave = app_trivfo_leave,
|
||||||
|
.trivfo_get = app_trivfo_get,
|
||||||
|
.trivfo_select = app_trivfo_select,
|
||||||
|
.trivfo_step = app_trivfo_step,
|
||||||
|
.trivfo_tick = app_trivfo_tick,
|
||||||
|
.trivfo_ptt = app_trivfo_ptt,
|
||||||
};
|
};
|
||||||
|
|
||||||
app_run_slot = slot; /* for cfg_load / cfg_save */
|
app_run_slot = slot; /* for cfg_load / cfg_save */
|
||||||
@@ -359,6 +752,9 @@ uint8_t APP_LaunchOverlay(uint8_t slot)
|
|||||||
app_entry_t entry = (app_entry_t)(((uint32_t)ws + h.entry_off) | 1u);
|
app_entry_t entry = (app_entry_t)(((uint32_t)ws + h.entry_off) | 1u);
|
||||||
entry(&api);
|
entry(&api);
|
||||||
|
|
||||||
|
/* A defensive leave also covers an app returning through an error path. */
|
||||||
|
app_trivfo_leave();
|
||||||
|
|
||||||
/* Restore the resident RX/dual-watch tuning the app ran on top of. */
|
/* Restore the resident RX/dual-watch tuning the app ran on top of. */
|
||||||
gEeprom.RX_VFO = saved_rx_vfo;
|
gEeprom.RX_VFO = saved_rx_vfo;
|
||||||
gRxVfo = saved_rx;
|
gRxVfo = saved_rx;
|
||||||
@@ -367,6 +763,18 @@ uint8_t APP_LaunchOverlay(uint8_t slot)
|
|||||||
/* The overlay held app code, not a valid config sector. */
|
/* The overlay held app code, not a valid config sector. */
|
||||||
PY25Q16_InvalidateCache();
|
PY25Q16_InvalidateCache();
|
||||||
|
|
||||||
|
if (app_trivfo_ab_dirty) {
|
||||||
|
SETTINGS_SaveVfoIndices();
|
||||||
|
app_trivfo_ab_dirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < 2u; i++) {
|
||||||
|
if (app_trivfo_freq_dirty & (1u << i))
|
||||||
|
SETTINGS_SaveChannel(gEeprom.VfoInfo[i].CHANNEL_SAVE, i,
|
||||||
|
&gEeprom.VfoInfo[i], 1);
|
||||||
|
}
|
||||||
|
app_trivfo_freq_dirty = 0;
|
||||||
|
|
||||||
/* Commit any deferred config the app staged (RMW keeps the slot header). */
|
/* Commit any deferred config the app staged (RMW keeps the slot header). */
|
||||||
if (app_cfg_len) {
|
if (app_cfg_len) {
|
||||||
PY25Q16_WriteBuffer(APP_SLOT_BASE(slot) + APP_CFG_OFFSET, app_cfg_buf, app_cfg_len, false);
|
PY25Q16_WriteBuffer(APP_SLOT_BASE(slot) + APP_CFG_OFFSET, app_cfg_buf, app_cfg_len, false);
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
APP_VMA = DEFINED(APP_VMA) ? APP_VMA : 0x20000280;
|
||||||
|
APP_LENGTH = 0x1000;
|
||||||
|
ENTRY(app_main)
|
||||||
|
MEMORY { APP (rwx) : ORIGIN = APP_VMA, LENGTH = APP_LENGTH }
|
||||||
|
SECTIONS {
|
||||||
|
.app APP_VMA : {
|
||||||
|
KEEP(*(.text.entry)) *(.text .text.*) *(.rodata .rodata.*)
|
||||||
|
. = ALIGN(4); *(.data .data.*) . = ALIGN(4); *(.bss .bss.* COMMON) . = ALIGN(4);
|
||||||
|
} > APP
|
||||||
|
ASSERT(. <= APP_VMA + APP_LENGTH, "Triple VFO overlay app overflows the 4 KiB overlay")
|
||||||
|
/DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.eh_frame*) *(.comment) *(.note.*) }
|
||||||
|
}
|
||||||
Executable
+20
@@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
APP="$(basename "$PWD")"
|
||||||
|
APP_NAME="Triple VFO"
|
||||||
|
APP_VER="1.0"
|
||||||
|
APP_VMA=${APP_VMA:-0x20000280}
|
||||||
|
OUT="${APP_NAME// /}"
|
||||||
|
CC=/opt/toolchain/bin/arm-none-eabi-gcc
|
||||||
|
OBJCOPY=/opt/toolchain/bin/arm-none-eabi-objcopy
|
||||||
|
command -v arm-none-eabi-gcc >/dev/null 2>&1 && { CC=arm-none-eabi-gcc; OBJCOPY=arm-none-eabi-objcopy; }
|
||||||
|
CFLAGS="-mcpu=cortex-m0plus -mthumb -Os -std=gnu11 -ffreestanding -fno-builtin -fno-common -fomit-frame-pointer -ffunction-sections -fdata-sections -Wall -Wextra"
|
||||||
|
LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} -Wl,--gc-sections -Wl,-Map=${APP}.map -Wl,--build-id=none -Wl,--no-warn-rwx-segments"
|
||||||
|
rm -f ./*.app ./*.elf ./*.bin
|
||||||
|
"$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
|
||||||
|
"$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
|
||||||
|
python3 ../pack_app.py "${APP}.bin" "${OUT}.app" --name "$APP_NAME" --ver "$APP_VER" --vma "${APP_VMA}" >/dev/null
|
||||||
|
BYTES=$(wc -c < "${APP}.bin")
|
||||||
|
test "$BYTES" -le 4096
|
||||||
|
printf ' ✅ %-13s %4d B (%d%% of 4 KiB) -> %s.app\n' \
|
||||||
|
"$APP_NAME" "$BYTES" "$(( BYTES * 100 / 4096 ))" "$OUT"
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
/* Copyright 2026 Armel F4HWN
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*
|
||||||
|
* Triple VFO — modal overlay receiver/transmitter. A and B are the resident
|
||||||
|
* Main Display VFOs; C is an app-persistent memory channel. The resident ABI
|
||||||
|
* performs all RF sequencing while this blob owns the seven-line Tiny UI.
|
||||||
|
*/
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include "../app_api.h"
|
||||||
|
|
||||||
|
#define TICK_MS 20u
|
||||||
|
#define LONG_MS 400u
|
||||||
|
#define CFG_MAGIC 0xC3u
|
||||||
|
#define VFO_COUNT 3u
|
||||||
|
#define MR_MAX 1024u
|
||||||
|
#define SCOPE_SAMPLES 43u
|
||||||
|
#define SCOPE_FLOOR 200u
|
||||||
|
|
||||||
|
static const app_api_t *A;
|
||||||
|
static app_trivfo_info_t vi[VFO_COUNT];
|
||||||
|
static uint8_t selected, state;
|
||||||
|
static uint16_t cChannel;
|
||||||
|
static bool running, fArm, txDenied, channelLabelOn, showFrequency;
|
||||||
|
static uint16_t scopeBuf[SCOPE_SAMPLES], scopeFloor;
|
||||||
|
static uint8_t scopeWrite, scopeReady, batteryTicks;
|
||||||
|
static bool scopeTx;
|
||||||
|
static char text[16];
|
||||||
|
|
||||||
|
/* Resident status.c uses this exact 8-column inverted glyph at x=69. Keep
|
||||||
|
* the overlay copy byte-for-byte identical so F has the same position and
|
||||||
|
* polarity as every other firmware screen. */
|
||||||
|
static const uint8_t fontF[8] = {
|
||||||
|
0x7f, 0x00, 0x76, 0x76, 0x76, 0x76, 0x7e, 0x7f
|
||||||
|
};
|
||||||
|
|
||||||
|
static uint8_t slen(const char *s){ uint8_t n=0; while(s[n])n++; return n; }
|
||||||
|
static bool copyName(char *d,const char *s){
|
||||||
|
uint8_t n=0;
|
||||||
|
while(n<10u){
|
||||||
|
uint8_t c=(uint8_t)s[n];
|
||||||
|
if(c==0u||c==0xffu) break;
|
||||||
|
d[n++]=(char)c;
|
||||||
|
}
|
||||||
|
while(n>0u&&d[n-1u]==' ') n--;
|
||||||
|
d[n]='\0';
|
||||||
|
return n>0u;
|
||||||
|
}
|
||||||
|
static char *put(char *o,const char *s){ while(*s)*o++=*s++; return o; }
|
||||||
|
static char *putu(char *o,uint32_t v){ char t[10]; int8_t n=0; do{t[n++]=(char)('0'+v%10u);v/=10u;}while(v); while(n--)*o++=t[n]; return o; }
|
||||||
|
static char *puti(char *o,int16_t v){ if(v<0){*o++='-';v=(int16_t)-v;} return putu(o,(uint16_t)v); }
|
||||||
|
static char *put3(char *o,uint16_t v){ *o++=(char)('0'+(v/100u)%10u); *o++=(char)('0'+(v/10u)%10u); *o++=(char)('0'+v%10u); return o; }
|
||||||
|
static char *put4(char *o,uint16_t v){ *o++=(char)('0'+(v/1000u)%10u); return put3(o,(uint16_t)(v%1000u)); }
|
||||||
|
|
||||||
|
static void formatFreq(char *s,uint32_t f){
|
||||||
|
char *o=putu(s,f/100000u); *o++='.'; f%=100000u;
|
||||||
|
uint32_t p=10000u; while(p){ *o++=(char)('0'+(f/p)%10u); p/=10u; } *o='\0';
|
||||||
|
}
|
||||||
|
static void formatChannel(char *s,const app_trivfo_info_t *v){
|
||||||
|
char *o=s;
|
||||||
|
if(v->channel<MR_MAX) o=put4(o,(uint16_t)(v->channel+1u));
|
||||||
|
else { *o++='F'; o=putu(o,(uint16_t)(v->channel-MR_MAX+1u)); }
|
||||||
|
*o='\0';
|
||||||
|
}
|
||||||
|
static void formatStep(char *s,uint16_t step){
|
||||||
|
char *o=putu(s,step/100u);
|
||||||
|
if(step%100u){ *o++='.'; *o++=(char)('0'+(step/10u)%10u); *o++=(char)('0'+step%10u); }
|
||||||
|
*o++='K'; *o='\0';
|
||||||
|
}
|
||||||
|
static void formatCode(char *s,const app_trivfo_info_t *v){
|
||||||
|
char *o=s;
|
||||||
|
if(v->code_type==1u){ o=putu(o,v->code_value/10u); *o++='.'; *o++=(char)('0'+v->code_value%10u); }
|
||||||
|
else if(v->code_type==2u||v->code_type==3u){
|
||||||
|
uint16_t n=v->code_value; *o++=(char)('0'+((n>>6)&7u)); *o++=(char)('0'+((n>>3)&7u)); *o++=(char)('0'+(n&7u)); *o++=(v->code_type==2u?'N':'I');
|
||||||
|
} else return formatStep(s,v->step);
|
||||||
|
*o='\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
static void drawMeter(const app_trivfo_info_t *v){
|
||||||
|
int16_t dbm=v->rssi_dbm; if(dbm>-53)dbm=-53;
|
||||||
|
uint8_t s=0,over=0;
|
||||||
|
if(dbm>=-93){ s=9; over=(uint8_t)(dbm+93); if(over>40)over=40; }
|
||||||
|
else if(dbm>=-141) s=(uint8_t)((dbm+147)/6);
|
||||||
|
|
||||||
|
char *o=text; if(dbm>-100)*o++=' '; o=puti(o,dbm); o=put(o," dBm"); *o='\0';
|
||||||
|
A->print_tiny(text,2,1,false,true);
|
||||||
|
if(over){ o=text; *o++='+'; if(over<10)*o++='0'; o=putu(o,over); }
|
||||||
|
else { o=text; *o++='S'; o=putu(o,s); }
|
||||||
|
*o='\0'; A->print_normal(text,38,0,0);
|
||||||
|
|
||||||
|
uint8_t level=(uint8_t)(s+over/10u); if(level>13u)level=13u;
|
||||||
|
for(uint8_t i=0;i<level;i++){
|
||||||
|
uint8_t *p=&A->fb[0][62u+i*5u];
|
||||||
|
p[0]=0x3e; p[1]=(i<9u)?0x3e:0x22; p[2]=(i<9u)?0x3e:0x22; p[3]=0x3e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void drawAudioScope(void){
|
||||||
|
if(!scopeTx){
|
||||||
|
for(uint8_t i=0;i<SCOPE_SAMPLES;i++) scopeBuf[i]=SCOPE_FLOOR;
|
||||||
|
scopeFloor=SCOPE_FLOOR; scopeWrite=scopeReady=0; scopeTx=true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(scopeReady>=7u) scopeBuf[scopeWrite]=A->bk_read(0x64u);
|
||||||
|
else scopeReady++;
|
||||||
|
if(scopeBuf[scopeWrite]==0u) scopeBuf[scopeWrite]=SCOPE_FLOOR;
|
||||||
|
scopeWrite=(uint8_t)((scopeWrite+1u)%SCOPE_SAMPLES);
|
||||||
|
|
||||||
|
uint16_t min=scopeBuf[0], max=scopeBuf[0];
|
||||||
|
for(uint8_t i=1;i<SCOPE_SAMPLES;i++){
|
||||||
|
if(scopeBuf[i]<min) min=scopeBuf[i];
|
||||||
|
if(scopeBuf[i]>max) max=scopeBuf[i];
|
||||||
|
}
|
||||||
|
if(scopeFloor>min) scopeFloor=(uint16_t)(scopeFloor-((scopeFloor-min)>>3)-1u);
|
||||||
|
else scopeFloor=(uint16_t)(scopeFloor+2u);
|
||||||
|
uint16_t range=max>scopeFloor?(uint16_t)(max-scopeFloor):0u;
|
||||||
|
|
||||||
|
for(uint8_t i=0;i<SCOPE_SAMPLES;i++){
|
||||||
|
uint8_t idx=(uint8_t)((scopeWrite+i)%SCOPE_SAMPLES), height=0;
|
||||||
|
if(range>=50u){
|
||||||
|
uint16_t value=scopeBuf[idx]>scopeFloor?(uint16_t)(scopeBuf[idx]-scopeFloor):0u;
|
||||||
|
height=(uint8_t)((uint32_t)value*7u/range);
|
||||||
|
}
|
||||||
|
uint8_t mask=height?(uint8_t)((0x7fu<<(7u-height))&0x7fu):0x40u;
|
||||||
|
A->fb[0][i*3u]=mask; A->fb[0][i*3u+1u]=mask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void drawVfo(uint8_t n){
|
||||||
|
const app_trivfo_info_t *v=&vi[n];
|
||||||
|
uint8_t mainLine=(uint8_t)(n*2u+1u), techLine=(uint8_t)(mainLine+1u);
|
||||||
|
uint8_t techY=(uint8_t)(techLine*8u+1u);
|
||||||
|
|
||||||
|
formatChannel(text,v);
|
||||||
|
if(!(v->flags&APP_TRIVFO_RECEIVING)||channelLabelOn)
|
||||||
|
A->print_inverse(text,2,mainLine,false,true,(uint8_t)(slen(text)*4u+3u));
|
||||||
|
if(n==selected && txDenied)
|
||||||
|
A->print_tiny("TX DISABLE",22,(uint8_t)(mainLine*8u+1u),false,true);
|
||||||
|
else {
|
||||||
|
bool useName=false;
|
||||||
|
if(v->channel<MR_MAX&&!showFrequency) useName=copyName(text,v->name);
|
||||||
|
if(!useName) formatFreq(text,v->frequency);
|
||||||
|
if(n==selected)
|
||||||
|
A->print_bold(text,22,0,mainLine);
|
||||||
|
else
|
||||||
|
A->print_normal(text,22,0,mainLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *mod=v->modulation==0u?"FM":v->modulation==1u?"AM":v->modulation==2u?"USB":v->modulation==3u?"BYP":v->modulation==4u?"RAW":"?";
|
||||||
|
A->print_tiny(mod,3,techY,false,true);
|
||||||
|
|
||||||
|
static const char *const power[7]={"LOW1","LOW2","LOW3","LOW4","LOW5","MID","HIGH"};
|
||||||
|
uint8_t p=(v->power>=1u&&v->power<=7u)?(uint8_t)(v->power-1u):0u;
|
||||||
|
A->print_tiny(power[p],24,techY,false,true);
|
||||||
|
if(v->flags&APP_TRIVFO_USER_POWER){
|
||||||
|
A->fb[techLine][19]=0x3e; A->fb[techLine][20]=0x1c; A->fb[techLine][21]=0x08;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(v->offset_direction==1u) A->print_normal("+",41,0,techLine);
|
||||||
|
else if(v->offset_direction==2u) A->print_normal("-",41,0,techLine);
|
||||||
|
if(v->reverse) A->print_tiny("R",51,techY,false,true);
|
||||||
|
|
||||||
|
if(v->code_type==1u){ A->print_tiny("CT",58,techY,false,true); formatCode(text,v); A->print_tiny(text,68,techY,false,true); }
|
||||||
|
else if(v->code_type==2u||v->code_type==3u){ A->print_tiny("DC",58,techY,false,true); formatCode(text,v); A->print_tiny(text,68,techY,false,true); }
|
||||||
|
else { formatStep(text,v->step); A->print_tiny(text,58,techY,false,true); }
|
||||||
|
|
||||||
|
A->print_tiny(v->bandwidth==0u?"WIDE":v->bandwidth==1u?"NAR":"NAR+",91,techY,false,true);
|
||||||
|
text[0]='S'; text[1]='Q'; text[2]='L'; text[3]=(char)('0'+(v->squelch%10u)); text[4]='\0';
|
||||||
|
A->print_tiny(text,110,techY,false,true);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void draw(void){
|
||||||
|
for(uint8_t i=0;i<VFO_COUNT;i++) {
|
||||||
|
A->trivfo_get(i,&vi[i]);
|
||||||
|
/* The resident scanner can temporarily select the VFO carrying the
|
||||||
|
* reception. Mirror that selection locally so the bold main field
|
||||||
|
* follows it immediately. */
|
||||||
|
if(vi[i].flags&APP_TRIVFO_SELECTED) selected=i;
|
||||||
|
}
|
||||||
|
A->display_clear(); A->status_clear();
|
||||||
|
/* Keep the application title in the resident upper-left position. */
|
||||||
|
A->draw_battery();
|
||||||
|
A->print_inverse("TRIPLE VFO",2,0,true,true,42);
|
||||||
|
if(fArm){ for(uint8_t i=0;i<8u;i++) A->status_line[69u+i]=fontF[i]; }
|
||||||
|
for(uint8_t i=0;i<VFO_COUNT;i++) drawVfo(i);
|
||||||
|
if(state==APP_TRIVFO_TX_STATE&&(vi[selected].flags&APP_TRIVFO_AUDIO_BAR))
|
||||||
|
drawAudioScope();
|
||||||
|
else {
|
||||||
|
scopeTx=false;
|
||||||
|
for(uint8_t i=0;i<VFO_COUNT;i++)
|
||||||
|
if(vi[i].flags&APP_TRIVFO_RECEIVING){ drawMeter(&vi[i]); break; }
|
||||||
|
}
|
||||||
|
A->blit_status(); A->blit_full();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void selectNext(void){ selected=(uint8_t)((selected+1u)%VFO_COUNT); A->trivfo_select(selected); }
|
||||||
|
static void toggleNameFrequency(void){ showFrequency=!showFrequency; }
|
||||||
|
static void stepSelected(uint8_t key){ int8_t d=A->nav_dir(key); uint16_t ch=A->trivfo_step(selected,d); if(selected==2u&&ch!=0xFFFFu)cChannel=ch; }
|
||||||
|
|
||||||
|
static void loadCfg(void){ uint8_t c[4]; A->cfg_load(c,4); cChannel=(c[0]==CFG_MAGIC)?(uint16_t)(c[1]|((uint16_t)c[2]<<8)):0xFFFFu; }
|
||||||
|
static void saveCfg(void){ uint8_t c[3]={CFG_MAGIC,(uint8_t)cChannel,(uint8_t)(cChannel>>8)}; A->cfg_save(c,3); }
|
||||||
|
|
||||||
|
__attribute__((section(".text.entry"),used))
|
||||||
|
void app_main(const app_api_t *api){
|
||||||
|
A=api; running=true; fArm=false; txDenied=false; channelLabelOn=true; showFrequency=false; selected=scopeWrite=scopeReady=batteryTicks=0; scopeTx=false; scopeFloor=SCOPE_FLOOR; loadCfg();
|
||||||
|
cChannel=A->trivfo_enter(cChannel); A->trivfo_select(0); A->backlight_on();
|
||||||
|
|
||||||
|
uint8_t held=APP_KEY_INVALID, blinkTicks=0; uint16_t heldMs=0; bool longDone=false, ptt=false;
|
||||||
|
while(running){
|
||||||
|
uint8_t key=A->get_key();
|
||||||
|
if(key==APP_KEY_PTT){
|
||||||
|
if(!ptt){ ptt=true; txDenied=A->trivfo_ptt(true)!=0; }
|
||||||
|
} else if(ptt){ ptt=false; A->trivfo_ptt(false); txDenied=false; }
|
||||||
|
|
||||||
|
if(key==APP_KEY_INVALID||key==APP_KEY_PTT){
|
||||||
|
if(held!=APP_KEY_INVALID&&!longDone){
|
||||||
|
if(held==APP_KEY_F) fArm=!fArm;
|
||||||
|
else if(held==APP_KEY_1&&fArm){ fArm=false; toggleNameFrequency(); }
|
||||||
|
else if(held==APP_KEY_2&&fArm){ fArm=false; selectNext(); }
|
||||||
|
else if(held==APP_KEY_EXIT) running=false;
|
||||||
|
}
|
||||||
|
held=APP_KEY_INVALID; heldMs=0; longDone=false;
|
||||||
|
} else if(key!=held){
|
||||||
|
held=key; heldMs=0; longDone=false;
|
||||||
|
if(key==APP_KEY_UP||key==APP_KEY_DOWN){ stepSelected(key); longDone=true; }
|
||||||
|
} else {
|
||||||
|
heldMs=(uint16_t)(heldMs+TICK_MS);
|
||||||
|
if((key==APP_KEY_UP||key==APP_KEY_DOWN)&&heldMs>=300u){ stepSelected(key); }
|
||||||
|
else if(!longDone&&heldMs>=LONG_MS){
|
||||||
|
longDone=true;
|
||||||
|
/* Long-1 toggles name/frequency and Long-2 selects the next
|
||||||
|
* VFO; both are deliberately independent of F. */
|
||||||
|
if(key==APP_KEY_1){ fArm=false; toggleNameFrequency(); }
|
||||||
|
else if(key==APP_KEY_2){ fArm=false; selectNext(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state=A->trivfo_tick();
|
||||||
|
if(state==APP_TRIVFO_RX){
|
||||||
|
if(++blinkTicks>=25u){ blinkTicks=0; channelLabelOn=!channelLabelOn; }
|
||||||
|
} else { blinkTicks=0; channelLabelOn=true; }
|
||||||
|
draw();
|
||||||
|
/* Match MAIN: no loaded-voltage samples during TX, then allow one
|
||||||
|
* second for the pack to recover before refreshing the four-sample
|
||||||
|
* battery average. */
|
||||||
|
if(state==APP_TRIVFO_TX_STATE) batteryTicks=0;
|
||||||
|
else if(++batteryTicks>=50u){ batteryTicks=0; A->battery_sample(); }
|
||||||
|
A->delay_ms(TICK_MS); A->backlight_update();
|
||||||
|
}
|
||||||
|
if(ptt)
|
||||||
|
A->trivfo_ptt(false);
|
||||||
|
saveCfg();
|
||||||
|
A->trivfo_leave();
|
||||||
|
}
|
||||||
Reference in new issue
Block a user