Merge private multiboot development into feature_update_v6

# Conflicts:
#	App/app/app.c
#	CMakePresets.json
This commit is contained in:
Armel FAUVEAU committed 2026-09-09 21:38:15 +02:00
commit 0aebf26ada
115 files changed
+10759 -2501

No files matched your search

+7 -4
View File
@@ -14,11 +14,14 @@ jobs:
- name: Compile firmware
run: |
chmod +x compile-with-docker.sh
./compile-with-docker.sh
chmod +x compile-firmware.sh compile-app.sh
./compile-firmware.sh All
./compile-app.sh All
- name: Upload firmware artifact
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: firmware-artifact
path: compiled-firmware/f4hwn.packed.bin
path: |
build/*/*.bin
build/Apps/*.app
+8
View File
@@ -17,3 +17,11 @@ __pycache__/
.DS_Store
AGENTS.md
# Overlay-app build artifacts (generated by build.sh / compile-app.sh)
App/apps/*/*.elf
App/apps/*/*.bin
App/apps/*/*.map
App/apps/*/*.app
App/apps/*/*_app_blob.h
build/apps/
+56 -13
View File
@@ -95,8 +95,7 @@ if(ENABLE_AIRCOPY OR ENABLE_UART OR ENABLE_USB)
endif()
enable_feature(ENABLE_UART
driver/uart.c
app/uart.c
driver/uart.c
# driver/aes.c
)
@@ -118,11 +117,17 @@ endif()
# ---- STOCK QUANSHENG FEATURES ----
# ENABLE_FMRADIO keeps the BK1080 driver and the FM data/helpers shared with the
# overlay. ENABLE_FMRADIO_EMBEDDED additionally selects the legacy resident
# state machine and its integration with the main scheduler.
enable_feature(ENABLE_FMRADIO
driver/bk1080.c
app/fm.c
ui/fmradio.c
driver/bk1080.c
app/fm.c
)
if(ENABLE_FMRADIO AND NOT ENABLE_FEAT_F4HWN_OVERLAY_APPS)
target_compile_definitions(App INTERFACE ENABLE_FMRADIO_EMBEDDED)
target_sources(App INTERFACE ui/fmradio.c)
endif()
enable_feature(ENABLE_AIRCOPY
app/aircopy.c
ui/aircopy.c
@@ -132,7 +137,6 @@ enable_feature(ENABLE_VOICE
driver/voice.c
)
enable_feature(ENABLE_VOX)
enable_feature(ENABLE_ALARM)
enable_feature(ENABLE_TX1750)
enable_feature(ENABLE_PWRON_PASSWORD
ui/lock.c
@@ -152,7 +156,6 @@ enable_feature(ENABLE_SMALL_BOLD)
enable_feature(ENABLE_CUSTOM_MENU_LAYOUT)
enable_feature(ENABLE_KEEP_MEM_NAME)
enable_feature(ENABLE_WIDE_RX)
enable_feature(ENABLE_TX_WHEN_AM)
enable_feature(ENABLE_F_CAL_MENU)
enable_feature(ENABLE_CTCSS_TAIL_PHASE_SHIFT)
enable_feature(ENABLE_BOOT_BEEPS)
@@ -167,7 +170,6 @@ enable_feature(ENABLE_FEAT_F4HWN_AUDIO_SCOPE)
enable_feature(ENABLE_COPY_CHAN_TO_VFO)
enable_feature(ENABLE_REDUCE_LOW_MID_TX_POWER)
enable_feature(ENABLE_BYP_RAW_DEMODULATORS)
enable_feature(ENABLE_BLMIN_TMP_OFF)
enable_feature(ENABLE_SCAN_RANGES)
# ---- CONTRIB MODS ----
@@ -177,10 +179,36 @@ enable_feature(ENABLE_EXTRA_UART_CMD)
# ---- F4HWN MODS ----
if(NOT ENABLE_UART AND NOT ENABLE_USB)
if(ENABLE_FEAT_F4HWN_K5VIEWER OR ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER)
message(STATUS "Disabling K5Viewer because both ENABLE_UART and ENABLE_USB are OFF.")
endif()
set(ENABLE_FEAT_F4HWN_K5VIEWER OFF)
set(ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER OFF)
endif()
enable_feature(ENABLE_FEAT_F4HWN)
enable_feature(ENABLE_FEAT_F4HWN_GAME
app/breakout.c
# GAME keeps the F+7 hook; the resident breakout.c is compiled only when the
# overlay does NOT provide Breakout as an app (that is the flash we recover).
if(ENABLE_FEAT_F4HWN_GAME)
target_compile_definitions(App INTERFACE ENABLE_FEAT_F4HWN_GAME)
if(NOT ENABLE_FEAT_F4HWN_OVERLAY_APPS)
target_sources(App INTERFACE app/breakout.c)
endif()
endif()
enable_feature(ENABLE_FEAT_F4HWN_OVERLAY_APPS
apps/app_overlay.c
apps/app_menu.c
)
enable_feature(ENABLE_FEAT_F4HWN_OVERLAY_TRIVFO)
enable_feature(ENABLE_FEAT_F4HWN_OVERLAY_BEAM)
if(ENABLE_FEAT_F4HWN_OVERLAY_APPS AND NOT ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY)
message(FATAL_ERROR "ENABLE_FEAT_F4HWN_OVERLAY_APPS requires ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY (the 4 KiB overlay workspace).")
endif()
if((ENABLE_FEAT_F4HWN_OVERLAY_TRIVFO OR ENABLE_FEAT_F4HWN_OVERLAY_BEAM) AND
NOT ENABLE_FEAT_F4HWN_OVERLAY_APPS)
message(FATAL_ERROR "Overlay Triple VFO/Beam services require ENABLE_FEAT_F4HWN_OVERLAY_APPS.")
endif()
enable_feature(ENABLE_FEAT_F4HWN_K5VIEWER
k5viewer.c
)
@@ -211,9 +239,11 @@ enable_feature(ENABLE_FEAT_F4HWN_BEAM
enable_feature(ENABLE_FEAT_F4HWN_RXTX_LOG
app/rxtx_log.c
)
enable_feature(ENABLE_FEAT_F4HWN_FOXHUNT
app/foxhunt.c
)
enable_feature(ENABLE_FEAT_F4HWN_FOXHUNT)
enable_feature(ENABLE_FEAT_F4HWN_BEACON)
if(ENABLE_FEAT_F4HWN_FOXHUNT OR ENABLE_FEAT_F4HWN_BEACON)
target_sources(App INTERFACE app/foxhunt.c)
endif()
enable_feature(ENABLE_FEAT_F4HWN_ACTION_PICKER)
if(ENABLE_FEAT_F4HWN_ACTION_PICKER AND NOT ENABLE_FEAT_F4HWN)
@@ -230,6 +260,19 @@ endif()
if(ENABLE_FEAT_F4HWN_BEAM AND NOT ENABLE_AIRCOPY)
message(FATAL_ERROR "ENABLE_FEAT_F4HWN_BEAM requires ENABLE_AIRCOPY (it reuses g_FSK_Buffer, AIRCOPY_Obfuscate and the FSK packet plumbing).")
endif()
enable_feature(ENABLE_FEAT_F4HWN_MULTIBOOT
driver/mb_flash.c
ui/multiboot.c
)
if(ENABLE_FEAT_F4HWN_MULTIBOOT AND NOT ENABLE_FEAT_F4HWN)
message(FATAL_ERROR "ENABLE_FEAT_F4HWN_MULTIBOOT requires ENABLE_FEAT_F4HWN (the SetCfg menu, SysInfo badge and multiboot UI all live under ENABLE_FEAT_F4HWN).")
endif()
enable_feature(ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY)
if(ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY AND NOT ENABLE_FEAT_F4HWN_MULTIBOOT)
message(FATAL_ERROR "ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY requires ENABLE_FEAT_F4HWN_MULTIBOOT.")
endif()
enable_feature(ENABLE_FEAT_F4HWN_QRCODE)
enable_feature(ENABLE_FEAT_F4HWN_LOGO)
enable_feature(ENABLE_FEAT_F4HWN_LOGO_SAV)
-397
View File
@@ -1,397 +0,0 @@
/* Copyright 2023 OneOfEleven
* 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.
*/
// code to 'try' and reduce the AM demodulator saturation problem
//
// that is until someone works out how to properly configure the BK chip !
#include <string.h>
#include "am_fix.h"
#include "app/main.h"
#include "board.h"
#include "driver/bk4819.h"
#include "external/printf/printf.h"
#include "frequencies.h"
#include "functions.h"
#include "misc.h"
#include "settings.h"
#ifdef ENABLE_AGC_SHOW_DATA
#include "ui/main.h"
#endif
#ifdef ENABLE_AM_FIX
typedef struct
{
uint16_t reg_val;
int8_t gain_dB;
} __attribute__((packed)) t_gain_table;
// REG_10 AGC gain table
//
// <15:10> ???
//
// <9:8> = LNA Gain Short
// 3 = 0dB < original value
// 2 = -19dB // was -11
// 1 = -24dB // was -16
// 0 = -28dB // was -19
//
// <7:5> = LNA Gain
// 7 = 0dB
// 6 = -2dB
// 5 = -4dB < original value
// 4 = -6dB
// 3 = -9dB
// 2 = -14dB
// 1 = -19dB
// 0 = -24dB
//
// <4:3> = MIXER Gain
// 3 = 0dB < original value
// 2 = -3dB
// 1 = -6dB
// 0 = -8dB
//
// <2:0> = PGA Gain
// 7 = 0dB
// 6 = -3dB < original value
// 5 = -6dB
// 4 = -9dB
// 3 = -15dB
// 2 = -21dB
// 1 = -27dB
// 0 = -33dB
// front end register dB values
//
// these values need to be accurate for the code to properly/reliably switch
// between table entries when adjusting the front end registers.
//
// these 4 tables need a measuring/calibration update
//
//// static const int16_t lna_short_dB[] = { -19, -16, -11, 0}; // was (but wrong)
// static const int16_t lna_short_dB[] = { (-28), (-24), (-19), 0}; // corrected'ish
// static const int16_t lna_dB[] = { (-24), (-19), (-14), ( -9), (-6), (-4), (-2), 0};
// static const int16_t mixer_dB[] = { ( -8), ( -6), ( -3), 0};
// static const int16_t pga_dB[] = { (-33), (-27), (-21), (-15), (-9), (-6), (-3), 0};
// lookup table is hugely easier than writing code to do the same
//
#define LOOKUP_TABLE 1
#if LOOKUP_TABLE
static const t_gain_table gain_table[] =
{
{0x03BE, -7}, // 0 .. 3 5 3 6 .. 0dB -4dB 0dB -3dB .. -7dB original
{0x0000,-93}, // 1 .. 0 0 0 0 .. -28dB -24dB -8dB -33dB .. -93dB
{0x0008,-91}, // 2 .. 0 0 1 0 .. -28dB -24dB -6dB -33dB .. -91dB
{0x0010,-88}, // 3 .. 0 0 2 0 .. -28dB -24dB -3dB -33dB .. -88dB
{0x0001,-87}, // 4 .. 0 0 0 1 .. -28dB -24dB -8dB -27dB .. -87dB
{0x0009,-85}, // 5 .. 0 0 1 1 .. -28dB -24dB -6dB -27dB .. -85dB
{0x0011,-82}, // 6 .. 0 0 2 1 .. -28dB -24dB -3dB -27dB .. -82dB
{0x0002,-81}, // 7 .. 0 0 0 2 .. -28dB -24dB -8dB -21dB .. -81dB
{0x000A,-79}, // 8 .. 0 0 1 2 .. -28dB -24dB -6dB -21dB .. -79dB
{0x0012,-76}, // 9 .. 0 0 2 2 .. -28dB -24dB -3dB -21dB .. -76dB
{0x0003,-75}, // 10 .. 0 0 0 3 .. -28dB -24dB -8dB -15dB .. -75dB
{0x000B,-73}, // 11 .. 0 0 1 3 .. -28dB -24dB -6dB -15dB .. -73dB
{0x0013,-70}, // 12 .. 0 0 2 3 .. -28dB -24dB -3dB -15dB .. -70dB
{0x0004,-69}, // 13 .. 0 0 0 4 .. -28dB -24dB -8dB -9dB .. -69dB
{0x000C,-67}, // 14 .. 0 0 1 4 .. -28dB -24dB -6dB -9dB .. -67dB
{0x000D,-64}, // 15 .. 0 0 1 5 .. -28dB -24dB -6dB -6dB .. -64dB
{0x001C,-61}, // 16 .. 0 0 3 4 .. -28dB -24dB 0dB - 9dB .. -61dB
{0x001D,-58}, // 17 .. 0 0 3 5 .. -28dB -24dB 0dB -6dB .. -58dB
{0x001E,-55}, // 18 .. 0 0 3 6 .. -28dB -24dB 0dB -3dB .. -55dB
{0x001F,-52}, // 19 .. 0 0 3 7 .. -28dB -24dB 0dB 0dB .. -52dB
{0x003E,-50}, // 20 .. 0 1 3 6 .. -28dB -19dB 0dB -3dB .. -50dB
{0x003F,-47}, // 21 .. 0 1 3 7 .. -28dB -19dB 0dB 0dB .. -47dB
{0x005E,-45}, // 22 .. 0 2 3 6 .. -28dB -14dB 0dB -3dB .. -45dB
{0x005F,-42}, // 23 .. 0 2 3 7 .. -28dB -14dB 0dB 0dB .. -42dB
{0x007E,-40}, // 24 .. 0 3 3 6 .. -28dB -9dB 0dB -3dB .. -40dB
{0x007F,-37}, // 25 .. 0 3 3 7 .. -28dB -9dB 0dB 0dB .. -37dB
{0x009F,-34}, // 26 .. 0 4 3 7 .. -28dB -6dB 0dB 0dB .. -34dB
{0x00BF,-32}, // 27 .. 0 5 3 7 .. -28dB -4dB 0dB 0dB .. -32dB
{0x00DF,-30}, // 28 .. 0 6 3 7 .. -28dB -2dB 0dB 0dB .. -30dB
{0x00FF,-28}, // 29 .. 0 7 3 7 .. -28dB 0dB 0dB 0dB .. -28dB
{0x01DF,-26}, // 30 .. 1 6 3 7 .. -24dB -2dB 0dB 0dB .. -26dB
{0x01FF,-24}, // 31 .. 1 7 3 7 .. -24dB 0dB 0dB 0dB .. -24dB
{0x02BF,-23}, // 32 .. 2 5 3 7 .. -19dB -4dB 0dB 0dB .. -23dB
{0x02DF,-21}, // 33 .. 2 6 3 7 .. -19dB -2dB 0dB -0dB .. -21dB
{0x02FF,-19}, // 34 .. 2 7 3 7 .. -19dB 0dB 0dB 0dB .. -19dB
{0x035E,-17}, // 35 .. 3 2 3 6 .. 0dB -14dB 0dB -3dB .. -17dB
{0x035F,-14}, // 36 .. 3 2 3 7 .. 0dB -14dB 0dB 0dB .. -14dB
{0x037E,-12}, // 37 .. 3 3 3 6 .. 0dB -9dB 0dB -3dB .. -12dB
{0x037F,-9}, // 38 .. 3 3 3 7 .. 0dB -9dB 0dB 0dB .. -9dB
{0x038F,-6}, // 39 .. 3 4 3 7 .. 0dB - 6dB 0dB 0dB .. -6dB
{0x03BF,-4}, // 40 .. 3 5 3 7 .. 0dB -4dB 0dB 0dB .. -4dB
{0x03DF,-2}, // 41 .. 3 6 3 7 .. 0dB - 2dB 0dB 0dB .. -2dB
{0x03FF,0} // 42 .. 3 7 3 7 .. 0dB 0dB 0dB 0dB .. 0dB
};
const uint8_t gain_table_size = ARRAY_SIZE(gain_table);
#else
t_gain_table gain_table[100] = {{0x03BE, -7}}; //original
uint8_t gain_table_size = 0;
void CreateTable()
{
typedef union {
struct {
uint8_t pgaIdx:3;
uint8_t mixerIdx:2;
uint8_t lnaIdx:3;
uint8_t lnaSIdx:2;
};
uint16_t __raw;
} GainData;
static const int8_t lna_short_dB[] = {-28, -24, -19, 0}; // corrected'ish
static const int8_t lna_dB[] = {-24, -19, -14, -9, -6, -4, -2, 0};
static const int8_t mixer_dB[] = { -8, -6, -3, 0};
static const int8_t pga_dB[] = {-33, -27, -21, -15, -9, -6, -3, 0};
unsigned i;
for (uint8_t lnaSIdx = 0; lnaSIdx < ARRAY_SIZE(lna_short_dB); lnaSIdx++) {
for (uint8_t lnaIdx = 0; lnaIdx < ARRAY_SIZE(lna_dB); lnaIdx++) {
for (uint8_t mixerIdx = 0; mixerIdx < ARRAY_SIZE(mixer_dB); mixerIdx++) {
for (uint8_t pgaIdx = 0; pgaIdx < ARRAY_SIZE(pga_dB); pgaIdx++) {
int16_t db = lna_short_dB[lnaSIdx] + lna_dB[lnaIdx] + mixer_dB[mixerIdx] + pga_dB[pgaIdx];
GainData gainData = {{
pgaIdx,
mixerIdx,
lnaIdx,
lnaSIdx,
}};
for (i = 1; i < ARRAY_SIZE(gain_table); i++) {
t_gain_table * gain = &gain_table[i];
if (db == gain->gain_dB)
break;
if (db > gain->gain_dB)
continue;
if (db < gain->gain_dB) {
if(gain->gain_dB)
memmove(gain + 1, gain, 100 - i);
gain->gain_dB = db;
gain->reg_val = gainData.__raw;
break;
}
gain->gain_dB = db;
gain->reg_val = gainData.__raw;
break;
}
}
}
}
}
gain_table_size = i+1;
}
#endif
#ifdef ENABLE_AM_FIX_SHOW_DATA
// display update rate
static const unsigned int display_update_rate = 250 / 10; // max 250ms display update rate
unsigned int counter = 0;
#endif
unsigned int gain_table_index[2] = {0, 0};
// used simply to detect a changed gain setting
unsigned int gain_table_index_prev[2] = {0, 0};
// holds the previous RSSI level .. we do an average of old + new RSSI reading
int16_t prev_rssi[2] = {0, 0};
// to help reduce gain hunting, peak hold count down tick
unsigned int hold_counter[2] = {0, 0};
// -89dBm, any higher and the AM demodulator starts to saturate/clip/distort
const int16_t desired_rssi = (-89 + 160) * 2;
int8_t currentGainDiff;
bool enabled = true;
void AM_fix_init(void)
{ // called at boot-up
for (int i = 0; i < 2; i++) {
gain_table_index[i] = 0; // re-start with original QS setting
}
#if !LOOKUP_TABLE
CreateTable();
#endif
}
void AM_fix_reset(const unsigned vfo)
{ // reset the AM fixer upper
if (vfo > 1)
return;
#ifdef ENABLE_AM_FIX_SHOW_DATA
counter = 0;
#endif
prev_rssi[vfo] = 0;
hold_counter[vfo] = 0;
gain_table_index_prev[vfo] = 0;
}
// adjust the RX gain to try and prevent the AM demodulator from
// saturating/overloading/clipping (distorted AM audio)
//
// we're actually doing the BK4819's job for it here, but as the chip
// won't/don't do it for itself, we're left to bodging it ourself by
// playing with the RF front end gain setting
//
void AM_fix_10ms(const unsigned vfo)
{
if(!gSetting_AM_fix || !enabled || vfo > 1 )
return;
if (gCurrentFunction != FUNCTION_FOREGROUND && !FUNCTION_IsRx()) {
#ifdef ENABLE_AM_FIX_SHOW_DATA
counter = display_update_rate; // queue up a display update as soon as we switch to RX mode
#endif
return;
}
#ifdef ENABLE_AM_FIX_SHOW_DATA
if (counter > 0) {
if (++counter >= display_update_rate) { // trigger a display update
counter = 0;
gUpdateDisplay = true;
}
}
#endif
static uint32_t lastFreq[2];
if(gEeprom.VfoInfo[vfo].pRX->Frequency != lastFreq[vfo]) {
lastFreq[vfo] = gEeprom.VfoInfo[vfo].pRX->Frequency;
AM_fix_reset(vfo);
}
int16_t rssi;
{ // sample the current RSSI level
// average it with the previous rssi (a bit of noise/spike immunity)
const int16_t new_rssi = BK4819_GetRSSI();
rssi = (prev_rssi[vfo] > 0) ? (prev_rssi[vfo] + new_rssi) / 2 : new_rssi;
prev_rssi[vfo] = new_rssi;
}
#ifdef ENABLE_AM_FIX_SHOW_DATA
{
static int16_t lastRssi;
if (lastRssi != rssi) { // rssi changed
lastRssi = rssi;
if (counter == 0) {
counter = 1;
gUpdateDisplay = true; // trigger a display update
}
}
}
#endif
// automatically adjust the RF RX gain
// update the gain hold counter
if (hold_counter[vfo] > 0)
hold_counter[vfo]--;
// dB difference between actual and desired RSSI level
int16_t diff_dB = (rssi - desired_rssi) / 2;
if (diff_dB > 0) { // decrease gain
unsigned int index = gain_table_index[vfo]; // current position we're at
if (diff_dB >= 10) { // jump immediately to a new gain setting
// this greatly speeds up initial gain reduction (but reduces noise/spike immunity)
const int16_t desired_gain_dB = (int16_t)gain_table[index].gain_dB - diff_dB + 8; // get no closer than 8dB (bit of noise/spike immunity)
// scan the table to see what index to jump straight too
while (index > 1)
if (gain_table[--index].gain_dB <= desired_gain_dB)
break;
}
else
{ // incrementally reduce the gain .. taking it slow improves noise/spike immunity
if (index > 1)
index--; // slow step-by-step gain reduction
}
index = MAX(1u, index);
if (gain_table_index[vfo] != index)
{
gain_table_index[vfo] = index;
hold_counter[vfo] = 30; // 300ms hold
}
}
if (diff_dB >= -6) // 6dB hysterisis (help reduce gain hunting)
hold_counter[vfo] = 30; // 300ms hold
if (hold_counter[vfo] == 0)
{ // hold has been released, we're free to increase gain
const unsigned int index = gain_table_index[vfo] + 1; // move up to next gain index
gain_table_index[vfo] = MIN(index, gain_table_size - 1u);
}
{ // apply the new settings to the front end registers
const unsigned int index = gain_table_index[vfo];
// remember the new table index
gain_table_index_prev[vfo] = index;
currentGainDiff = gain_table[0].gain_dB - gain_table[index].gain_dB;
BK4819_WriteRegister(BK4819_REG_13, gain_table[index].reg_val);
#ifdef ENABLE_AGC_SHOW_DATA
UI_MAIN_PrintAGC(true);
#endif
}
#ifdef ENABLE_AM_FIX_SHOW_DATA
if (counter == 0) {
counter = 1;
gUpdateDisplay = true;
}
#endif
}
#ifdef ENABLE_AM_FIX_SHOW_DATA
void AM_fix_print_data(const unsigned vfo, char *s) {
if (s != NULL && vfo < ARRAY_SIZE(gain_table_index)) {
const unsigned int index = gain_table_index[vfo];
sprintf(s, "%2u %4ddB %3u", index, gain_table[index].gain_dB, prev_rssi[vfo]);
counter = 0;
}
}
#endif
int8_t AM_fix_get_gain_diff()
{
return currentGainDiff;
}
void AM_fix_enable(bool on)
{
enabled = on;
}
#endif
+119 -92
View File
@@ -25,12 +25,15 @@
#ifdef ENABLE_FLASHLIGHT
#include "app/flashlight.h"
#endif
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
#include "app/fm.h"
#endif
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_APPS
#include "apps/app_overlay.h"
#endif
#include "app/scanner.h"
#include "audio.h"
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
#include "driver/bk1080.h"
#endif
#include "driver/bk4819.h"
@@ -48,26 +51,28 @@
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
#include "app/rxtx_log.h"
#endif
#ifdef ENABLE_FEAT_F4HWN_FOXHUNT
#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
#include "app/foxhunt.h"
#endif
#ifdef ENABLE_FEAT_F4HWN_ACTION_PICKER
#include "ui/menu.h"
#endif
#if defined(ENABLE_FMRADIO)
#if defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) && !defined(ENABLE_FEAT_F4HWN_BEAM)
static void ACTION_Beam(void);
#endif
#if defined(ENABLE_FMRADIO_EMBEDDED)
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); };
#ifdef ENABLE_TX1750
static void ACTION_1750(void);
#endif
inline static void ACTION_ScanRestart() { ACTION_Scan(true); };
void (*const action_opt_table[])(void) = {
void (*const action_opt_table[ACTION_OPT_LEN])(void) = {
[ACTION_OPT_NONE] = &FUNCTION_NOP,
[ACTION_OPT_POWER] = &ACTION_Power,
[ACTION_OPT_MONITOR] = &ACTION_Monitor,
@@ -79,38 +84,18 @@ void (*const action_opt_table[])(void) = {
#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_FEAT_F4HWN
@@ -118,7 +103,6 @@ void (*const action_opt_table[])(void) = {
[ACTION_OPT_MAINONLY] = &ACTION_MainOnly,
[ACTION_OPT_PTT] = &ACTION_Ptt,
[ACTION_OPT_WN] = &ACTION_Wn,
[ACTION_OPT_BACKLIGHT] = &ACTION_BackLight,
//#if !defined(ENABLE_SPECTRUM) || !defined(ENABLE_FMRADIO)
[ACTION_OPT_MUTE] = &ACTION_Mute,
//#else
@@ -126,29 +110,65 @@ void (*const action_opt_table[])(void) = {
//#endif
#ifdef ENABLE_FEAT_F4HWN_AUDIO
[ACTION_OPT_RXA] = &ACTION_RxA,
#else
[ACTION_OPT_RXA] = &FUNCTION_NOP,
#endif
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
[ACTION_OPT_POWER_HIGH] = &ACTION_Power_High,
[ACTION_OPT_REMOVE_OFFSET] = &ACTION_Remove_Offset,
#endif
#else
[ACTION_OPT_RXMODE] = &FUNCTION_NOP,
#endif
#ifdef ENABLE_FEAT_F4HWN_BEAM
#if defined(ENABLE_FEAT_F4HWN_BEAM) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
[ACTION_OPT_BEAM] = &ACTION_Beam,
#endif
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
[ACTION_OPT_RXTX_LOG] = &ACTION_RxTxLog,
#endif
#ifdef ENABLE_FEAT_F4HWN_FOXHUNT
#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
[ACTION_OPT_FOXHUNT] = &ACTION_FoxHunt,
#endif
#if defined(ENABLE_FEAT_F4HWN_BEACON) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
[ACTION_OPT_BEACON] = &ACTION_Beacon,
#endif
};
static_assert(ARRAY_SIZE(action_opt_table) == ACTION_OPT_LEN);
static_assert(ACTION_OPT_RXTX_LOG == 18);
static_assert(ACTION_OPT_BEAM == 19);
static_assert(ACTION_OPT_POWER_HIGH == 20);
static_assert(ACTION_OPT_REMOVE_OFFSET == 21);
static_assert(ACTION_OPT_FOXHUNT == 22);
static_assert(ACTION_OPT_BEACON == 23);
bool ACTION_IsAvailable(uint8_t action)
{
if (action >= ACTION_OPT_LEN || action_opt_table[action] == NULL)
return false;
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_APPS
switch (action) {
#ifdef ENABLE_FMRADIO
case ACTION_OPT_FM:
return (APP_OverlayShortcutMask() & APP_SHORTCUT_FM) != 0;
#endif
#ifndef ENABLE_FEAT_F4HWN_FOXHUNT
case ACTION_OPT_FOXHUNT:
return (APP_OverlayShortcutMask() & APP_SHORTCUT_FOXHUNT) != 0;
#endif
#ifndef ENABLE_FEAT_F4HWN_BEACON
case ACTION_OPT_BEACON:
return (APP_OverlayShortcutMask() & APP_SHORTCUT_BEACON) != 0;
#endif
#ifndef ENABLE_FEAT_F4HWN_BEAM
case ACTION_OPT_BEAM:
return (APP_OverlayShortcutMask() & APP_SHORTCUT_BEAM) != 0;
#endif
default:
break;
}
#endif
return true;
}
void ACTION_Power(void)
{
@@ -195,7 +215,7 @@ void ACTION_Monitor(void)
RADIO_SetupRegisters(true);
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
if (gFmRadioMode) {
FM_Start();
gRequestDisplayScreen = DISPLAY_FM;
@@ -209,7 +229,7 @@ void ACTION_Scan(bool bRestart)
{
(void)bRestart;
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
if (gFmRadioMode) {
ACTION_Scan_FM(bRestart);
return;
@@ -302,7 +322,7 @@ void ACTION_SwitchDemodul(void)
}
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
inline static bool ACTION_IsBlockedInFM(uint8_t action)
{
switch (action) {
@@ -326,11 +346,14 @@ inline static bool ACTION_IsBlockedInFM(uint8_t action)
case ACTION_OPT_REMOVE_OFFSET:
#endif
#endif
#ifdef ENABLE_FEAT_F4HWN_BEAM
#if defined(ENABLE_FEAT_F4HWN_BEAM) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
case ACTION_OPT_BEAM:
#endif
#ifdef ENABLE_FEAT_F4HWN_FOXHUNT
#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
case ACTION_OPT_FOXHUNT:
#endif
#if defined(ENABLE_FEAT_F4HWN_BEACON) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
case ACTION_OPT_BEACON:
#endif
return true;
@@ -340,15 +363,14 @@ inline static bool ACTION_IsBlockedInFM(uint8_t action)
}
#endif
#ifdef ENABLE_FEAT_F4HWN_ACTION_PICKER
static void ACTION_Execute(uint8_t action)
{
if (action >= ACTION_OPT_LEN || action_opt_table[action] == NULL) {
if (!ACTION_IsAvailable(action)) {
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
return;
}
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
if (gFmRadioMode && ACTION_IsBlockedInFM(action)) {
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
return;
@@ -359,6 +381,7 @@ static void ACTION_Execute(uint8_t action)
action_opt_table[action]();
}
#ifdef ENABLE_FEAT_F4HWN_ACTION_PICKER
uint8_t gActionPickerKey;
uint8_t gActionPickerSelection[2] = {1, 1};
uint8_t gActionPickerTimeout_500ms;
@@ -477,26 +500,52 @@ void ACTION_Handle(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
}
// held or released after short press
#ifdef ENABLE_FEAT_F4HWN_ACTION_PICKER
ACTION_Execute(func);
#else
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
#ifdef ENABLE_FMRADIO
if (gFmRadioMode && ACTION_IsBlockedInFM(func)) {
}
#if defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) && !defined(ENABLE_FEAT_F4HWN_BEAM)
static void ACTION_Beam(void)
{
if (APP_LaunchOverlayShortcut(APP_SHORTCUT_BEAM) != APP_OK)
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
return;
}
}
#endif
action_opt_table[func]();
#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
void ACTION_FoxHunt(void)
{
#ifdef ENABLE_FEAT_F4HWN_FOXHUNT
APP_RunFoxHunt();
GUI_SelectNextDisplay(DISPLAY_MAIN);
#else
if (APP_LaunchOverlayShortcut(APP_SHORTCUT_FOXHUNT) != APP_OK)
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
#endif
}
#endif
#if defined(ENABLE_FEAT_F4HWN_BEACON) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
void ACTION_Beacon(void)
{
#ifdef ENABLE_FEAT_F4HWN_BEACON
APP_RunBeacon();
GUI_SelectNextDisplay(DISPLAY_MAIN);
#else
if (APP_LaunchOverlayShortcut(APP_SHORTCUT_BEACON) != APP_OK)
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
#endif
}
#endif
#ifdef ENABLE_FMRADIO
void ACTION_FM(void)
{
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_APPS
if (APP_LaunchOverlayShortcut(APP_SHORTCUT_FM) != APP_OK)
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
return;
#else
if (gCurrentFunction != FUNCTION_TRANSMIT && gCurrentFunction != FUNCTION_MONITOR)
{
gInputBoxIndex = 0;
@@ -512,6 +561,14 @@ void ACTION_FM(void)
return;
}
// Do not start broadcast FM while a VFO reception is already active.
// Keeping this check after the block above ensures EXIT can still
// turn FM off if the UI ever reaches DISPLAY_MAIN with FM mode active.
if (FUNCTION_IsRx()) {
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
return;
}
gMonitor = false;
if (gScanStateDir != SCAN_OFF) {
@@ -527,8 +584,10 @@ void ACTION_FM(void)
gRequestDisplayScreen = DISPLAY_FM;
}
#endif /* !ENABLE_FEAT_F4HWN_OVERLAY_APPS */
}
#ifdef ENABLE_FMRADIO_EMBEDDED
static void ACTION_Scan_FM(bool bRestart)
{
if (FUNCTION_IsRx())
@@ -568,40 +627,24 @@ static void ACTION_Scan_FM(bool bRestart)
#endif
}
#endif
#endif
#if defined(ENABLE_ALARM) || defined(ENABLE_TX1750)
static void ACTION_AlarmOr1750(const bool b1750)
#ifdef ENABLE_TX1750
static void ACTION_1750(void)
{
if(gEeprom.KEY_LOCK && (gSetting_set_lck & SET_LCK_PTT))
return;
#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;
gTx1750Active = true;
gInputBoxIndex = 0;
gFlagPrepareTX = gAlarmState != ALARM_STATE_OFF;
gFlagPrepareTX = true;
if (gScreenToDisplay != DISPLAY_MENU) // 1of11 .. don't close the menu
gRequestDisplayScreen = DISPLAY_MAIN;
}
#endif
#ifdef ENABLE_VOX
@@ -618,18 +661,6 @@ void ACTION_Vox(void)
}
#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
#ifdef ENABLE_FEAT_F4HWN
void ACTION_Update(void)
{
@@ -714,11 +745,7 @@ void ACTION_Wn(void)
}
#endif
#ifdef ENABLE_AM_FIX
BK4819_SetFilterBandwidth(bw, true);
#else
BK4819_SetFilterBandwidth(bw, false);
#endif
BK4819_SetFilterBandwidth(bw, false);
}
void ACTION_BackLight(void)
@@ -760,7 +787,7 @@ void ACTION_Mute(void)
gMute = !gMute;
// Update the registers
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
BK1080_WriteRegister(BK1080_REG_05_SYSTEM_CONFIGURATION2, gMute ? 0x0A10 : 0x0A1F);
#endif
gEeprom.VOLUME_GAIN = gMute ? 0 : gEeprom.VOLUME_GAIN_BACKUP;
+4 -4
View File
@@ -17,6 +17,9 @@
#ifndef APP_ACTION_H
#define APP_ACTION_H
#include <stdbool.h>
#include <stdint.h>
#include "driver/keyboard.h"
void ACTION_Power(void);
@@ -31,10 +34,6 @@ void ACTION_Scan(bool bRestart);
#endif
void ACTION_SwitchDemodul(void);
#ifdef ENABLE_BLMIN_TMP_OFF
void ACTION_BlminTmpOff(void);
#endif
#ifdef ENABLE_FEAT_F4HWN
void ACTION_RxMode(void);
void ACTION_MainOnly(void);
@@ -62,6 +61,7 @@ extern uint8_t gActionPickerSelection[2];
extern uint8_t gActionPickerTimeout_500ms;
bool ACTION_PickerProcessKey(KEY_Code_t key, bool isPressed, bool isHeld);
#endif
bool ACTION_IsAvailable(uint8_t action);
void ACTION_Handle(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld);
#endif
+254 -201
View File
@@ -21,6 +21,7 @@
#include "driver/bk4819.h"
#include "driver/crc.h"
#include "driver/eeprom.h"
#include "driver/system.h"
#include "frequencies.h"
#include "misc.h"
#include "radio.h"
@@ -29,6 +30,7 @@
#include "ui/ui.h"
#include "settings.h"
#include <stddef.h>
#include <string.h>
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
#include "k5viewer.h"
@@ -40,141 +42,104 @@ AIRCOPY_State_t gAircopyState;
uint16_t gAirCopyBlockNumber;
uint16_t gErrorsDuringAirCopy;
bool gAirCopyIsSendMode;
bool gAircopyAll;
uint16_t g_FSK_Buffer[36];
// ============================================================================
// Transfer Maps Definition
// ============================================================================
// Stop-and-wait protocol. Every frame keeps the original 64-byte payload:
// DATA is acknowledged only after storage, ACK confirms that offset, and
// RESEND requests the same offset immediately instead of waiting for timeout.
#define AIRCOPY_PACKET_DATA 0xABCDu
#define AIRCOPY_PACKET_ACK 0xABCEu
#define AIRCOPY_PACKET_RESEND 0xABCFu
#define AIRCOPY_PACKET_END 0xDCBAu
#define AIRCOPY_ACK_TIMEOUT_10MS 400u
#define AIRCOPY_RX_TIMEOUT_10MS 2000u
#define AIRCOPY_RX_LINGER_10MS 500u
#define AIRCOPY_MAX_RETRIES 3u
#define AIRCOPY_BANK_SEGMENTS(bank) \
{ \
{ 0x0000 + (bank)*0x0800, 0x0000 + (bank)*0x0800 + 0x0800, AIRCOPY_WRITE_STRUCT }, \
{ 0x4000 + (bank)*0x0800, 0x4000 + (bank)*0x0800 + 0x0800, AIRCOPY_WRITE_STRUCT }, \
{ 0x8000 + (bank)*0x0100, 0x8000 + (bank)*0x0100 + 0x0100, AIRCOPY_WRITE_BYTES }, \
}
static uint16_t AircopyCountdown;
static uint8_t AircopyRetries;
#define AIRCOPY_STD_MAP(seg_array) \
{ \
.segments = seg_array, \
.num_segments = 3, \
.total_blocks = 68 \
}
// total_blocks = 68 (blocs of 64 bytes) because (16 bytes + 16 bytes + 2 bytes) * 128 = 4352 / 64 = 68
#define DECLARE_AIRCOPY_BANK(n) \
static const AIRCOPY_Segment_t AIRCOPY_Segments_Bank##n[] = \
AIRCOPY_BANK_SEGMENTS(n); \
\
static const AIRCOPY_TransferMap_t AIRCOPY_Map_Bank##n = \
AIRCOPY_STD_MAP(AIRCOPY_Segments_Bank##n);
DECLARE_AIRCOPY_BANK(0)
DECLARE_AIRCOPY_BANK(1)
#if AIRCOPY_NUM_BANKS >= 4 // if 512 MR CHANNEL
DECLARE_AIRCOPY_BANK(2)
DECLARE_AIRCOPY_BANK(3)
#endif
#if AIRCOPY_NUM_BANKS >= 6 // if 758 MR CHANNEL
DECLARE_AIRCOPY_BANK(4)
DECLARE_AIRCOPY_BANK(5)
#endif
#if AIRCOPY_NUM_BANKS >= 8 // if 1024 MR CHANNEL
DECLARE_AIRCOPY_BANK(6)
DECLARE_AIRCOPY_BANK(7)
#endif
// For settings only
static const AIRCOPY_Segment_t AIRCOPY_Segments_Settings[] = {
{ 0xA000, 0xA170, AIRCOPY_WRITE_BYTES },
{ 0x880E, 0x886E, AIRCOPY_WRITE_BYTES },
{ 0x9000, 0x90E5, AIRCOPY_WRITE_BYTES }, // VFO area (full 14 VFOs 0x9000..0x90E0) +
// Fox Hunt settings tail 0x90E0..0x90E5
};
// total_blocks = ceil(0x170/64) + ceil(0x60/64) + ceil(0xE5/64) = 6 + 2 + 4 = 12
static const AIRCOPY_TransferMap_t AIRCOPY_Map_Settings = {
.segments = AIRCOPY_Segments_Settings,
.num_segments = 3,
.total_blocks = 12
};
// Finally
static const AIRCOPY_TransferMap_t *AIRCOPY_AvailableMaps[] = {
&AIRCOPY_Map_Bank0,
&AIRCOPY_Map_Bank1,
#if AIRCOPY_NUM_BANKS >= 4 // if 512 MR CHANNEL
&AIRCOPY_Map_Bank2,
&AIRCOPY_Map_Bank3,
#endif
#if AIRCOPY_NUM_BANKS >= 6 // if 758 MR CHANNEL
&AIRCOPY_Map_Bank4,
&AIRCOPY_Map_Bank5,
#endif
#if AIRCOPY_NUM_BANKS >= 8 // if 1024 MR CHANNEL
&AIRCOPY_Map_Bank6,
&AIRCOPY_Map_Bank7,
#endif
&AIRCOPY_Map_Settings,
};
#define AIRCOPY_NUM_MAPS (sizeof(AIRCOPY_AvailableMaps) / sizeof(AIRCOPY_AvailableMaps[0]))
#define AIRCOPY_BANK_BLOCKS 68u
#define AIRCOPY_SETTINGS_BLOCKS 12u
#define AIRCOPY_ALL_BLOCKS (AIRCOPY_NUM_BANKS * AIRCOPY_BANK_BLOCKS + AIRCOPY_SETTINGS_BLOCKS)
// ============================================================================
// Helper Functions
// ============================================================================
const AIRCOPY_TransferMap_t* AIRCOPY_GetCurrentMap(void)
uint16_t AIRCOPY_GetTotalBlocks(void)
{
if (gAircopyCurrentMapIndex >= AIRCOPY_NUM_MAPS) {
gAircopyCurrentMapIndex = 0;
if (gAircopyAll)
return AIRCOPY_ALL_BLOCKS; // banks + settings, one continuous run
return gAircopyCurrentMapIndex == AIRCOPY_NUM_BANKS
? AIRCOPY_SETTINGS_BLOCKS
: AIRCOPY_BANK_BLOCKS;
}
// Resolve the map that a (possibly global, in All mode) block index lands in.
// On return, *block is rewritten to the block index within that map.
static uint8_t AIRCOPY_ResolveMap(uint16_t *block)
{
if (!gAircopyAll)
return gAircopyCurrentMapIndex;
uint8_t map = 0;
while (map < AIRCOPY_NUM_BANKS && *block >= AIRCOPY_BANK_BLOCKS)
{
*block -= AIRCOPY_BANK_BLOCKS;
map++;
}
return AIRCOPY_AvailableMaps[gAircopyCurrentMapIndex];
return map; // AIRCOPY_NUM_BANKS once the banks are exhausted (settings map)
}
// Map index of the block currently in progress, for the All-mode slice label.
uint8_t AIRCOPY_CurrentSliceMap(void)
{
uint16_t block = gAirCopyBlockNumber;
return AIRCOPY_ResolveMap(&block);
}
static uint16_t AIRCOPY_GetBlockOffset(uint16_t block)
{
const uint8_t map = AIRCOPY_ResolveMap(&block);
if (map == AIRCOPY_NUM_BANKS)
{
// Settings: 6 blocks at 0xA000, 2 at 0x880E and 4 at 0x9000.
if (block < 6u)
return 0xA000u + block * AIRCOPY_BLOCK_SIZE;
if (block < 8u)
return 0x880Eu + (block - 6u) * AIRCOPY_BLOCK_SIZE;
return 0x9000u + (block - 8u) * AIRCOPY_BLOCK_SIZE;
}
// A bank contains 32 frequency, 32 name and 4 attribute blocks.
const uint16_t channelOffset = map * 0x0800u;
if (block < 32u)
return channelOffset + block * AIRCOPY_BLOCK_SIZE;
if (block < 64u)
return 0x4000u + channelOffset + (block - 32u) * AIRCOPY_BLOCK_SIZE;
return 0x8000u + map * 0x0100u
+ (block - 64u) * AIRCOPY_BLOCK_SIZE;
}
static void AIRCOPY_clear()
{
for (uint8_t i = 0; i < 15; i++)
{
crc[i] = 0;
}
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
K5VIEWER_Update(true);
#endif
}
static inline const AIRCOPY_Segment_t *AIRCOPY_FindSegmentForOffset(uint16_t off)
static void AIRCOPY_Finish(AIRCOPY_State_t state)
{
const AIRCOPY_TransferMap_t *map = AIRCOPY_GetCurrentMap();
for (uint16_t i = 0; i < map->num_segments; i++)
{
const AIRCOPY_Segment_t *seg = &map->segments[i];
if (off >= seg->start_offset && off < seg->end_offset)
return seg;
}
return NULL;
}
static inline void AIRCOPY_CheckComplete(uint16_t *num)
{
*num = *num + 1;
const AIRCOPY_TransferMap_t *map = AIRCOPY_GetCurrentMap();
uint16_t done = gAirCopyBlockNumber + gErrorsDuringAirCopy;
if (done >= map->total_blocks)
{
gAircopyState = AIRCOPY_COMPLETE;
AircopyCountdown = 0;
gAircopyState = state;
gUpdateDisplay = true;
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
K5VIEWER_Update(false);
K5VIEWER_Update(false);
#endif
}
}
void AIRCOPY_Obfuscate(unsigned int count)
@@ -184,70 +149,97 @@ void AIRCOPY_Obfuscate(unsigned int count)
}
}
static void AIRCOPY_TransmitBuffer(void)
{
// Both sides need time to leave TX and re-arm FSK RX before the reply.
SYSTEM_DelayMs(50);
RADIO_SetTxParameters();
BK4819_SendFSKData(g_FSK_Buffer);
BK4819_SetupPowerAmplifier(0, 0);
BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, false);
}
static void AIRCOPY_FinalizeAndSend(void)
{
g_FSK_Buffer[34] = CRC_Calculate(&g_FSK_Buffer[0],
4 + AIRCOPY_BLOCK_SIZE);
g_FSK_Buffer[35] = AIRCOPY_PACKET_END;
AIRCOPY_Obfuscate(34);
AIRCOPY_TransmitBuffer();
gFSKWriteIndex = 0;
BK4819_PrepareFSKReceive();
}
static void AIRCOPY_SendControl(uint16_t type, uint16_t offset)
{
g_FSK_Buffer[0] = type;
g_FSK_Buffer[1] = offset;
memset(&g_FSK_Buffer[2], 0, AIRCOPY_BLOCK_SIZE);
AIRCOPY_FinalizeAndSend();
}
static void AIRCOPY_RequestResend(void)
{
gErrorsDuringAirCopy++;
gUpdateDisplay = true;
AircopyCountdown = AIRCOPY_RX_TIMEOUT_10MS;
AIRCOPY_SendControl(AIRCOPY_PACKET_RESEND,
AIRCOPY_GetBlockOffset(gAirCopyBlockNumber));
}
static bool AIRCOPY_Retry(void)
{
if (AircopyRetries >= AIRCOPY_MAX_RETRIES)
{
AIRCOPY_Finish(AIRCOPY_FAILED);
return false;
}
AircopyRetries++;
gErrorsDuringAirCopy++;
gUpdateDisplay = true;
AircopyCountdown = 0;
return true;
}
// ============================================================================
// Send/Receive Functions
// ============================================================================
bool AIRCOPY_SendMessage(void)
{
static uint8_t gAircopySendCountdown = 1;
static uint16_t CurrentOffset = 0;
static uint16_t CurrentSegmentIndex = 0;
if (gAircopyState != AIRCOPY_TRANSFER) {
return 1;
}
if (--gAircopySendCountdown) {
if (!gAirCopyIsSendMode)
{
if (AircopyCountdown != 0 && --AircopyCountdown == 0)
{
AIRCOPY_Finish(gAirCopyBlockNumber >= AIRCOPY_GetTotalBlocks()
? AIRCOPY_COMPLETE
: AIRCOPY_FAILED);
return 0;
}
return 1;
}
const AIRCOPY_TransferMap_t *map = AIRCOPY_GetCurrentMap();
// Initialize on first call
if (gAirCopyBlockNumber == 0) {
CurrentSegmentIndex = 0;
CurrentOffset = map->segments[0].start_offset;
}
// Advance to next segment if current is done
while (CurrentSegmentIndex < map->num_segments &&
CurrentOffset >= map->segments[CurrentSegmentIndex].end_offset)
if (AircopyCountdown != 0)
{
CurrentSegmentIndex++;
if (CurrentSegmentIndex < map->num_segments) {
CurrentOffset = map->segments[CurrentSegmentIndex].start_offset;
}
if (--AircopyCountdown != 0)
return 1;
if (!AIRCOPY_Retry())
return 0;
}
// Check if transfer is complete
if (CurrentSegmentIndex >= map->num_segments) {
gAircopyState = AIRCOPY_COMPLETE;
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
K5VIEWER_Update(false);
#endif
return 0;
}
const uint16_t currentOffset = AIRCOPY_GetBlockOffset(gAirCopyBlockNumber);
g_FSK_Buffer[0] = AIRCOPY_PACKET_DATA;
g_FSK_Buffer[1] = currentOffset;
EEPROM_ReadBuffer(currentOffset, &g_FSK_Buffer[2], AIRCOPY_BLOCK_SIZE);
AIRCOPY_FinalizeAndSend();
AircopyCountdown = AIRCOPY_ACK_TIMEOUT_10MS;
// Send data from current offset
g_FSK_Buffer[1] = CurrentOffset;
EEPROM_ReadBuffer(CurrentOffset, &g_FSK_Buffer[2], 64);
g_FSK_Buffer[34] = CRC_Calculate(&g_FSK_Buffer[1], 2 + 64);
AIRCOPY_Obfuscate(34);
RADIO_SetTxParameters();
BK4819_SendFSKData(g_FSK_Buffer);
BK4819_SetupPowerAmplifier(0, 0);
BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, false);
CurrentOffset += 64;
gAirCopyBlockNumber++;
gAircopySendCountdown = 30;
return 0;
return 1;
}
void AIRCOPY_StorePacket(void)
@@ -257,53 +249,112 @@ void AIRCOPY_StorePacket(void)
}
gFSKWriteIndex = 0;
gUpdateDisplay = true;
uint16_t Status = BK4819_ReadRegister(BK4819_REG_0B);
BK4819_PrepareFSKReceive();
const uint16_t status = BK4819_ReadRegister(BK4819_REG_0B);
const uint16_t type = g_FSK_Buffer[0];
bool valid = (status & 0x0010u) == 0 &&
(type == AIRCOPY_PACKET_DATA ||
type == AIRCOPY_PACKET_ACK ||
type == AIRCOPY_PACKET_RESEND) &&
g_FSK_Buffer[35] == AIRCOPY_PACKET_END;
if ((Status & 0x0010U) != 0 || g_FSK_Buffer[0] != 0xABCD || g_FSK_Buffer[35] != 0xDCBA) {
BK4819_ResetFSK(); // <- important
BK4819_PrepareFSKReceive(); // <- re-arm proprement
AIRCOPY_CheckComplete(&gErrorsDuringAirCopy);
return;
}
AIRCOPY_Obfuscate(34);
uint16_t Crc = CRC_Calculate(&g_FSK_Buffer[1], 2 + 64);
if (g_FSK_Buffer[34] != Crc) {
AIRCOPY_CheckComplete(&gErrorsDuringAirCopy);
return;
}
uint16_t Offset = g_FSK_Buffer[1];
const AIRCOPY_Segment_t *seg = AIRCOPY_FindSegmentForOffset(Offset);
if (seg == NULL) {
AIRCOPY_CheckComplete(&gErrorsDuringAirCopy);
return;
}
const uint8_t *pData = (const uint8_t *)&g_FSK_Buffer[2];
for (unsigned int i = 0; i < 8; i++)
if (valid)
{
EEPROM_WriteBuffer(Offset + (i * 8), pData + (i * 8));
AIRCOPY_Obfuscate(34);
valid = g_FSK_Buffer[34] ==
CRC_Calculate(&g_FSK_Buffer[0], 4 + AIRCOPY_BLOCK_SIZE);
}
AIRCOPY_CheckComplete(&gAirCopyBlockNumber);
if (gAirCopyIsSendMode)
{
if (!valid)
{
BK4819_PrepareFSKReceive();
return;
}
const uint16_t offset = g_FSK_Buffer[1];
const uint16_t currentOffset = AIRCOPY_GetBlockOffset(gAirCopyBlockNumber);
if (type == AIRCOPY_PACKET_ACK && offset == currentOffset)
{
AircopyCountdown = 0;
AircopyRetries = 0;
gAirCopyBlockNumber++;
gUpdateDisplay = true;
if (gAirCopyBlockNumber >= AIRCOPY_GetTotalBlocks())
AIRCOPY_Finish(AIRCOPY_COMPLETE);
return;
}
if (type == AIRCOPY_PACKET_RESEND && offset == currentOffset)
{
(void)AIRCOPY_Retry();
return;
}
BK4819_PrepareFSKReceive();
return;
}
if (!valid)
{
if (type == AIRCOPY_PACKET_DATA)
AIRCOPY_RequestResend();
else
BK4819_PrepareFSKReceive();
return;
}
if (type != AIRCOPY_PACKET_DATA)
{
BK4819_PrepareFSKReceive();
return;
}
const uint16_t offset = g_FSK_Buffer[1];
if (gAirCopyBlockNumber != 0u &&
offset == AIRCOPY_GetBlockOffset(gAirCopyBlockNumber - 1u))
{
AircopyCountdown = gAirCopyBlockNumber >= AIRCOPY_GetTotalBlocks()
? AIRCOPY_RX_LINGER_10MS
: AIRCOPY_RX_TIMEOUT_10MS;
AIRCOPY_SendControl(AIRCOPY_PACKET_ACK, offset);
return;
}
if (offset != AIRCOPY_GetBlockOffset(gAirCopyBlockNumber))
{
AIRCOPY_RequestResend();
return;
}
EEPROM_WriteBuffer(offset, &g_FSK_Buffer[2], AIRCOPY_BLOCK_SIZE);
// All pending RX errors concerned this stop-and-wait block.
gErrorsDuringAirCopy = 0;
gAirCopyBlockNumber++;
gUpdateDisplay = true;
AircopyCountdown = gAirCopyBlockNumber < AIRCOPY_GetTotalBlocks()
? AIRCOPY_RX_TIMEOUT_10MS
: AIRCOPY_RX_LINGER_10MS;
AIRCOPY_SendControl(AIRCOPY_PACKET_ACK, offset);
}
static void AIRCOPY_InitTransfer(bool isSendMode)
{
gAircopyStep = 1;
if (gAircopyCurrentMapIndex > AIRCOPY_ALL_INDEX)
gAircopyCurrentMapIndex = 0;
gAircopyAll = (gAircopyCurrentMapIndex == AIRCOPY_ALL_INDEX);
gFSKWriteIndex = 0;
gAirCopyBlockNumber = 0;
gErrorsDuringAirCopy = 0;
gInputBoxIndex = 0;
gAirCopyIsSendMode = isSendMode;
AircopyCountdown = isSendMode ? 0 : AIRCOPY_RX_TIMEOUT_10MS;
AircopyRetries = 0;
AIRCOPY_clear();
gAircopyState = AIRCOPY_TRANSFER;
@@ -357,8 +408,6 @@ static void AIRCOPY_Key_EXIT()
{
if (gInputBoxIndex == 0) {
AIRCOPY_InitTransfer(0); // Mode: Receive
gErrorsDuringAirCopy = lErrorsDuringAirCopy = 0;
BK4819_PrepareFSKReceive();
} else {
@@ -369,10 +418,6 @@ static void AIRCOPY_Key_EXIT()
static void AIRCOPY_Key_MENU()
{
AIRCOPY_InitTransfer(1); // Mode: Send
g_FSK_Buffer[0] = 0xABCD;
g_FSK_Buffer[1] = 0;
g_FSK_Buffer[35] = 0xDCBA;
}
static void AIRCOPY_Key_UP_DOWN(int8_t Direction)
@@ -384,10 +429,10 @@ static void AIRCOPY_Key_UP_DOWN(int8_t Direction)
switch(Direction)
{
case 1:
gAircopyCurrentMapIndex = (gAircopyCurrentMapIndex + 1) % AIRCOPY_NUM_MAPS;
gAircopyCurrentMapIndex = (gAircopyCurrentMapIndex + 1) % (AIRCOPY_NUM_MAPS + 1u);
break;
case -1:
gAircopyCurrentMapIndex = (gAircopyCurrentMapIndex + AIRCOPY_NUM_MAPS - 1) % AIRCOPY_NUM_MAPS;
gAircopyCurrentMapIndex = (gAircopyCurrentMapIndex + AIRCOPY_NUM_MAPS) % (AIRCOPY_NUM_MAPS + 1u);
break;
}
}
@@ -398,6 +443,14 @@ void AIRCOPY_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
return;
}
if (gAircopyState == AIRCOPY_COMPLETE || gAircopyState == AIRCOPY_FAILED)
{
gAircopyState = AIRCOPY_READY;
gUpdateDisplay = true;
gRequestDisplayScreen = DISPLAY_AIRCOPY;
return;
}
if (Key != KEY_PTT) {
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
}
@@ -426,4 +479,4 @@ void AIRCOPY_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
gRequestDisplayScreen = DISPLAY_AIRCOPY;
}
#endif
#endif
+7 -48
View File
@@ -28,53 +28,10 @@
#define AIRCOPY_BLOCK_SIZE 0x0040u // 64 bytes per AirCopy block
#define AIRCOPY_CHANNELS_PER_BANK 128
#define AIRCOPY_NUM_BANKS MR_CHANNELS_MAX / AIRCOPY_CHANNELS_PER_BANK
#define AIRCOPY_CHANNEL_SIZE 16 // bytes per channel (freq/name)
#define AIRCOPY_BANK_SIZE_BYTES 0x1080u // 0x800 (Freq) + 0x800 (Name) + 0x80 (Attr)
#define AIRCOPY_NUM_MAPS (AIRCOPY_NUM_BANKS + 1u) // banks + one settings map
#define AIRCOPY_ALL_INDEX AIRCOPY_NUM_MAPS // selection sentinel: send/receive everything
#define AIRCOPY_BAR_WIDTH 120 // Visible width of the progress gauge
// ============================================================================
// Segment write mode
// ============================================================================
/*
* Defines how a segment must be written to EEPROM.
*
* - STRUCT: structured data (frequencies, names)
* - BYTES : raw byte stream (attributes, settings, etc.)
*/
typedef enum {
AIRCOPY_WRITE_STRUCT = 0,
AIRCOPY_WRITE_BYTES = 1,
} AIRCOPY_WriteMode_t;
// ============================================================================
// Transfer segment structure
// ============================================================================
/*
* Describes a contiguous EEPROM region involved in AirCopy.
* The write_mode defines how the RX side must write the data.
*/
typedef struct {
uint16_t start_offset;
uint16_t end_offset;
AIRCOPY_WriteMode_t write_mode;
} AIRCOPY_Segment_t;
// ============================================================================
// Transfer map structure
// ============================================================================
/*
* A transfer map is a collection of segments describing
* one complete AirCopy operation (bank, settings, etc.).
*/
typedef struct {
const AIRCOPY_Segment_t *segments;
uint16_t num_segments;
uint16_t total_blocks;
} AIRCOPY_TransferMap_t;
// ============================================================================
// AirCopy state
// ============================================================================
@@ -82,7 +39,8 @@ typedef struct {
typedef enum {
AIRCOPY_READY = 0,
AIRCOPY_TRANSFER,
AIRCOPY_COMPLETE
AIRCOPY_COMPLETE,
AIRCOPY_FAILED
} AIRCOPY_State_t;
// ============================================================================
@@ -93,6 +51,7 @@ extern AIRCOPY_State_t gAircopyState;
extern uint16_t gAirCopyBlockNumber;
extern uint16_t gErrorsDuringAirCopy;
extern bool gAirCopyIsSendMode;
extern bool gAircopyAll; // All mode: banks + settings in one pass
extern uint16_t g_FSK_Buffer[36];
@@ -103,8 +62,8 @@ 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);
const AIRCOPY_TransferMap_t* AIRCOPY_GetCurrentMap(void);
uint16_t AIRCOPY_GetTotalBlocks(void);
uint8_t AIRCOPY_CurrentSliceMap(void); // map index of the block in progress (All slice label)
// XOR-obfuscate `count` words of g_FSK_Buffer starting at index 1.
// Self-inverse: applying twice restores the original buffer.
+153 -134
View File
@@ -18,7 +18,6 @@
#include <stdint.h>
#include <string.h>
#include "am_fix.h"
#include "app/action.h"
#ifdef ENABLE_AIRCOPY
@@ -33,7 +32,7 @@
#ifdef ENABLE_FLASHLIGHT
#include "app/flashlight.h"
#endif
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
#include "app/fm.h"
#endif
#include "app/generic.h"
@@ -54,7 +53,7 @@
// #include "bsp/dp32g030/pwmplus.h"
#endif
#include "driver/backlight.h"
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
#include "driver/bk1080.h"
#endif
#include "driver/bk4819.h"
@@ -112,8 +111,12 @@ void (*const ProcessKeysFunctions[])(KEY_Code_t Key, bool bKeyPressed, bool bKey
[DISPLAY_MENU] = &MENU_ProcessKeys,
[DISPLAY_SCANNER] = &SCANNER_ProcessKeys,
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
[DISPLAY_FM] = &FM_ProcessKeys,
#elif defined(ENABLE_FMRADIO)
/* The modal FM overlay handles its own keys. Keep the enum slot populated
for configurations where DISPLAY_FM is the final display entry. */
[DISPLAY_FM] = &MAIN_ProcessKeys,
#endif
#ifdef ENABLE_AIRCOPY
@@ -235,7 +238,7 @@ static void ScreenSaverUpdateViewer(void)
#endif
}
static bool ScreenSaverCanDisplay(void)
static bool ScreenSaverCanDisplay(bool modal)
{
if (gSetting_set_sav == SET_SAV_OFF ||
gEeprom.BACKLIGHT_TIME == 0 ||
@@ -247,7 +250,7 @@ static bool ScreenSaverCanDisplay(void)
gCurrentFunction == FUNCTION_TRANSMIT ||
FUNCTION_IsRx() ||
gPttIsPressed
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
|| (gFM_ScanState != FM_SCAN_OFF && !gFM_FoundFrequency)
#endif
#ifdef ENABLE_FEAT_F4HWN_BEAM
@@ -258,10 +261,10 @@ static bool ScreenSaverCanDisplay(void)
return false;
}
if (gScreenToDisplay == DISPLAY_MAIN)
if (modal || gScreenToDisplay == DISPLAY_MAIN)
return true;
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
if (gScreenToDisplay == DISPLAY_FM)
return true;
#endif
@@ -269,9 +272,9 @@ static bool ScreenSaverCanDisplay(void)
return false;
}
static void ScreenSaverTryDisplay(void)
static void ScreenSaverTryDisplay(bool modal)
{
if (!ScreenSaverCanDisplay())
if (!ScreenSaverCanDisplay(modal))
return;
if (gSetting_set_sav == SET_SAV_LOGO)
@@ -296,6 +299,28 @@ static void ScreenSaverExit(void)
gUpdateStatus = true;
}
}
static bool ScreenSaverAnimate(void)
{
if (!gScreenSaverDisplayed)
return false;
if (gSetting_set_sav == SET_SAV_MATRIX) {
if (++gScreenSaverTick >= 8u) {
gScreenSaverTick = 0;
ScreenSaverRenderMatrix(false);
return true;
}
} else if (gSetting_set_sav == SET_SAV_LOGO_PLUS) {
if (++gScreenSaverTick >= 16u) {
gScreenSaverTick = 0;
ScreenSaverRenderLogoPlus(false);
return true;
}
}
return false;
}
#endif
bool APP_IsScreenSaverDisplayed(void)
@@ -307,12 +332,53 @@ bool APP_IsScreenSaverDisplayed(void)
#endif
}
/* Modal foreground loops (resident tools and overlay apps) bypass APP_Update()
* and therefore also bypass the normal 10 ms fade and 500 ms BLTime service.
* Keep that service resident so every overlay app gets identical timing without
* extending the app ABI. Only selected modal screens opt into the saver; the
* others still fade from BLMax to BLMin when BLTime expires. */
void APP_ModalBacklightTick(bool allowScreenSaver)
{
if (gNextTimeslice) {
gNextTimeslice = false;
BACKLIGHT_Update();
#ifdef ENABLE_FEAT_F4HWN_LOGO_SAV
if (ScreenSaverAnimate())
ScreenSaverUpdateViewer();
#endif
}
if (!gNextTimeslice_500ms)
return;
gNextTimeslice_500ms = false;
if (gBacklightCountdown_500ms > 0 &&
gEeprom.BACKLIGHT_TIME < 61 &&
--gBacklightCountdown_500ms == 0)
BACKLIGHT_TurnOff();
#ifdef ENABLE_FEAT_F4HWN_LOGO_SAV
if (allowScreenSaver && gBacklightCountdown_500ms == 0)
ScreenSaverTryDisplay(true);
#else
(void)allowScreenSaver;
#endif
}
void APP_ModalScreenSaverExit(void)
{
#ifdef ENABLE_FEAT_F4HWN_LOGO_SAV
ScreenSaverExit();
#endif
}
static void CheckForIncoming(void)
{
if (!g_SquelchLost)
return; // squelch is closed
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
// FM scan in progress: ignore main-channel RX so scanning is not interrupted.
// Normal FM listening (FM_SCAN_OFF) still yields to channel signals as before.
if (gFmRadioMode && gFM_ScanState != FM_SCAN_OFF)
@@ -451,7 +517,7 @@ static void HandleIncoming(void)
}
#endif
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
// Defensive: do not leave FM scan for a main-channel signal.
if (gFmRadioMode && gFM_ScanState != FM_SCAN_OFF)
return;
@@ -722,7 +788,7 @@ void APP_StartListening(FUNCTION_Type_t function)
return;
#endif
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
if (gFmRadioMode)
BK1080_Init0();
#endif
@@ -791,7 +857,7 @@ void APP_StartListening(FUNCTION_Type_t function)
RXTX_LOG_BeginRx(gRxVfo, function);
#endif
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
if (function == FUNCTION_MONITOR || gFmRadioMode)
#else
if (function == FUNCTION_MONITOR)
@@ -1008,36 +1074,44 @@ static void CheckRadioInterrupts(void)
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, false);
}
#ifdef ENABLE_AIRCOPY
if (interrupts.fskFifoAlmostFull &&
gScreenToDisplay == DISPLAY_AIRCOPY &&
gAircopyState == AIRCOPY_TRANSFER &&
gAirCopyIsSendMode == 0)
#if defined(ENABLE_AIRCOPY) || defined(ENABLE_FEAT_F4HWN_BEAM)
if (interrupts.fskFifoAlmostFull || interrupts.fskRxFinied)
{
for (unsigned int i = 0; i < 4; i++) {
g_FSK_Buffer[gFSKWriteIndex++] = BK4819_ReadRegister(BK4819_REG_5F);
}
AIRCOPY_StorePacket();
}
#endif
uint8_t fskTarget = 0;
#ifdef ENABLE_FEAT_F4HWN_BEAM
if ((interrupts.fskFifoAlmostFull || interrupts.fskRxFinied) &&
gBeamActive &&
gBeamMode == BEAM_MODE_RX &&
(gBeamStatus == BEAM_STATUS_RX_WAIT || gBeamStatus == BEAM_STATUS_ERROR))
{
const unsigned int wordsToRead = interrupts.fskRxFinied ? (36 - gFSKWriteIndex) : 4;
for (unsigned int i = 0; i < wordsToRead; i++) {
const uint16_t word = BK4819_ReadRegister(BK4819_REG_5F);
if (gFSKWriteIndex < 36)
g_FSK_Buffer[gFSKWriteIndex++] = word;
}
if (gBeamActive &&
gBeamMode == BEAM_MODE_RX &&
(gBeamStatus == BEAM_STATUS_RX_WAIT || gBeamStatus == BEAM_STATUS_ERROR))
fskTarget = 2;
#endif
#ifdef ENABLE_AIRCOPY
// Aircopy wins if stale state ever makes both receivers eligible.
if (gScreenToDisplay == DISPLAY_AIRCOPY &&
gAircopyState == AIRCOPY_TRANSFER)
fskTarget = 1;
#endif
gBeamRxWordCount = gFSKWriteIndex;
gUpdateDisplay = true;
BEAM_StorePacket();
if (fskTarget != 0)
{
const unsigned int wordsToRead = interrupts.fskRxFinied
? (gFSKWriteIndex < 36 ? 36u - gFSKWriteIndex : 0u)
: 4u;
for (unsigned int i = 0; i < wordsToRead; i++) {
const uint16_t word = BK4819_ReadRegister(BK4819_REG_5F);
if (gFSKWriteIndex < 36)
g_FSK_Buffer[gFSKWriteIndex++] = word;
}
#ifdef ENABLE_AIRCOPY
if (fskTarget == 1)
AIRCOPY_StorePacket();
#endif
#ifdef ENABLE_FEAT_F4HWN_BEAM
if (fskTarget == 2)
BEAM_StorePacket();
#endif
}
}
#endif
}
@@ -1089,7 +1163,7 @@ static void HandleVox(void)
gVoxPauseCountdown = 0;
}
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
if (gFmRadioMode)
return;
#endif
@@ -1251,7 +1325,7 @@ void APP_Update(void)
if (gCurrentFunction != FUNCTION_TRANSMIT)
HandleFunction();
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
// if (gFmRadioCountdown_500ms > 0)
if (gFmRadioMode && gFmRadioCountdown_500ms > 0) // 1of11
return;
@@ -1296,7 +1370,7 @@ void APP_Update(void)
#ifdef ENABLE_VOICE
&& gVoiceWriteIndex == 0
#endif
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
&& !gFmRadioMode
#endif
#ifdef ENABLE_DTMF_CALLING
@@ -1315,7 +1389,7 @@ void APP_Update(void)
gScheduleDualWatch = false;
}
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
if (gScheduleFM && gFM_ScanState != FM_SCAN_OFF && !FUNCTION_IsRx()) {
// switch to FM radio mode
FM_Play();
@@ -1335,7 +1409,7 @@ void APP_Update(void)
|| gScanStateDir != SCAN_OFF
|| gCssBackgroundScan
|| gScreenToDisplay != DISPLAY_MAIN
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
|| gFmRadioMode
#endif
#ifdef ENABLE_DTMF_CALLING
@@ -1447,7 +1521,7 @@ void CheckKeys(void)
#endif
#ifdef ENABLE_AIRCOPY
if (gScreenToDisplay == DISPLAY_AIRCOPY && gAircopyState != AIRCOPY_READY){
if (gScreenToDisplay == DISPLAY_AIRCOPY && gAircopyState == AIRCOPY_TRANSFER){
return;
}
#endif
@@ -1605,12 +1679,6 @@ void APP_TimeSlice10ms(void)
gFlashLightBlinkCounter++;
#ifdef ENABLE_AM_FIX
if (gRxVfo->Modulation == MODULATION_AM) {
AM_fix_10ms(gEeprom.RX_VFO);
}
#endif
#ifdef ENABLE_UART
if (UART_IsCommandAvailable(UART_PORT_UART)) {
// SCHEDULER_Disable();
@@ -1622,6 +1690,8 @@ void APP_TimeSlice10ms(void)
if (gReducedService)
return;
UI_MAIN_TimeSlice10ms(); // scan-list name hold countdown (10 ms resolution)
if (gCurrentFunction != FUNCTION_POWER_SAVE || !gRxIdleMode)
CheckRadioInterrupts();
#ifdef ENABLE_FEAT_F4HWN_ACTION_PICKER
@@ -1654,7 +1724,9 @@ void APP_TimeSlice10ms(void)
}
#ifdef ENABLE_FEAT_F4HWN_LOGO_SAV
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
bool screenSaverRendered = false;
#endif
if (gScreenSaverDisplayed) {
if (gUpdateDisplayCurrent) {
@@ -1664,18 +1736,10 @@ void APP_TimeSlice10ms(void)
gUpdateStatus = false;
}
if (gSetting_set_sav == SET_SAV_MATRIX) {
if (++gScreenSaverTick >= 8u) {
gScreenSaverTick = 0;
ScreenSaverRenderMatrix(false);
screenSaverRendered = true;
}
} else if (gSetting_set_sav == SET_SAV_LOGO_PLUS) {
if (++gScreenSaverTick >= 16u) {
gScreenSaverTick = 0;
ScreenSaverRenderLogoPlus(false);
screenSaverRendered = true;
}
if (ScreenSaverAnimate()) {
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
screenSaverRendered = true;
#endif
}
}
#endif
@@ -1702,7 +1766,7 @@ void APP_TimeSlice10ms(void)
// Skipping authentic device checks
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
if (gFmRadioMode && gFmRadioCountdown_500ms > 0) // 1of11
return;
#endif
@@ -1722,52 +1786,6 @@ void APP_TimeSlice10ms(void)
#endif
if (gCurrentFunction == FUNCTION_TRANSMIT) {
#ifdef ENABLE_ALARM
if (gAlarmState == ALARM_STATE_TXALARM || gAlarmState == ALARM_STATE_SITE_ALARM) {
uint16_t Tone;
gAlarmRunningCounter++;
gAlarmToneCounter++;
Tone = 500 + (gAlarmToneCounter * 25);
if (Tone > 1500) {
Tone = 500;
gAlarmToneCounter = 0;
}
BK4819_SetScrambleFrequencyControlWord(Tone);
if (gEeprom.ALARM_MODE == ALARM_MODE_TONE && gAlarmRunningCounter == 512) {
gAlarmRunningCounter = 0;
if (gAlarmState == ALARM_STATE_TXALARM) {
gAlarmState = ALARM_STATE_SITE_ALARM;
RADIO_SendCssTail();
BK4819_SetupPowerAmplifier(0, 0);
BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, false);
BK4819_Enable_AfDac_DiscMode_TxDsp();
BK4819_ToggleGpioOut(BK4819_GPIO5_PIN1_RED, false);
GUI_DisplayScreen();
}
else {
gAlarmState = ALARM_STATE_TXALARM;
GUI_DisplayScreen();
BK4819_ToggleGpioOut(BK4819_GPIO5_PIN1_RED, true);
RADIO_SetTxParameters();
BK4819_TransmitTone(true, 500);
SYSTEM_DelayMs(2);
AUDIO_AudioPathOn();
gEnableSpeaker = true;
gAlarmToneCounter = 0;
}
}
}
#endif
// repeater tail tone elimination
if (gRTTECountdown_10ms > 0) {
if (--gRTTECountdown_10ms == 0) {
@@ -1780,7 +1798,7 @@ void APP_TimeSlice10ms(void)
}
}
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
if (gFmRadioMode && gFM_RestoreCountdown_10ms > 0) {
if (--gFM_RestoreCountdown_10ms == 0) {
FM_Start(); // switch back to FM radio mode
@@ -1797,7 +1815,7 @@ void APP_TimeSlice10ms(void)
#endif
#ifdef ENABLE_AIRCOPY
if (gScreenToDisplay == DISPLAY_AIRCOPY && gAircopyState == AIRCOPY_TRANSFER && gAirCopyIsSendMode == 1) {
if (gScreenToDisplay == DISPLAY_AIRCOPY && gAircopyState == AIRCOPY_TRANSFER) {
if (!AIRCOPY_SendMessage()) {
GUI_DisplayScreen();
}
@@ -1868,7 +1886,7 @@ void APP_TimeSlice500ms(void)
{
if (IS_MR_CHANNEL(gTxVfo->CHANNEL_SAVE) && (gInputBoxIndex > 0 && gInputBoxIndex < 4)
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
&& (!gFmRadioMode)
#endif
)
@@ -1915,7 +1933,7 @@ void APP_TimeSlice500ms(void)
// Skipped authentic device check
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
if (gFmRadioCountdown_500ms > 0)
{
gFmRadioCountdown_500ms--;
@@ -1934,7 +1952,7 @@ void APP_TimeSlice500ms(void)
) {
BACKLIGHT_TurnOff();
#ifdef ENABLE_FEAT_F4HWN_LOGO_SAV
ScreenSaverTryDisplay();
ScreenSaverTryDisplay(false);
#endif
}
@@ -2024,10 +2042,14 @@ void APP_TimeSlice500ms(void)
if ((gBatteryCheckCounter & 1) == 0)
{
#if defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) || defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON)
BATTERY_Sample(true);
#else
BOARD_ADC_GetBatteryInfo(&gBatteryVoltages[gBatteryVoltageIndex++], &gBatteryCurrent);
if (gBatteryVoltageIndex > 3)
gBatteryVoltageIndex = 0;
BATTERY_GetReadings(true);
#endif
}
}
@@ -2043,7 +2065,7 @@ void APP_TimeSlice500ms(void)
}
if (!gCssBackgroundScan && gScanStateDir == SCAN_OFF && !SCANNER_IsScanning()
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
&& (gFM_ScanState == FM_SCAN_OFF || gAskToSave)
#endif
#ifdef ENABLE_AIRCOPY
@@ -2098,7 +2120,7 @@ void APP_TimeSlice500ms(void)
GUI_DisplayType_t disp = DISPLAY_INVALID;
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
if (gFmRadioMode && ! FUNCTION_IsRx()) {
disp = DISPLAY_FM;
}
@@ -2120,7 +2142,7 @@ void APP_TimeSlice500ms(void)
if (!gPttIsPressed && gVFOStateResumeCountdown_500ms > 0 && --gVFOStateResumeCountdown_500ms == 0) {
RADIO_SetVfoState(VFO_STATE_NORMAL);
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
if (gFmRadioMode && !FUNCTION_IsRx()) {
// switch back to FM radio mode
FM_Start();
@@ -2136,7 +2158,7 @@ void APP_TimeSlice500ms(void)
!gAskToSave &&
!gCssBackgroundScan)
{
ScreenSaverTryDisplay();
ScreenSaverTryDisplay(false);
}
#endif
@@ -2174,17 +2196,15 @@ void APP_TimeSlice500ms(void)
#endif
}
#if defined(ENABLE_ALARM) || defined(ENABLE_TX1750)
static void ALARM_Off(void)
#ifdef ENABLE_TX1750
static void TX1750_Off(void)
{
AUDIO_AudioPathOff();
gEnableSpeaker = false;
if (gAlarmState == ALARM_STATE_TXALARM || gAlarmState == ALARM_STATE_TX1750) {
RADIO_SendEndOfTransmission();
}
RADIO_SendEndOfTransmission();
gAlarmState = ALARM_STATE_OFF;
gTx1750Active = false;
#ifdef ENABLE_VOX
gVoxResumeCountdown = 80;
@@ -2275,7 +2295,7 @@ static void ProcessKey(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
flagSaveSettings = false;
}
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
if (gFlagSaveFM) {
SETTINGS_SaveFM();
gFlagSaveFM = false;
@@ -2448,8 +2468,8 @@ static void ProcessKey(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
}
if (gCurrentFunction == FUNCTION_TRANSMIT) {
#if defined(ENABLE_ALARM) || defined(ENABLE_TX1750)
if (gAlarmState == ALARM_STATE_OFF)
#ifdef ENABLE_TX1750
if (!gTx1750Active)
#endif
{
char Code;
@@ -2501,10 +2521,9 @@ static void ProcessKey(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
BK4819_PlayDTMFEx(gEeprom.DTMF_SIDE_TONE, Code);
}
}
#if defined(ENABLE_ALARM) || defined(ENABLE_TX1750)
// else if ((!bKeyHeld && bKeyPressed) || (gAlarmState == ALARM_STATE_TX1750 && bKeyHeld && !bKeyPressed)) {
else if ((bKeyHeld != bKeyPressed) && (gAlarmState == ALARM_STATE_TX1750 || bKeyPressed)) {
ALARM_Off();
#ifdef ENABLE_TX1750
else if (bKeyHeld != bKeyPressed) {
TX1750_Off();
if (gEeprom.REPEATER_TAIL_TONE_ELIMINATION == 0)
FUNCTION_Select(FUNCTION_FOREGROUND);
@@ -2570,7 +2589,7 @@ Skip:
gUpdateStatus = true;
}
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
if (gRequestSaveFM) {
gRequestSaveFM = false;
if (!bKeyHeld)
+2
View File
@@ -32,5 +32,7 @@ void APP_Update(void);
void APP_TimeSlice10ms(void);
void APP_TimeSlice500ms(void);
bool APP_IsScreenSaverDisplayed(void);
void APP_ModalBacklightTick(bool allowScreenSaver);
void APP_ModalScreenSaverExit(void);
#endif
+3 -14
View File
@@ -69,7 +69,6 @@ static_assert(sizeof(BEAM_Payload_t) <= 64);
BEAM_Mode_t gBeamMode = BEAM_MODE_TX;
BEAM_Status_t gBeamStatus = BEAM_STATUS_READY;
uint16_t gBeamCopiedChannel = 0xFFFFu;
uint8_t gBeamRxWordCount;
bool gBeamActive;
static VFO_Info_t gBeamRadioVfo;
@@ -120,7 +119,6 @@ static void BEAM_SendPacket(void)
payload->dtmf_decoding_enable = vfo->DTMF_DECODING_ENABLE;
#endif
payload->step_setting = vfo->STEP_SETTING;
payload->scrambling_type = vfo->SCRAMBLING_TYPE;
payload->band = vfo->Band;
payload->scanlist = vfo->SCANLIST_PARTICIPATION;
payload->compander = vfo->Compander;
@@ -174,7 +172,6 @@ static void BEAM_SavePayloadToFirstFreeChannel(const BEAM_Payload_t *payload)
VFO_Info_t vfo;
RADIO_InitInfo(&vfo, channel, payload->rx_frequency);
// CRC + magic + version already validate the payload — no need to clamp fields.
vfo.TX_OFFSET_FREQUENCY = payload->tx_offset_frequency;
vfo.freq_config_RX.Code = payload->rx_code;
vfo.freq_config_TX.Code = payload->tx_code;
@@ -191,21 +188,18 @@ static void BEAM_SavePayloadToFirstFreeChannel(const BEAM_Payload_t *payload)
#ifdef ENABLE_DTMF_CALLING
vfo.DTMF_DECODING_ENABLE = payload->dtmf_decoding_enable;
#endif
vfo.STEP_SETTING = payload->step_setting;
vfo.STEP_SETTING = payload->step_setting < STEP_N_ELEM ? payload->step_setting : STEP_12_5kHz;
vfo.StepFrequency = gStepFrequencyTable[vfo.STEP_SETTING];
vfo.SCRAMBLING_TYPE = payload->scrambling_type;
vfo.Band = payload->band;
vfo.SCANLIST_PARTICIPATION = payload->scanlist;
vfo.Compander = payload->compander;
memcpy(vfo.Name, payload->name, sizeof(vfo.Name));
vfo.Name[sizeof(vfo.Name) - 1] = '\0';
RADIO_ApplyOffset(&vfo);
RADIO_ConfigureSquelchAndOutputPower(&vfo);
SETTINGS_SaveChannel(channel, gEeprom.TX_VFO, &vfo, 3);
#ifndef ENABLE_KEEP_MEM_NAME
SETTINGS_SaveChannelName(channel, vfo.Name);
#endif
gBeamCopiedChannel = channel;
gBeamStatus = BEAM_STATUS_RX_SAVED;
@@ -223,7 +217,6 @@ static void BEAM_KeyMenu(void)
} else {
gBeamStatus = BEAM_STATUS_RX_WAIT;
gBeamCopiedChannel = 0xFFFFu;
gBeamRxWordCount = 0;
gFSKWriteIndex = 0;
BK4819_PrepareFSKReceive();
}
@@ -251,7 +244,6 @@ void ACTION_Beam(void)
gBeamMode = BEAM_MODE_TX;
gBeamStatus = BEAM_STATUS_READY;
gBeamCopiedChannel = 0xFFFFu;
gBeamRxWordCount = 0;
gBeamActive = true;
GUI_SelectNextDisplay(DISPLAY_MAIN);
}
@@ -269,7 +261,6 @@ void BEAM_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
case KEY_DOWN:
gBeamMode ^= 1; // (gBeamMode == BEAM_MODE_TX) ? BEAM_MODE_RX : BEAM_MODE_TX
gBeamStatus = BEAM_STATUS_READY;
gBeamRxWordCount = 0;
break;
case KEY_MENU:
BEAM_KeyMenu();
@@ -292,7 +283,6 @@ void BEAM_StorePacket(void)
if (gFSKWriteIndex < 36)
return;
gBeamRxWordCount = gFSKWriteIndex;
gFSKWriteIndex = 0;
const uint16_t Status = BK4819_ReadRegister(BK4819_REG_0B);
@@ -317,7 +307,6 @@ void BEAM_StorePacket(void)
error:
gBeamStatus = BEAM_STATUS_ERROR;
gBeamRxWordCount = 0;
gUpdateDisplay = true;
BACKLIGHT_TurnOn();
}
-1
View File
@@ -42,7 +42,6 @@ typedef enum {
extern BEAM_Mode_t gBeamMode;
extern BEAM_Status_t gBeamStatus;
extern uint16_t gBeamCopiedChannel;
extern uint8_t gBeamRxWordCount;
extern bool gBeamActive;
void ACTION_Beam(void);
+26 -32
View File
@@ -24,6 +24,14 @@
#include "k5viewer.h"
#endif
#define BRICK_WIDTH 14
#define BRICK_HEIGHT 5
#define BALL_WIDTH 3
#define BALL_HEIGHT 3
#define RACKET_WIDTH 24
#define RACKET_HEIGHT 2
#define RACKET_Y 50
static uint32_t randSeed = 1;
static uint8_t blockAnim = 0;
@@ -101,16 +109,14 @@ void drawScore()
// Render the ball
void renderBall(bool state) {
UI_DrawRectangleBuffer(gFrameBuffer, ball.x, ball.y, ball.x + ball.w - 1, ball.y + ball.h - 1, state);
UI_DrawLineBuffer(gFrameBuffer, ball.x - 1, ball.y + 1, ball.x + ball.w, ball.y + 1, state);
UI_DrawRectangleBuffer(gFrameBuffer, ball.x, ball.y, ball.x + BALL_WIDTH - 1, ball.y + BALL_HEIGHT - 1, state);
UI_DrawLineBuffer(gFrameBuffer, ball.x - 1, ball.y + 1, ball.x + BALL_WIDTH, ball.y + 1, state);
}
// Init ball
void initBall() {
ball.x = 62;
ball.y = 30;
ball.w = 3;
ball.h = 3;
ball.dx = 0;
ball.dy = 1;
@@ -119,7 +125,7 @@ void initBall() {
// Calculate the direction of the bounced ball
void directionBall(int16_t x, uint8_t w, int8_t num) {
ball.dx = map(x + w - ball.x, 0, w, num, -num);
ball.dx = (int16_t)(x + w - ball.x) * (-num - num) / w + num;
ball.dy *= -1;
}
@@ -145,8 +151,8 @@ void drawBall() {
}
// And now Down...
if (ball.y == 47) {
if (ball.x + 1 >= racket.x && ball.x - 1 <= racket.x + racket.w) {
directionBall(racket.x, racket.w, 3);
if (ball.x + 1 >= racket.x && ball.x - 1 <= racket.x + RACKET_WIDTH) {
directionBall(racket.x, RACKET_WIDTH, 3);
tone = 400;
}
}
@@ -172,24 +178,15 @@ void drawBall() {
// Init wall
void initWall() {
uint8_t offset = 6;
uint8_t i = 0;
uint8_t j = 0;
uint8_t k = 0;
Brick *current = brick;
for (i = 0; i < BRICK_NUMBER; i++) {
if (i % 6 == 0) {
j = 0;
k++;
for (uint8_t y = 0; y < 24; y += 8) {
for (uint8_t x = 6; x < 126; x += 20) {
current->x = x;
current->y = y;
current->destroy = false;
current++;
}
brick[i].x = offset + (20 * j);
brick[i].y = -8 + 8 * k;
brick[i].w = 14;
brick[i].h = 5;
brick[i].destroy = false;
j++;
}
}
@@ -205,13 +202,13 @@ void drawWall() {
fb_ptr[14] = 0b00011110;
if ((ball.x + 1 >= brick[i].x &&
ball.x - 1 <= brick[i].x + brick[i].w) &&
ball.x - 1 <= brick[i].x + BRICK_WIDTH) &&
((ball.y + 1 >= brick[i].y &&
ball.y - 1 <= brick[i].y + brick[i].h))) {
ball.y - 1 <= brick[i].y + BRICK_HEIGHT))) {
brick[i].destroy = true;
score++;
directionBall(brick[i].x, brick[i].w, 2);
directionBall(brick[i].x, BRICK_WIDTH, 2);
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, true);
memset(fb_ptr + 1, 0b00111111, 13);
@@ -238,16 +235,13 @@ void drawWall() {
// Render the racket shape
void renderRacket(int x, bool state) {
UI_DrawRectangleBuffer(gFrameBuffer, x + 1, racket.y, x + racket.w - 2, racket.y + racket.h, state);
UI_DrawLineBuffer(gFrameBuffer, x, racket.y + 1, x + racket.w - 1, racket.y + 1, state);
UI_DrawRectangleBuffer(gFrameBuffer, x + 1, RACKET_Y, x + RACKET_WIDTH - 2, RACKET_Y + RACKET_HEIGHT, state);
UI_DrawLineBuffer(gFrameBuffer, x, RACKET_Y + 1, x + RACKET_WIDTH - 1, RACKET_Y + 1, state);
}
// Init racket
void initRacket() {
racket.w = 24;
racket.x = (64) - (racket.w / 2);
racket.y = 50;
racket.h = 2;
racket.x = 64 - (RACKET_WIDTH / 2);
racket.p = racket.x;
renderRacket(racket.x, true);
-7
View File
@@ -47,24 +47,17 @@
typedef struct {
uint8_t x; // x
uint8_t y; // y
uint8_t w; // width
uint8_t h; // height
bool destroy; // active, if true, check this button, else bypass
} Brick;
typedef struct {
int8_t x; // x
uint8_t y; // y
uint8_t w; // width
uint8_t h; // height
uint8_t p; // previous x
} Racket;
typedef struct {
int16_t x; // x
int8_t y; // y
uint8_t w; // width
uint8_t h; // height
int8_t dx; // move x
int8_t dy; // move y
} Ball;
-4
View File
@@ -553,11 +553,7 @@ static void ScanFastApplyChannelShape(ModulationMode_t modulation)
}
else
{
#ifdef ENABLE_AM_FIX
BK4819_SetFilterBandwidth(BK4819_FILTER_BW_WIDE, true);
#else
BK4819_SetFilterBandwidth(BK4819_FILTER_BW_WIDE, false);
#endif
}
if (modulationChanged)
+2 -2
View File
@@ -18,7 +18,7 @@
#include <stdio.h> // NULL
#include "app/chFrScanner.h"
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
#include "app/fm.h"
#endif
#include "app/scanner.h"
@@ -279,7 +279,7 @@ void DTMF_HandleRequest(void)
gDTMF_ReplyState = DTMF_REPLY_AB;
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
if (gFmRadioMode)
{
FM_TurnOff();
+6 -1
View File
@@ -33,6 +33,7 @@
#include "ui/ui.h"
uint16_t gFM_Channels[FM_CHANNELS_MAX];
#ifdef ENABLE_FMRADIO_EMBEDDED
bool gFmRadioMode;
uint8_t gFmRadioCountdown_500ms;
volatile uint16_t gFmPlayCountdown_10ms;
@@ -52,8 +53,8 @@ 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);
#endif
bool FM_CheckValidChannel(uint8_t Channel)
{
@@ -94,6 +95,7 @@ int FM_ConfigureChannelState(void)
return 0;
}
#ifdef ENABLE_FMRADIO_EMBEDDED
void FM_SetFrequency(void)
{
BK1080_SetFrequency(gEeprom.FM_FrequencyPlaying, gEeprom.FM_Band/*, gEeprom.FM_Space*/);
@@ -196,6 +198,7 @@ void FM_PlayAndUpdate(void)
BACKLIGHT_TurnOn();
FM_AudioPathOn();
}
#endif
int FM_CheckFrequencyLock(uint16_t Frequency, uint16_t LowerLimit)
{
@@ -240,6 +243,7 @@ int FM_CheckFrequencyLock(uint16_t Frequency, uint16_t LowerLimit)
return 0;
}
#ifdef ENABLE_FMRADIO_EMBEDDED
static void Key_DIGITS(KEY_Code_t Key, uint8_t state)
{
enum { STATE_FREQ_MODE, STATE_MR_MODE, STATE_SAVE };
@@ -662,5 +666,6 @@ void FM_Start(void)
SETTINGS_WriteCurrentState();
#endif
}
#endif
#endif
+13 -8
View File
@@ -19,9 +19,12 @@
#ifdef ENABLE_FMRADIO
#include "driver/keyboard.h"
#include "misc.h"
#ifdef ENABLE_FMRADIO_EMBEDDED
#include "driver/keyboard.h"
#endif
#define FM_CHANNEL_UP 0x01
#define FM_CHANNEL_DOWN 0xFF
@@ -30,32 +33,34 @@ enum {
};
extern uint16_t gFM_Channels[FM_CHANNELS_MAX];
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);
int FM_CheckFrequencyLock(uint16_t Frequency, uint16_t LowerLimit);
#ifdef ENABLE_FMRADIO_EMBEDDED
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
+176 -177
View File
@@ -16,7 +16,7 @@
#include "app/foxhunt.h"
#ifdef ENABLE_FEAT_F4HWN_FOXHUNT
#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON)
#if defined(ENABLE_UART) || defined(ENABLE_USB)
#include "app/uart.h"
@@ -26,6 +26,9 @@
#include "k5viewer.h"
#endif
#include "settings.h"
#include "ui/status.h"
// Signal window mapped onto the RSSI bar, in dBm.
// Roughly S0 (empty) to S9 + 40 dB (full), IARU VHF/UHF scale.
#define FOXHUNT_DBM_FLOOR (-141)
@@ -84,11 +87,15 @@
#define FOXHUNT_AUDIO_BEEP 1
#define FOXHUNT_AUDIO_STATION 2
// --- Beacon (fox) sub-mode ---------------------------------------------------
// --- Beacon (fox) app --------------------------------------------------------
// Turns the radio into the hidden transmitter: each cycle keys up on the TX VFO and
// repeats the CW fox identifier (MOE..MO5, or "<call> MOE") in Morse for the TX window,
// then stays silent for the (adjustable) idle gap. The carrier stays up during the
// window; only the tone modulation is keyed on/off (EnterTxMute/ExitTxMute) = MCW on FM.
// then stays silent for the (adjustable) idle gap. Two keying modes (KEY_4):
// TONE (default): carrier stays up the whole window, only the tone is keyed on/off
// (EnterTxMute/ExitTxMute) = MCW on FM (F2A).
// CARR: the carrier (PA_ENABLE) is keyed together with the tone, so between
// elements the carrier itself is gone (carrier interruption, the ARDF
// field pattern) — harder to home in on, at the cost of some key clicks.
#define FOXHUNT_BEACON_TONE_HZ 1000 // CW tone pitch (Hz)
#define FOXHUNT_MORSE_UNIT_MS 100 // one Morse time unit (~12 WPM, ARDF pace)
#define FOXHUNT_BEACON_IDLE_DEF 30 // default silence between IDs (s)
@@ -102,7 +109,6 @@
#define FOXHUNT_BEACON_TX_MIN 5 // shortest TX window (s)
#define FOXHUNT_BEACON_TX_MAX 60 // longest TX window (s)
#define FOXHUNT_BEACON_TX_STEP 5 // TX adjust step (s)
#define FOXHUNT_CALLSIGN_ADDR 0x00A0C8u // boot message line 1 in SPI flash
#define FOXHUNT_CALLSIGN_MAX 12 // maximum boot-message characters used by the beacon
// Fox identifier (3 key). MOE..MO5 are the five standard IARU ARDF foxes ("MO" + 1..5
@@ -199,8 +205,7 @@ static const uint8_t FOXHUNT_MORSE_DIGIT[10] = {
// Trailing character of each fox id: MOE / MOI / MOS / MOH / MO5 (index = FOXHUNT_FOX_*).
static const char FOXHUNT_FOX_TAIL[5] = {'E', 'I', 'S', 'H', '5'};
// Beacon sub-mode state.
static bool foxBeacon; // false = hunt (RX), true = beacon (TX)
// Beacon state.
static bool beaconPhaseTx; // true = transmit this tick, false = idle gap
static uint8_t beaconIdle; // configured silence between IDs (s)
static uint8_t beaconIdleLeft; // seconds left in the current silence
@@ -211,6 +216,7 @@ static uint8_t beaconTx; // TX window length (s): the ID repeats for th
static uint16_t beaconTxMsLeft; // ms left in the current TX window (drained as it plays)
static uint8_t beaconTxSecShown; // whole-second value last painted on the TX line
static uint8_t foxFox; // selected fox identifier (FOXHUNT_FOX_*)
static bool beaconCarrierKeyed; // false = TONE (F2A, keyed tone); true = CARR (keyed carrier)
static char foxCall[FOXHUNT_CALLSIGN_MAX + 1]; // sanitised callsign for the CALL id
static void FOXHUNT_EnterHunt(void);
@@ -428,9 +434,9 @@ static void FOXHUNT_Tag(const char *s, uint8_t x, uint8_t line)
static void FOXHUNT_DrawFKey(void)
{
if (foxLocked)
memcpy(gStatusLine + 69, gFontKeyLock, sizeof(gFontKeyLock));
memcpy(gStatusLine + 70, gFontKeyLock, sizeof(gFontKeyLock));
else if (gWasFKeyPressed)
memcpy(gStatusLine + 69, gFontF, sizeof(gFontF));
memcpy(gStatusLine + 70, gFontF, sizeof(gFontF));
}
// Draw a string right-aligned in the small (7 px) font: its right edge lands at rightX.
@@ -439,24 +445,6 @@ static void FOXHUNT_DrawRightSmall(const char *s, uint8_t rightX, uint8_t line)
UI_PrintStringSmallNormal(s, (uint8_t)(rightX - strlen(s) * 7), 0, line);
}
// Battery icon plus the optional voltage/percentage text, top-right of the status
// line — shared by the hunt and beacon screens.
static void FOXHUNT_DrawStatusBattery(void)
{
unsigned int bx = LCD_WIDTH - sizeof(BITMAP_BatteryLevel1);
UI_DrawBattery(gStatusLine + bx, gBatteryDisplayLevel, gLowBatteryBlink);
if (gSetting_battery_text != 0) {
if (gSetting_battery_text == 1) { // voltage
const uint16_t v = MIN(gBatteryVoltageAverage, 999);
sprintf(str, "%u.%02u", v / 100, v % 100);
} else { // percentage
sprintf(str, "%02u%%", BATTERY_VoltsToPercent(gBatteryVoltageAverage));
}
bx -= 7 * strlen(str);
UI_PrintStringSmallBufferNormal(str, gStatusLine + bx);
}
}
// Right-aligned frequency on the bottom line (line 6), shared by the hunt and
// beacon screens.
static void FOXHUNT_DrawFreqBR(uint32_t freq)
@@ -472,7 +460,7 @@ static void FOXHUNT_BeaconChrome(void)
UI_DisplayClear();
UI_StatusClear();
GUI_DisplaySmallestInverse("BEACON", 2, 0, true, true, 26);
FOXHUNT_DrawStatusBattery();
UI_DrawStatusBattery(gStatusLine, str);
FOXHUNT_DrawFKey();
FOXHUNT_DrawFreqBR(gTxVfo->pTX->Frequency);
}
@@ -490,7 +478,7 @@ static void FOXHUNT_Draw(void)
GUI_DisplaySmallestInverse("FOX HUNT", 2, 0, true, true, 34);
// Battery (icon + optional percentage/voltage) top-right, as on the main screens.
FOXHUNT_DrawStatusBattery();
UI_DrawStatusBattery(gStatusLine, str);
FOXHUNT_DrawFKey();
// Gauge-mode icon (2 key), between the label and the audio icon: ascending
@@ -571,7 +559,7 @@ static void FOXHUNT_Draw(void)
// the burst end, fox at the next repeat), so cycling any of them is safe at any time.
// Wrap a 0..count-1 index one step forward (dir > 0) or backward, both ways round.
static uint8_t FOXHUNT_WrapStep(uint8_t v, uint8_t count, int8_t dir)
static __attribute__((noinline)) uint8_t FOXHUNT_WrapStep(uint8_t v, uint8_t count, int8_t dir)
{
return (uint8_t)((v + (dir > 0 ? 1u : (unsigned)(count - 1u))) % count);
}
@@ -624,6 +612,16 @@ static void FOXHUNT_AttCycle(int8_t dir)
FOXHUNT_RebaseMeasurements();
}
// Convert the two navigation keys into the same semantic value direction used
// by the resident menus and by the overlay FoxHunt app:
// UV-K5 UP/DOWN -> +1/-1
// UV-K1 LEFT/RIGHT -> -1/+1
static int8_t FOXHUNT_NavDirection(KEY_Code_t key)
{
int8_t direction = (key == KEY_UP) ? 1 : -1;
return gEeprom.SET_NAV ? direction : -direction;
}
// Fox identifier: MOE -> MOI -> MOS -> MOH -> MO5 -> MO -> CALL (F reverses).
static void FOXHUNT_FoxCycle(int8_t dir)
{
@@ -645,14 +643,15 @@ static void FOXHUNT_IdleCycle(int8_t dir)
}
// Apply a beacon number key in the given direction; returns true when it changed a
// setting so the caller can refresh. Keys follow the on-screen layout: 1 = TX (top-left),
// 2 = IDLE (below it), 3 = FOX (right). Shared by both beacon phases.
// setting so the caller can refresh. Keys: 1 = TX, 2 = IDLE, 3 = FOX, 4 = keying mode
// (TONE/CARR, a plain toggle so dir is moot). Shared by both beacon phases.
static bool FOXHUNT_BeaconKey(KEY_Code_t key, int8_t dir)
{
switch (key) {
case KEY_1: FOXHUNT_TxCycle(dir); return true;
case KEY_2: FOXHUNT_IdleCycle(dir); return true;
case KEY_3: FOXHUNT_FoxCycle(dir); return true;
case KEY_4: beaconCarrierKeyed = !beaconCarrierKeyed; return true;
default: return false;
}
}
@@ -689,10 +688,7 @@ static void FOXHUNT_IdleHousekeeping(void)
// hunt loop, and the beacon idle gap, never a burst), so the reading is not
// pulled down by TX load — keeping the status icon live and letting the
// beacon's battery gate react to a pack draining under a long run.
BOARD_ADC_GetBatteryInfo(&gBatteryVoltages[gBatteryVoltageIndex++], &gBatteryCurrent);
if (gBatteryVoltageIndex > 3)
gBatteryVoltageIndex = 0;
BATTERY_GetReadings(false);
BATTERY_Sample(false);
// Persist any changed setting within ~0.5 s, so it survives a power-off (not
// just a clean EXIT). No-op when nothing changed.
@@ -750,8 +746,8 @@ static void FOXHUNT_HandleKeys(void)
// released only by the long-press F handled above. Keeping ATT reachable is the whole
// point: on the final approach the sensitivity still has to be pulled down by hand.
if (foxLocked) {
if (kbd.current == KEY_UP) FOXHUNT_AttCycle(+1); // more attenuation
if (kbd.current == KEY_DOWN) FOXHUNT_AttCycle(-1); // less attenuation
if (kbd.current == KEY_UP || kbd.current == KEY_DOWN)
FOXHUNT_AttCycle(FOXHUNT_NavDirection(kbd.current));
return;
}
@@ -781,12 +777,9 @@ static void FOXHUNT_HandleKeys(void)
FOXHUNT_AttCycle(dir);
break;
case KEY_UP:
// Attenuation up one step (also the locked-mode control).
FOXHUNT_AttCycle(+1);
break;
case KEY_DOWN:
// Attenuation down one step.
FOXHUNT_AttCycle(-1);
// Follow SetNav: UP/DOWN on K5, LEFT/RIGHT on K1.
FOXHUNT_AttCycle(FOXHUNT_NavDirection(kbd.current));
break;
case KEY_MENU:
// Reset the peak / min hold and the trend reference (before each body scan).
@@ -794,11 +787,6 @@ static void FOXHUNT_HandleKeys(void)
minDbm = curDbm;
trendRef = curDbm;
break;
case KEY_SIDE1:
case KEY_SIDE2:
// Same shortcut that opened Fox Hunt now toggles to the beacon.
FOXHUNT_EnterBeacon();
break;
default:
break;
}
@@ -806,22 +794,18 @@ static void FOXHUNT_HandleKeys(void)
gWasFKeyPressed = false; // any non-F key consumes (or cancels) the reverse arm
}
// (Re)enter the hunt (RX) sub-mode: fixed front-end gain, attenuator and audio mode
// re-applied, peak/trend reset. Called at start-up and when leaving the beacon. The
// audio mode is a hunt setting, so it is (re)applied here, not forced off — it must
// survive a trip through the beacon.
// Enter Fox Hunt with fixed front-end gain, the persisted attenuator/audio mode,
// and freshly rebased level tracking.
static void FOXHUNT_EnterHunt(void)
{
foxBeacon = false;
BK4819_SetAGC(false);
FOXHUNT_ApplyAtt();
FOXHUNT_SetAudio(); // (re)apply the current audio mode: off / beep / station
// Let the RSSI settle to the gain just applied, then reset the peak/min hold, trend
// reference and signal history onto a fresh reading (also re-primes the sparkline on
// return from the beacon). Same settle as FOXHUNT_AttCycle: without it a restored
// BYP/BYP+ step at start-up or on beacon return would rebase onto the old-gain reading.
// reference and signal history onto a fresh reading. Same settle as
// FOXHUNT_AttCycle: without it a restored
// BYP/BYP+ step at start-up would rebase onto the old-gain reading.
// SetAudio already waits when audio is on, but not in the default audio-off case.
SYSTEM_DelayMs(FOXHUNT_ATT_SETTLE_MS);
FOXHUNT_RebaseMeasurements();
@@ -841,18 +825,15 @@ static VfoState_t FOXHUNT_TxState(void)
return VFO_STATE_BAT_LOW;
if (gBatteryDisplayLevel > 6)
return VFO_STATE_VOLTAGE_HIGH;
#ifndef ENABLE_TX_WHEN_AM
if (gTxVfo->Modulation != MODULATION_FM)
return VFO_STATE_TX_DISABLE;
#endif
return VFO_STATE_NORMAL;
}
// Refuse feedback shown when a burst is barred: reuse the beacon screen layout and
// the radio's own state label (VfoStateStr, e.g. "TX DISABLE" / "BAT LOW" / "VOLT
// HIGH"), same font as the main screen, for a beat; the caller then falls back to
// the hunt. The RX front-end and audio path are left untouched so the hunt keeps
// reading cleanly.
// HIGH"), same font as the main screen, for a beat; the caller then waits through
// a fresh idle gap before checking again.
static void FOXHUNT_TxDeniedNotice(VfoState_t state)
{
// Frame (clear + BEACON tag + battery + barred TX frequency).
@@ -869,12 +850,10 @@ static void FOXHUNT_TxDeniedNotice(VfoState_t state)
FOXHUNT_TickDelay();
}
// Enter the beacon (fox) sub-mode. The TX gates are enforced per burst in
// FOXHUNT_BeaconTick (which runs immediately), so entry just arms the state.
// Enter the Beacon app. TX gates are enforced before each burst.
static void FOXHUNT_EnterBeacon(void)
{
foxBeacon = true;
AUDIO_AudioPathOff(); // no RX audio while beaconing; foxAudioMode kept for the hunt
AUDIO_AudioPathOff();
gCurrentVfo = gTxVfo; // the VFO RADIO_SetTxParameters keys up
beaconPhaseTx = true;
}
@@ -893,8 +872,8 @@ static void FOXHUNT_DrawTxSeconds(void)
}
// Sleep in short slices while watching the keypad, so a burst stays interactive and the
// TX-window countdown (beaconTxMsLeft) drains in real time. EXIT leaves Fox Hunt and a
// side key aborts back to the hunt (both return true). The 1/2/3 keys edit the beacon
// TX-window countdown (beaconTxMsLeft) drains in real time. EXIT leaves Beacon. The
// 1/2/3 keys edit the beacon
// settings live, exactly as in the idle phase: the TX window (1) and idle gap (2) take
// effect at the upcoming window / gap, and a FOX change (3) at the next repeat (the
// message is rebuilt there, never under the running loop).
@@ -940,17 +919,12 @@ static bool FOXHUNT_TxDelay(uint16_t ms)
continue;
if (kbd.current == KEY_EXIT) {
foxRunning = false; // abort the burst and leave Fox Hunt
return true;
}
if (kbd.current == KEY_SIDE1 || kbd.current == KEY_SIDE2) {
foxBeacon = false; // abort the burst and switch back to hunt
foxRunning = false; // abort the burst and leave Beacon
return true;
}
if (kbd.current == KEY_MENU) {
// Cut the transmission short but stay in Beacon: aborting the burst without
// clearing foxBeacon/foxRunning drops the loop straight into a fresh idle
// gap of the configured IDLE length.
// Cut the transmission short but stay in Beacon: the loop drops straight
// into a fresh idle gap of the configured IDLE length.
return true;
}
if (kbd.current == KEY_F) { // arm / disarm the reverse step
@@ -978,8 +952,28 @@ static uint8_t FOXHUNT_MorseByte(char c)
return 0;
}
// Send one character as modulated CW: key the running TX tone on per element,
// muting the modulation (carrier stays up) between them. Returns true if aborted.
// Beacon keying primitive. Both modes key the tone (Enter/ExitTxMute); CARR additionally
// gates the PA in lockstep, so between elements the carrier itself is gone — not just the
// tone. Muting the tone in CARR too is deliberate: gating PA_ENABLE does not perfectly
// kill the carrier, and a tone left riding would bleed through that residual carrier as a
// near-continuous note. The PLL stays locked, so the carrier gate is only a GPIO toggle.
static void FOXHUNT_KeyOn(void)
{
BK4819_ExitTxMute();
// Assert the carrier in both modes: a no-op in TONE (already up), but it re-arms the PA
// if the mode was switched from CARR mid-window while the carrier happened to be down.
BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, true);
}
static void FOXHUNT_KeyOff(void)
{
BK4819_EnterTxMute();
if (beaconCarrierKeyed)
BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, false);
}
// Send one character as Morse: key each element on (FOXHUNT_KeyOn) for its dit/dah
// duration, off (FOXHUNT_KeyOff) in the gaps. What "key" means depends on the beacon
// mode (tone vs carrier), see above. Returns true if aborted.
static bool FOXHUNT_MorseChar(char c, uint8_t visibleChars)
{
uint8_t code = FOXHUNT_MorseByte(c);
@@ -996,9 +990,9 @@ static bool FOXHUNT_MorseChar(char c, uint8_t visibleChars)
for (bit >>= 1; bit; bit >>= 1) { // then walk the elements, MSB first
const uint16_t on = (code & bit) ? (FOXHUNT_MORSE_UNIT_MS * 3) // dah
: FOXHUNT_MORSE_UNIT_MS; // dit
BK4819_ExitTxMute();
if (FOXHUNT_TxDelay(on)) { BK4819_EnterTxMute(); return true; }
BK4819_EnterTxMute();
FOXHUNT_KeyOn();
if (FOXHUNT_TxDelay(on)) { FOXHUNT_KeyOff(); return true; }
FOXHUNT_KeyOff();
if (FOXHUNT_TxDelay(FOXHUNT_MORSE_UNIT_MS)) return true; // intra gap
}
@@ -1017,11 +1011,14 @@ static bool FOXHUNT_MorseChar(char c, uint8_t visibleChars)
// keys up for a fixed slot rather than sending a single one-shot.
static void FOXHUNT_BeaconTransmit(void)
{
RADIO_SetTxParameters(); // key up: carrier + PA
RADIO_SetTxParameters(); // key up: carrier + PA + PLL lock
// Prime the tone generator, then start silent before the first element.
// Prime the tone generator, then open the window silent before the first element:
// tone muted in both modes, and in CARR the carrier dropped too (PLL still locked).
BK4819_TransmitTone(false, FOXHUNT_BEACON_TONE_HZ);
BK4819_EnterTxMute();
if (beaconCarrierKeyed)
BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, false);
beaconTxMsLeft = (uint16_t)beaconTx * 1000u; // drained inside FOXHUNT_TxDelay
@@ -1065,8 +1062,8 @@ static void FOXHUNT_BeaconTransmit(void)
stop = FOXHUNT_TxDelay(FOXHUNT_MORSE_UNIT_MS * 7);
}
// Key down: mute, drop the PA and restore RX for the silent gap. If the burst
// was interrupted, FOXHUNT_TxDelay already set the target mode (quit or hunt).
// Key down: mute, drop the PA and restore RX for the silent gap. If EXIT
// interrupted the burst, FOXHUNT_TxDelay has already stopped the app.
BK4819_EnterTxMute();
BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, false);
RADIO_SetupRegisters(true);
@@ -1183,16 +1180,19 @@ static void FOXHUNT_BeaconDraw(bool txNow, uint8_t idleLeft)
UI_PrintString(str, 0, 127, 3, 10);
}
// Bottom-left column: 1 = TX window over 2 = IDLE gap. Bottom-right: 3 = FOX id over
// the freq (drawn by FOXHUNT_BeaconChrome). All three are inverse tags; each has its
// own number key, editable in either phase, so there is no cursor to show.
// Settings tags: TX (line 5, left) over IDLE (line 6, left). On line 5, right of TX:
// FOX shifted left with the keying mode (TONE/CARR) right-aligned beside it, so the pair
// spans roughly the width of the TX frequency drawn just below on line 6
// (FOXHUNT_BeaconChrome). Keys: 1 TX, 2 IDLE, 3 FOX, 4 mode; editable in either phase.
sprintf(str, "TX %us", beaconTx);
FOXHUNT_Tag(str, 4, 5);
sprintf(str, "IDLE %us", beaconIdle);
FOXHUNT_Tag(str, 4, 6);
FOXHUNT_FoxLabel(str);
FOXHUNT_Tag(str, (uint8_t)(125 - strlen(str) * 4), 5);
FOXHUNT_Tag(str, 66, 5);
const char *mode = beaconCarrierKeyed ? "CARR" : "TONE";
FOXHUNT_Tag(mode, (uint8_t)(125 - strlen(mode) * 4), 5);
}
static void FOXHUNT_BeaconKeys(void)
@@ -1222,11 +1222,7 @@ static void FOXHUNT_BeaconKeys(void)
switch (kbd.current) {
case KEY_EXIT:
foxRunning = false; // leave Fox Hunt entirely
break;
case KEY_SIDE1:
case KEY_SIDE2:
foxBeacon = false; // toggle back to the hunt
foxRunning = false;
break;
case KEY_MENU:
// Restart a full idle interval now (the TX-phase M drops here too).
@@ -1251,28 +1247,26 @@ static void FOXHUNT_BeaconTick(void)
// LOCK / TX LOCK and modulation are fixed for the session, but the battery
// is re-sampled over the idle gaps (FOXHUNT_IdleHousekeeping), so a beacon
// left running stops keying up once the pack falls low or over-voltage
// instead of transmitting blind. Refuse, notify, and drop back to hunt.
// instead of transmitting blind. Refuse, notify, then retry after an idle gap.
VfoState_t state = FOXHUNT_TxState();
if (state != VFO_STATE_NORMAL) {
FOXHUNT_TxDeniedNotice(state);
foxBeacon = false;
beaconPhaseTx = false;
FOXHUNT_EnterHunt();
beaconIdleLeft = beaconIdle;
beaconIdleTick = 0;
return;
}
// The burst repeats the ID for the whole TX window, redrawing the screen
// itself each repeat (remaining-window countdown); it may clear foxBeacon
// (side-key abort) or foxRunning (EXIT).
// itself each repeat (remaining-window countdown); EXIT may clear
// foxRunning and abort it.
FOXHUNT_BeaconTransmit();
beaconPhaseTx = false;
beaconIdleLeft = beaconIdle;
beaconIdleTick = 0;
if (foxRunning && !foxBeacon) // side-key switched to hunt
FOXHUNT_EnterHunt();
return; // EXIT (quit) falls through
return;
}
// Idle (silent) phase: responsive, editable countdown.
@@ -1281,12 +1275,8 @@ static void FOXHUNT_BeaconTick(void)
#endif
FOXHUNT_BeaconKeys();
if (!foxRunning) // EXIT: leave Fox Hunt
if (!foxRunning)
return;
if (!foxBeacon) { // side-key: back to the hunt
FOXHUNT_EnterHunt();
return;
}
FOXHUNT_BeaconDraw(false, beaconIdleLeft);
FOXHUNT_BlitScreen();
@@ -1312,14 +1302,14 @@ static void FOXHUNT_BeaconTick(void)
// (eeprom_compat.c) so aircopy clones them with the VFOs. A factory reset clears it.
#define FOXHUNT_CFG_ADDR 0x0090E0u
#define FOXHUNT_CFG_MAGIC 0xF4u // tells a written config from erased flash (0xFF)
#define FOXHUNT_CFG_LEN 7 // magic + att + graph + audio + idle + fox + tx
#define FOXHUNT_CFG_LEN 8 // magic + att + graph + audio + idle + fox + tx + carrier
// RAM mirror of the bytes last written to flash, so FOXHUNT_SaveConfig only touches
// the flash when a value actually changed.
static uint8_t foxCfgSaved[FOXHUNT_CFG_LEN];
// Pack the live settings into the on-flash layout.
static void FOXHUNT_ConfigPack(uint8_t out[FOXHUNT_CFG_LEN])
static __attribute__((noinline)) void FOXHUNT_ConfigPack(uint8_t out[FOXHUNT_CFG_LEN])
{
out[0] = FOXHUNT_CFG_MAGIC;
out[1] = attStep;
@@ -1328,6 +1318,7 @@ static void FOXHUNT_ConfigPack(uint8_t out[FOXHUNT_CFG_LEN])
out[4] = beaconIdle;
out[5] = foxFox;
out[6] = beaconTx;
out[7] = beaconCarrierKeyed;
}
// Restore persisted settings; erased/invalid flash leaves the defaults in place.
@@ -1347,6 +1338,7 @@ static void FOXHUNT_LoadConfig(void)
if (cfg[6] >= FOXHUNT_BEACON_TX_MIN && cfg[6] <= FOXHUNT_BEACON_TX_MAX
&& (cfg[6] % FOXHUNT_BEACON_TX_STEP) == 0)
beaconTx = cfg[6];
if (cfg[7] <= 1) beaconCarrierKeyed = cfg[7]; // erased (0xFF) legacy config -> keep default
}
FOXHUNT_ConfigPack(foxCfgSaved); // mirror the loaded (or default) state
@@ -1354,7 +1346,7 @@ static void FOXHUNT_LoadConfig(void)
// Write the settings to flash, but only when they changed since the last write.
// Called on the 500 ms tick (so any change persists within ~0.5 s and survives a
// power-off, not just a clean EXIT) and once more on leaving Fox Hunt. The RAM
// power-off, not just a clean EXIT) and once more on leaving either app. The RAM
// compare avoids re-reading the 4 KB sector on every quiet tick.
static void FOXHUNT_SaveConfig(void)
{
@@ -1368,83 +1360,91 @@ static void FOXHUNT_SaveConfig(void)
memcpy(foxCfgSaved, cfg, sizeof(cfg));
}
void APP_RunFoxHunt(void)
static void FOXHUNT_Begin(void)
{
// Finish any pending backlight fade, then start with the screen on and a full
// BLTime window (BACKLIGHT_TurnOn also re-arms the sleep countdown).
BACKLIGHT_UpdateTickless();
BACKLIGHT_TurnOn();
// Hunt on the user-selected VFO: dual-watch / cross-band may have left the
// radio listening on the other VFO, so force RX on the selected one and retune.
// Both apps operate on the user-selected VFO. Dual watch may have left the
// receiver on the other one, so establish a deterministic starting point.
gEeprom.RX_VFO = gEeprom.TX_VFO;
gRxVfo = gTxVfo;
gCurrentVfo = gTxVfo;
RADIO_SetupRegisters(true);
// Beacon defaults (LoadConfig may override fox / tx / idle below).
beaconIdle = FOXHUNT_BEACON_IDLE_DEF;
beaconTx = FOXHUNT_BEACON_TX_DEF;
foxFox = FOXHUNT_FOX_CALL; // identified beacon by default (legal on ham)
beaconPhaseTx = false;
// Read the callsign from the boot message (line 1), sanitised to what Morse can
// send; empty/erased leaves foxCall empty (the CALL id then falls back to bare MOE).
{
char call[FOXHUNT_CALLSIGN_MAX + 1];
uint8_t n = 0;
PY25Q16_ReadBuffer(FOXHUNT_CALLSIGN_ADDR, call, FOXHUNT_CALLSIGN_MAX);
call[FOXHUNT_CALLSIGN_MAX] = '\0';
for (uint8_t i = 0; i < FOXHUNT_CALLSIGN_MAX; i++) {
char c = call[i];
if (c == '\0' || (uint8_t)c == 0xFF)
break;
if (c >= 'a' && c <= 'z')
c -= 32;
if ((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '/')
foxCall[n++] = c;
}
foxCall[n] = '\0';
}
// Start in the hunt (RX) sub-mode, S-meter staircase gauge by default, then restore
// any persisted settings (attenuator, gauge, audio, beacon idle / fox / tx).
// Initialise the full configuration before loading it. The carrier-keying byte was
// appended (index 7): an older seven-byte config reads 0xFF there and LoadConfig's
// `<= 1` guard rejects it, so it stays at the default below — no migration needed.
attStep = 0;
foxGraphMode = FOXHUNT_GRAPH_BAR;
foxAudioMode = FOXHUNT_AUDIO_OFF;
FOXHUNT_LoadConfig(); // may override att / gauge / audio / idle / fox / tx
FOXHUNT_EnterHunt(); // applies the restored attStep and audio mode
// beaconMsg is (re)built by FOXHUNT_BeaconTransmit before every burst, and nothing
// reads it before the first burst, so no need to assemble it here.
beaconIdle = FOXHUNT_BEACON_IDLE_DEF;
beaconTx = FOXHUNT_BEACON_TX_DEF;
foxFox = FOXHUNT_FOX_CALL;
beaconCarrierKeyed = false; // TONE (F2A) by default
beaconPhaseTx = false;
FOXHUNT_LoadConfig();
// Prime the key state with whatever is held right now, so the side key that
// launched Fox Hunt (still down on a long-press assignment) is not taken for
// a fresh press and does not immediately toggle to the beacon.
// Ignore the key that launched the modal app until it has been released.
kbd.prev = kbd.current = KEYBOARD_GetKey();
gWasFKeyPressed = false; // start with the reverse-step arm cleared
// Always enter unlocked: the keypad lock is a transient safety toggle, not a
// persisted setting, so a previous session must not leave the pad locked.
gWasFKeyPressed = false;
foxLocked = false;
fHoldMs = 0;
fLongDone = false;
foxRunning = true;
}
static void FOXHUNT_LoadCallsign(void)
{
char call[FOXHUNT_CALLSIGN_MAX + 1];
uint8_t n = 0;
// Read the callsign from the boot message (line 1), sanitised to what Morse can
// send; empty/erased leaves foxCall empty (the CALL id then falls back to bare MOE).
PY25Q16_ReadBuffer(SETTINGS_BOOT_MESSAGE_LINE1_ADDR, call, FOXHUNT_CALLSIGN_MAX);
call[FOXHUNT_CALLSIGN_MAX] = '\0';
for (uint8_t i = 0; i < FOXHUNT_CALLSIGN_MAX; i++) {
char c = call[i];
if (c == '\0' || (uint8_t)c == 0xFF)
break;
if (c >= 'a' && c <= 'z')
c -= 32;
if ((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '/')
foxCall[n++] = c;
}
foxCall[n] = '\0';
}
static void FOXHUNT_End(void)
{
FOXHUNT_SaveConfig();
gWasFKeyPressed = false;
AUDIO_AudioPathOff();
BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, false);
RADIO_SelectVfos();
RADIO_SetupRegisters(true);
}
/* Both entry points stay in this translation unit so their small modal services
* can be shared when both features are enabled. The action table references only
* the enabled entry point(s); --gc-sections then drops the complete unused RX or
* Beacon call graph, including its private constants and bitmaps. */
void APP_RunFoxHunt(void)
{
FOXHUNT_Begin();
FOXHUNT_EnterHunt();
while (foxRunning) {
#if defined(ENABLE_UART) || defined(ENABLE_USB)
UART_ServiceCommands();
#endif
if (foxBeacon) {
FOXHUNT_BeaconTick();
continue;
}
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
// Keep the K5Viewer link alive and pick up any remote key.
K5VIEWER_ParseInput();
#endif
FOXHUNT_HandleKeys();
if (foxBeacon) // just switched to beacon: skip the RX work this tick
continue;
if (!foxRunning)
break;
curDbm = FOXHUNT_ReadDbm();
if (curDbm > peakDbm)
@@ -1482,24 +1482,23 @@ void APP_RunFoxHunt(void)
FOXHUNT_TickDelay(); // tick delay + smooth backlight fade
}
// Persist the session's settings on the way out (EXIT clears foxRunning) so
// they survive a later power cycle.
FOXHUNT_SaveConfig();
gWasFKeyPressed = false; // don't leak a pending reverse-step arm to the main screen
// Mute any pending tone, drop the PA (safety), then restore the normal VFO
// selection and RX config.
AUDIO_AudioPathOff();
BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, false);
RADIO_SelectVfos();
RADIO_SetupRegisters(true);
FOXHUNT_End();
}
void ACTION_FoxHunt(void)
void APP_RunBeacon(void)
{
APP_RunFoxHunt();
GUI_SelectNextDisplay(DISPLAY_MAIN);
FOXHUNT_Begin();
FOXHUNT_LoadCallsign();
FOXHUNT_EnterBeacon();
while (foxRunning) {
#if defined(ENABLE_UART) || defined(ENABLE_USB)
UART_ServiceCommands();
#endif
FOXHUNT_BeaconTick();
}
FOXHUNT_End();
}
#endif // ENABLE_FEAT_F4HWN_FOXHUNT
#endif // ENABLE_FEAT_F4HWN_FOXHUNT || ENABLE_FEAT_F4HWN_BEACON
+12 -3
View File
@@ -17,7 +17,7 @@
#ifndef APP_FOXHUNT_H
#define APP_FOXHUNT_H
#ifdef ENABLE_FEAT_F4HWN_FOXHUNT
#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON)
#include "keyboard_state.h"
@@ -46,12 +46,21 @@
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#endif
// Entry point wired as an assignable side-key action (see action.c).
#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
// Assignable resident/overlay launchers (see action.c).
void ACTION_FoxHunt(void);
void ACTION_Beacon(void);
#endif
#ifdef ENABLE_FEAT_F4HWN_FOXHUNT
// Self-contained modal loop that owns the screen and keypad until EXIT.
void APP_RunFoxHunt(void);
#endif
#ifdef ENABLE_FEAT_F4HWN_BEACON
void APP_RunBeacon(void);
#endif
#endif // ENABLE_FEAT_F4HWN_FOXHUNT
#endif // APP_FOXHUNT_H
+7 -7
View File
@@ -20,7 +20,7 @@
#include "app/chFrScanner.h"
#include "app/common.h"
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
#include "app/fm.h"
#endif
@@ -56,7 +56,7 @@ void GENERIC_Key_F(bool bKeyPressed, bool bKeyHeld)
COMMON_KeypadLockToggle();
}
else { // released
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
if ((gFmRadioMode || gScreenToDisplay != DISPLAY_MAIN) && gScreenToDisplay != DISPLAY_FM)
return;
#else
@@ -77,7 +77,7 @@ void GENERIC_Key_F(bool bKeyPressed, bool bKeyHeld)
}
}
else { // short pressed
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
if (gScreenToDisplay != DISPLAY_FM)
#endif
{
@@ -85,7 +85,7 @@ void GENERIC_Key_F(bool bKeyPressed, bool bKeyHeld)
return;
}
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
if (gFM_ScanState == FM_SCAN_OFF) { // not scanning
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
return;
@@ -132,7 +132,7 @@ void GENERIC_Key_PTT(bool bKeyPressed)
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
if (gFM_ScanState != FM_SCAN_OFF) { // FM radio is scanning .. stop
FM_PlayAndUpdate();
#ifdef ENABLE_VOICE
@@ -143,7 +143,7 @@ void GENERIC_Key_PTT(bool bKeyPressed)
}
#endif
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
if (gScreenToDisplay == DISPLAY_FM)
goto start_tx; // listening to the FM radio .. start TX'ing
#endif
@@ -200,7 +200,7 @@ cancel_tx:
done:
gPttDebounceCounter = 0;
if (gScreenToDisplay != DISPLAY_MENU
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
&& gRequestDisplayScreen != DISPLAY_FM
#endif
) {
+18 -9
View File
@@ -20,7 +20,7 @@
#include "app/app.h"
#include "app/chFrScanner.h"
#include "app/common.h"
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
#include "app/fm.h"
#endif
#include "app/generic.h"
@@ -31,8 +31,11 @@
#include "app/spectrum.h"
#endif
#ifdef ENABLE_FEAT_F4HWN_GAME
#include "app/breakout.h"
#if defined(ENABLE_FEAT_F4HWN_GAME) && !defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
#include "app/breakout.h" // resident game only; the overlay path uses app_menu.h
#endif
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_APPS
#include "apps/app_menu.h"
#endif
#include "audio.h"
@@ -269,9 +272,15 @@ static void processFKeyFunction(const KEY_Code_t Key, const bool beep)
break;
case KEY_7:
#ifdef ENABLE_FEAT_F4HWN_GAME
// F + 7 opens the overlay-apps menu when that support is built;
// otherwise it launches the resident game (GAME); otherwise VOX.
#if defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) || defined(ENABLE_FEAT_F4HWN_GAME)
if (!beep) {
APP_RunBreakout();
#if defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
APP_MenuOpen(); // overlay-apps selector
#else
APP_RunBreakout(); // resident game (no overlay support)
#endif
} else {
#endif
#ifdef ENABLE_VOX
@@ -279,7 +288,7 @@ static void processFKeyFunction(const KEY_Code_t Key, const bool beep)
//#else
// toggle_chan_scanlist();
#endif
#ifdef ENABLE_FEAT_F4HWN_GAME
#if defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) || defined(ENABLE_FEAT_F4HWN_GAME)
}
#endif
@@ -719,7 +728,7 @@ static void MAIN_Key_EXIT(bool bKeyPressed, bool bKeyHeld)
}
#endif
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
if (!gFmRadioMode)
#endif
{
@@ -754,7 +763,7 @@ static void MAIN_Key_EXIT(bool bKeyPressed, bool bKeyHeld)
return;
}
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
ACTION_FM();
#endif
return;
@@ -1055,7 +1064,7 @@ static void MAIN_Key_UP_DOWN(bool bKeyPressed, bool bKeyHeld, int8_t Direction)
void MAIN_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
{
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
if (gFmRadioMode && Key != KEY_PTT && Key != KEY_EXIT) {
if (!bKeyHeld && bKeyPressed)
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
+55 -43
View File
@@ -30,6 +30,10 @@
#include "driver/eeprom.h"
#include "driver/gpio.h"
#include "driver/keyboard.h"
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
#include "driver/mb_flash.h"
#include "ui/multiboot.h"
#endif
#include "frequencies.h"
#include "helper/battery.h"
#include "misc.h"
@@ -68,7 +72,7 @@ uint8_t gUnlockAllTxConfCnt;
//
EEPROM_ReadBuffer(0x1F88, &misc, 8);
misc.BK4819_XtalFreqLow = value;
EEPROM_WriteBuffer(0x1F88, &misc);
EEPROM_WriteBuffer(0x1F88, &misc, 8);
}
}
#endif
@@ -204,18 +208,18 @@ int MENU_GetLimits(uint8_t menu_id, int32_t *pMin, int32_t *pMax)
*pMax = ARRAY_SIZE(gSubMenu_W_N) - 1;
break;
#ifdef ENABLE_ALARM
case MENU_AL_MOD:
//*pMin = 0;
*pMax = ARRAY_SIZE(gSubMenu_AL_MOD) - 1;
break;
#endif
case MENU_RESET:
//*pMin = 0;
*pMax = ARRAY_SIZE(gSubMenu_RESET) - 1;
break;
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
case MENU_SET_CFG:
//*pMin = 0;
*pMax = MB_BANK_COUNT - 1;
break;
#endif
case MENU_COMPAND:
case MENU_ABR_ON_TX_RX:
//*pMin = 0;
@@ -229,11 +233,6 @@ int MENU_GetLimits(uint8_t menu_id, int32_t *pMin, int32_t *pMax)
break;
#endif
#ifndef ENABLE_FEAT_F4HWN
#ifdef ENABLE_AM_FIX
case MENU_AM_FIX:
#endif
#endif
#ifdef ENABLE_AUDIO_BAR
case MENU_MIC_BAR:
#endif
@@ -770,12 +769,6 @@ void MENU_AcceptSetting(void)
gEeprom.SCAN_LIST_ENABLED = gSubMenuSelection;
break;
#ifdef ENABLE_ALARM
case MENU_AL_MOD:
gEeprom.ALARM_MODE = gSubMenuSelection;
break;
#endif
case MENU_D_ST:
gEeprom.DTMF_SIDE_TONE = gSubMenuSelection;
break;
@@ -846,16 +839,6 @@ void MENU_AcceptSetting(void)
gRequestSaveChannel = 1;
return;
#ifndef ENABLE_FEAT_F4HWN
#ifdef ENABLE_AM_FIX
case MENU_AM_FIX:
gSetting_AM_fix = gSubMenuSelection;
gVfoConfigureMode = VFO_CONFIGURE_RELOAD;
gFlagResetVfos = true;
break;
#endif
#endif
#ifdef ENABLE_NOAA
case MENU_NOAA_S:
gEeprom.NOAA_AUTO_SCAN = gSubMenuSelection;
@@ -1092,6 +1075,12 @@ void MENU_ShowCurrentSetting(void)
gSubMenuSelection = 0;
break;
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
case MENU_SET_CFG:
gSubMenuSelection = MB_GetActiveBank();
break;
#endif
case MENU_R_DCS:
case MENU_R_CTCS:
{
@@ -1290,12 +1279,6 @@ void MENU_ShowCurrentSetting(void)
gSubMenuSelection = gEeprom.SCANLIST_PRIORITY_CH[1];
break;
#ifdef ENABLE_ALARM
case MENU_AL_MOD:
gSubMenuSelection = gEeprom.ALARM_MODE;
break;
#endif
case MENU_D_ST:
gSubMenuSelection = gEeprom.DTMF_SIDE_TONE;
break;
@@ -1346,14 +1329,6 @@ void MENU_ShowCurrentSetting(void)
gSubMenuSelection = gTxVfo->Modulation;
break;
#ifndef ENABLE_FEAT_F4HWN
#ifdef ENABLE_AM_FIX
case MENU_AM_FIX:
gSubMenuSelection = gSetting_AM_fix;
break;
#endif
#endif
#ifdef ENABLE_NOAA
case MENU_NOAA_S:
gSubMenuSelection = gEeprom.NOAA_AUTO_SCAN;
@@ -2006,6 +1981,9 @@ static void MENU_Key_MENU(const bool bKeyPressed, const bool bKeyHeld)
if (m == MENU_RESET ||
m == MENU_MEM_CH ||
m == MENU_DEL_CH ||
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
m == MENU_SET_CFG ||
#endif
m == MENU_MEM_NAME)
{
switch (gAskForConfirmation)
@@ -2034,6 +2012,40 @@ static void MENU_Key_MENU(const bool bKeyPressed, const bool bKeyHeld)
NVIC_SystemReset();
#endif
}
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
else if (m == MENU_SET_CFG)
{
/* Bind the chosen config bank, then reboot so it is mapped
* before any settings are read. Confirming the current bank
* is a no-op: do not wear a marker sector or reboot. */
if (gSubMenuSelection == MB_GetActiveBank())
{
gFlagAcceptSetting = false;
gIsInSubMenu = false;
gAskForConfirmation = 0;
SCANNER_Stop();
return;
}
const uint8_t err = MB_SetActiveBank(gSubMenuSelection);
if (err != MB_OK)
{
/* The previous redundant marker remains authoritative.
* Explain the failure and keep the selector open. */
UI_MultibootShowConfigError(err);
gAskForConfirmation = 0;
gRequestDisplayScreen = DISPLAY_MENU;
SCANNER_Stop();
return;
}
#if defined(ENABLE_OVERLAY)
overlay_FLASH_RebootToBootloader();
#else
NVIC_SystemReset();
#endif
}
#endif
gFlagAcceptSetting = true;
gIsInSubMenu = false;
-11
View File
@@ -14,7 +14,6 @@
* limitations under the License.
*/
#include "app/spectrum.h"
#include "am_fix.h"
#include "audio.h"
#include "misc.h"
@@ -608,10 +607,6 @@ uint16_t GetRssi()
// Discard first read (AGC may still be transitioning), keep second
BK4819_GetRSSI();
uint16_t rssi = BK4819_GetRSSI();
#ifdef ENABLE_AM_FIX
if (settings.modulationType == MODULATION_AM && gSetting_AM_fix)
rssi += AM_fix_get_gain_diff() * 2;
#endif
return rssi;
}
@@ -2474,12 +2469,6 @@ static void Tick()
if (gNextTimeslice)
{
gNextTimeslice = false;
#ifdef ENABLE_AM_FIX
if (settings.modulationType == MODULATION_AM && !lockAGC)
{
AM_fix_10ms(vfo); // allow AM_Fix to apply its AGC action
}
#endif
BACKLIGHT_Update();
}
+316 -26
View File
@@ -20,7 +20,7 @@
#if !defined(ENABLE_OVERLAY)
#include "py32f0xx.h"
#endif
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
#include "app/fm.h"
#endif
#include "app/uart.h"
@@ -45,6 +45,14 @@
#include "settings.h"
#include "version.h"
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
#include "driver/mb_flash.h"
#endif
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_APPS
#include "apps/app_overlay.h"
#endif
#if defined(ENABLE_OVERLAY)
#include "sram-overlay.h"
#endif
@@ -193,7 +201,8 @@ typedef union
#ifdef ENABLE_USB
static void SendReply_VCP(void *pReply, uint16_t Size)
{
static uint8_t VCP_ReplyBuf[MAX_REPLY_SIZE + sizeof(Header_t) + sizeof(Footer_t)];
static uint8_t VCP_ReplyBuf[MAX_REPLY_SIZE + sizeof(Header_t) + sizeof(Footer_t)]
__attribute__((aligned(4)));
// !!
if (Size > MAX_REPLY_SIZE)
@@ -201,11 +210,11 @@ static void SendReply_VCP(void *pReply, uint16_t Size)
return;
}
memcpy(VCP_ReplyBuf + sizeof(Header_t), pReply, Size);
uint8_t *pBody = VCP_ReplyBuf + sizeof(Header_t);
uint8_t *pFooter = pBody + Size;
Header_t *pHeader = (Header_t *)VCP_ReplyBuf;
Footer_t *pFooter = (Footer_t *)(VCP_ReplyBuf + sizeof(Header_t) + Size);
pReply = VCP_ReplyBuf + sizeof(Header_t);
memcpy(pBody, pReply, Size);
pReply = pBody;
if (bIsEncrypted)
{
@@ -215,23 +224,29 @@ static void SendReply_VCP(void *pReply, uint16_t Size)
pBytes[i] ^= Obfuscation[i % 16];
}
pHeader->ID = 0xCDAB;
pHeader->Size = Size;
/* Build the transport header/footer byte by byte. The reply body may have
* an odd size, so pFooter is not necessarily half-word aligned; casting it
* to Footer_t and storing ID as uint16_t can HardFault on Cortex-M0+. */
VCP_ReplyBuf[0] = 0xAB;
VCP_ReplyBuf[1] = 0xCD;
VCP_ReplyBuf[2] = (uint8_t)(Size & 0xFFu);
VCP_ReplyBuf[3] = (uint8_t)(Size >> 8);
// VCP_Send((uint8_t *)&Header, sizeof(Header));
// VCP_Send(pReply, Size);
if (bIsEncrypted)
{
pFooter->Padding[0] = Obfuscation[(Size + 0) % 16] ^ 0xFF;
pFooter->Padding[1] = Obfuscation[(Size + 1) % 16] ^ 0xFF;
pFooter[0] = Obfuscation[(Size + 0) % 16] ^ 0xFF;
pFooter[1] = Obfuscation[(Size + 1) % 16] ^ 0xFF;
}
else
{
pFooter->Padding[0] = 0xFF;
pFooter->Padding[1] = 0xFF;
pFooter[0] = 0xFF;
pFooter[1] = 0xFF;
}
pFooter->ID = 0xBADC;
pFooter[2] = 0xDC;
pFooter[3] = 0xBA;
// VCP_Send((uint8_t *)&Footer, sizeof(Footer));
@@ -249,6 +264,7 @@ static void SendReply(uint32_t Port, void *pReply, uint16_t Size)
}
#endif
#if defined(ENABLE_UART)
Header_t Header;
Footer_t Footer;
@@ -279,15 +295,17 @@ static void SendReply(uint32_t Port, void *pReply, uint16_t Size)
Footer.ID = 0xBADC;
UART_Send(&Footer, sizeof(Footer));
#endif
}
static void SendVersion(uint32_t Port)
{
REPLY_0514_t Reply;
Reply.Data.Padding[0] = Reply.Data.Padding[1] = 0;
Reply.Header.ID = 0x0515;
Reply.Header.Size = sizeof(Reply.Data);
strcpy(Reply.Data.Version, Version);
strncpy(Reply.Data.Version, Version, sizeof(Reply.Data.Version));
Reply.Data.bHasCustomAesKey = bHasCustomAesKey;
Reply.Data.bIsInLockScreen = bIsInLockScreen;
Reply.Data.Challenge[0] = gChallenge[0];
@@ -342,14 +360,14 @@ static void CMD_0514(uint32_t Port, const uint8_t *pBuffer)
}
#endif
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
gFmRadioCountdown_500ms = fm_radio_countdown_500ms;
#endif
gSerialConfigCountDown_500ms = 12; // 6 sec
if (gEeprom.BACKLIGHT_TIME < 61) // backlight is set to be always on
BACKLIGHT_TurnOff(); // turn the LCD backlight off
// Backlight left untouched: a serial session is neutral, so the normal BLTime
// inactivity countdown keeps running from the last keypress (no forced turn-off).
SendVersion(Port);
}
@@ -386,10 +404,14 @@ static void CMD_051B(uint32_t Port, const uint8_t *pBuffer)
gSerialConfigCountDown_500ms = 12; // 6 sec
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
gFmRadioCountdown_500ms = fm_radio_countdown_500ms;
#endif
// Reject reads that do not fit in the fixed-size reply buffer.
if (pCmd->Size > sizeof(Reply.Data.Data))
return;
memset(&Reply, 0, sizeof(Reply));
Reply.Header.ID = 0x051C;
Reply.Header.Size = pCmd->Size + 4;
@@ -403,7 +425,7 @@ static void CMD_051B(uint32_t Port, const uint8_t *pBuffer)
{
EEPROM_ReadBuffer(pCmd->Offset, Reply.Data.Data, pCmd->Size);
}
SendReply(Port, &Reply, pCmd->Size + 8);
}
@@ -417,6 +439,14 @@ static void CMD_051D(uint32_t Port, const uint8_t *pBuffer)
uint32_t Timestamp = 0;
/* Bound the write against the received frame: Data[] must hold pCmd->Size
* bytes, otherwise the loop below would read adjacent RAM and persist it to
* EEPROM (memory disclosure). A non-multiple-of-8 Size is NOT rejected: the
* Size/8 loop simply truncates the sub-page tail, matching the historical
* behavior CHIRP relies on for its final (unaligned) config block. */
if (pCmd->Header.Size < 8u + pCmd->Size)
return;
if(0) {}
#if defined(ENABLE_UART)
else if (Port == UART_PORT_UART)
@@ -442,7 +472,7 @@ static void CMD_051D(uint32_t Port, const uint8_t *pBuffer)
bReloadEeprom = false;
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
gFmRadioCountdown_500ms = fm_radio_countdown_500ms;
#endif
@@ -465,7 +495,7 @@ static void CMD_051D(uint32_t Port, const uint8_t *pBuffer)
if ((Offset < 0x0E98 || Offset >= 0x0EA0) || !bIsInLockScreen || pCmd->bAllowPassword)
{
EEPROM_WriteBuffer(Offset, &pCmd->Data[i * 8U]);
EEPROM_WriteBuffer(Offset, &pCmd->Data[i * 8U], 8);
}
}
@@ -512,7 +542,7 @@ static void CMD_052D(uint32_t Port, const uint8_t *pBuffer)
REPLY_052D_t Reply;
bool bIsLocked;
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
gFmRadioCountdown_500ms = fm_radio_countdown_500ms;
#endif
Reply.Header.ID = 0x052E;
@@ -543,6 +573,7 @@ static void CMD_052D(uint32_t Port, const uint8_t *pBuffer)
gIsLocked = bIsLocked;
Reply.Data.bIsLocked = bIsLocked;
Reply.Data.Padding[0] = Reply.Data.Padding[1] = Reply.Data.Padding[2] = 0;
SendReply(Port, &Reply, sizeof(Reply));
}
@@ -593,8 +624,8 @@ static void CMD_052F(uint32_t Port, const uint8_t *pBuffer)
}
#endif
if (gEeprom.BACKLIGHT_TIME < 61) // backlight is set to be always on
BACKLIGHT_TurnOff(); // turn the LCD backlight off
// Backlight left untouched: a serial session is neutral, so the normal BLTime
// inactivity countdown keeps running from the last keypress (no forced turn-off).
SendVersion(Port);
}
@@ -779,9 +810,29 @@ bool UART_IsCommandAvailable(uint32_t Port)
Crc = pUART_Command->Buffer[Size] | (pUART_Command->Buffer[Size + 1] << 8);
return CRC_Calculate(pUART_Command->Buffer, Size) == Crc;
return Size >= sizeof(Header_t) &&
pUART_Command->Header.Size <= Size - sizeof(Header_t) &&
CRC_Calculate(pUART_Command->Buffer, Size) == Crc;
}
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
/* Timestamp latched by the device-info handshake (0x0514) for this port. Slot
* writes/erases require it to match, like the EEPROM write command (CMD_051D). */
static uint32_t mb_port_timestamp(uint32_t Port)
{
#if defined(ENABLE_UART)
if (Port == UART_PORT_UART)
return UART_Timestamp;
#endif
#if defined(ENABLE_USB)
if (Port == UART_PORT_VCP)
return VCP_Timestamp;
#endif
(void)Port;
return 0;
}
#endif
void UART_HandleCommand(uint32_t Port)
{
UART_Command_t *pUART_Command;
@@ -852,6 +903,245 @@ void UART_HandleCommand(uint32_t Port)
#endif
break;
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
// ---- M4 slot management ("Firmware Slots") ------------------------
case 0x0720: // slot info: read the 64-byte header only (fast, no CRC)
{
if (pUART_Command->Header.Size < 1u) break; // needs Data[0] (slot)
gSerialConfigCountDown_500ms = 12; // keep serial mode alive (6 s)
uint8_t slot = pUART_Command->Data[0];
mb_slot_header_t hdr;
memset(&hdr, 0, sizeof(hdr));
uint8_t status = MB_SlotInfo(slot, &hdr);
struct __attribute__((packed)) {
Header_t Header;
uint8_t Slot;
uint8_t Status;
uint8_t Hdr[sizeof(mb_slot_header_t)];
} Reply;
Reply.Header.ID = 0x0721;
Reply.Header.Size = 2 + sizeof(mb_slot_header_t);
Reply.Slot = slot;
Reply.Status = status;
memcpy(Reply.Hdr, &hdr, sizeof(hdr));
SendReply(Port, &Reply, sizeof(Reply));
break;
}
case 0x0722: // slot erase: wipe the whole 128 KiB slot region
{
if (pUART_Command->Header.Size < 6u) break; // needs Data[0] slot + Data[2..5] timestamp
gSerialConfigCountDown_500ms = 12; // keep serial mode alive (6 s)
uint8_t slot = pUART_Command->Data[0];
uint32_t ts = (uint32_t)pUART_Command->Data[2]
| ((uint32_t)pUART_Command->Data[3] << 8)
| ((uint32_t)pUART_Command->Data[4] << 16)
| ((uint32_t)pUART_Command->Data[5] << 24);
uint8_t status = (ts != mb_port_timestamp(Port))
? MB_ERR_AUTH : MB_SlotErase(slot);
struct __attribute__((packed)) {
Header_t Header;
uint8_t Slot;
uint8_t Status;
} Reply;
Reply.Header.ID = 0x0723;
Reply.Header.Size = 2;
Reply.Slot = slot;
Reply.Status = status;
SendReply(Port, &Reply, sizeof(Reply));
break;
}
case 0x0724: // slot write: program bytes at slot+offset (slot pre-erased)
{
if (pUART_Command->Header.Size < 12u)
break;
gSerialConfigCountDown_500ms = 12; // keep serial mode alive (6 s)
uint8_t slot = pUART_Command->Data[0];
uint32_t offset = (uint32_t)pUART_Command->Data[2]
| ((uint32_t)pUART_Command->Data[3] << 8)
| ((uint32_t)pUART_Command->Data[4] << 16)
| ((uint32_t)pUART_Command->Data[5] << 24);
uint16_t len = (uint16_t)(pUART_Command->Data[6]
| ((uint16_t)pUART_Command->Data[7] << 8));
uint32_t ts = (uint32_t)pUART_Command->Data[8]
| ((uint32_t)pUART_Command->Data[9] << 8)
| ((uint32_t)pUART_Command->Data[10] << 16)
| ((uint32_t)pUART_Command->Data[11] << 24);
uint8_t status;
if (ts != mb_port_timestamp(Port))
status = MB_ERR_AUTH;
else if (len > pUART_Command->Header.Size - 12u)
status = MB_ERR_SIZE;
else
status = MB_SlotWrite(slot, offset, &pUART_Command->Data[12], len);
struct __attribute__((packed)) {
Header_t Header;
uint8_t Slot;
uint8_t Status;
} Reply;
Reply.Header.ID = 0x0725;
Reply.Header.Size = 2;
Reply.Slot = slot;
Reply.Status = status;
SendReply(Port, &Reply, sizeof(Reply));
break;
}
case 0x0726: // slot validate: full image CRC-32, no reflash
{
if (pUART_Command->Header.Size < 1u) break; // needs Data[0] (slot)
gSerialConfigCountDown_500ms = 12; // keep serial mode alive (6 s)
uint8_t slot = pUART_Command->Data[0];
uint32_t crc = 0;
uint8_t status = MB_ValidateSlot(slot, NULL, &crc);
struct __attribute__((packed)) {
Header_t Header;
uint32_t Crc32; // offset 4: 4-byte aligned, no unaligned store
uint8_t Slot;
uint8_t Status;
} Reply;
Reply.Header.ID = 0x0727;
Reply.Header.Size = 6;
Reply.Crc32 = crc;
Reply.Slot = slot;
Reply.Status = status;
SendReply(Port, &Reply, sizeof(Reply));
break;
}
case 0x0728: // config reset: wipe the 64 KiB of a config bank (1..4)
{
if (pUART_Command->Header.Size < 6u) break; // needs Data[0] bank + Data[2..5] timestamp
gSerialConfigCountDown_500ms = 12; // keep serial mode alive (6 s)
uint8_t bank = pUART_Command->Data[0];
uint32_t ts = (uint32_t)pUART_Command->Data[2]
| ((uint32_t)pUART_Command->Data[3] << 8)
| ((uint32_t)pUART_Command->Data[4] << 16)
| ((uint32_t)pUART_Command->Data[5] << 24);
uint8_t status = (ts != mb_port_timestamp(Port))
? MB_ERR_AUTH : MB_BankErase(bank);
struct __attribute__((packed)) {
Header_t Header;
uint8_t Bank; // echoes the erased bank (same wire layout as slot replies)
uint8_t Status;
} Reply;
Reply.Header.ID = 0x0729;
Reply.Header.Size = 2;
Reply.Bank = bank;
Reply.Status = status;
SendReply(Port, &Reply, sizeof(Reply));
break;
}
#endif
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_APPS
// ---- overlay-app slot management ("Apps") -------------------------
// Parallels the firmware-slot family (0x072x); targets the external-flash
// Apps region. External flash only, never brick-critical.
case 0x0730: // app slot info: read the 64-byte header only
{
if (pUART_Command->Header.Size < 1u) break; // needs Data[0] (slot)
gSerialConfigCountDown_500ms = 12; // keep serial mode alive (6 s)
uint8_t slot = pUART_Command->Data[0];
app_header_t hdr;
memset(&hdr, 0, sizeof(hdr));
uint8_t status = APP_SlotInfo(slot, &hdr);
struct __attribute__((packed)) {
Header_t Header;
uint8_t Slot;
uint8_t Status;
uint8_t Hdr[sizeof(app_header_t)];
} Reply;
Reply.Header.ID = 0x0731;
Reply.Header.Size = 2 + sizeof(app_header_t);
Reply.Slot = slot;
Reply.Status = status;
memcpy(Reply.Hdr, &hdr, sizeof(hdr));
SendReply(Port, &Reply, sizeof(Reply));
break;
}
case 0x0732: // app slot erase: wipe the whole 8 KiB slot region
{
if (pUART_Command->Header.Size < 6u) break; // needs Data[0] slot + Data[2..5] timestamp
gSerialConfigCountDown_500ms = 12; // keep serial mode alive (6 s)
uint8_t slot = pUART_Command->Data[0];
uint32_t ts = (uint32_t)pUART_Command->Data[2]
| ((uint32_t)pUART_Command->Data[3] << 8)
| ((uint32_t)pUART_Command->Data[4] << 16)
| ((uint32_t)pUART_Command->Data[5] << 24);
uint8_t status = (ts != mb_port_timestamp(Port))
? APP_ERR_AUTH : APP_SlotErase(slot);
struct __attribute__((packed)) {
Header_t Header;
uint8_t Slot;
uint8_t Status;
} Reply;
Reply.Header.ID = 0x0733;
Reply.Header.Size = 2;
Reply.Slot = slot;
Reply.Status = status;
SendReply(Port, &Reply, sizeof(Reply));
break;
}
case 0x0734: // app slot write: program bytes at slot+offset (pre-erased)
{
if (pUART_Command->Header.Size < 12u)
break;
gSerialConfigCountDown_500ms = 12; // keep serial mode alive (6 s)
uint8_t slot = pUART_Command->Data[0];
uint32_t offset = (uint32_t)pUART_Command->Data[2]
| ((uint32_t)pUART_Command->Data[3] << 8)
| ((uint32_t)pUART_Command->Data[4] << 16)
| ((uint32_t)pUART_Command->Data[5] << 24);
uint16_t len = (uint16_t)(pUART_Command->Data[6]
| ((uint16_t)pUART_Command->Data[7] << 8));
uint32_t ts = (uint32_t)pUART_Command->Data[8]
| ((uint32_t)pUART_Command->Data[9] << 8)
| ((uint32_t)pUART_Command->Data[10] << 16)
| ((uint32_t)pUART_Command->Data[11] << 24);
uint8_t status;
if (ts != mb_port_timestamp(Port))
status = APP_ERR_AUTH;
else if (len > pUART_Command->Header.Size - 12u)
status = APP_ERR_SIZE;
else
status = APP_SlotWrite(slot, offset, &pUART_Command->Data[12], len);
struct __attribute__((packed)) {
Header_t Header;
uint8_t Slot;
uint8_t Status;
} Reply;
Reply.Header.ID = 0x0735;
Reply.Header.Size = 2;
Reply.Slot = slot;
Reply.Status = status;
SendReply(Port, &Reply, sizeof(Reply));
break;
}
case 0x0736: // app slot validate: header only (code CRC is checked at launch)
{
if (pUART_Command->Header.Size < 1u) break; // needs Data[0] (slot)
gSerialConfigCountDown_500ms = 12; // keep serial mode alive (6 s)
uint8_t slot = pUART_Command->Data[0];
uint8_t status = APP_ValidateSlot(slot, NULL);
struct __attribute__((packed)) {
Header_t Header;
uint8_t Slot;
uint8_t Status;
} Reply;
Reply.Header.ID = 0x0737;
Reply.Header.Size = 2;
Reply.Slot = slot;
Reply.Status = status;
SendReply(Port, &Reply, sizeof(Reply));
break;
}
#endif
#ifdef ENABLE_UART_RW_BK_REGS
case 0x0601:
CMD_0601_ReadBK4819Reg(Port, pUART_Command->Buffer);
+291
View File
@@ -0,0 +1,291 @@
/* Copyright 2026 Armel F4HWN
* https://github.com/armel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Overlay-app ABI (POC).
*
* A leaf, modal "overlay app" is a self-contained code blob stored in the
* external SPI flash and copied into the 4 KiB overlay RAM (the PY25Q16 sector
* cache) to run, then discarded. It is linked with NOCROSSREFS and must not
* reference resident firmware symbols: every service it needs is reached through
* this table, which the loader fills and passes to the entry point.
*
* void app_main(const app_api_t *api); // entry, at blob offset 0
*
* Both the firmware loader and the app include THIS header. Within one ABI
* major, app_api_t is append-only: existing fields may never move, disappear or
* change signature. Append a service and bump APP_API_LEVEL; only apps using
* that service need the new level. A breaking layout change bumps
* APP_ABI_MAJOR and resets APP_API_LEVEL to 1.
*/
#ifndef APPS_APP_API_H
#define APPS_APP_API_H
#include <stdint.h>
#include <stdbool.h>
/* First unpublished/public baseline: all services currently present below are
* ABI major 1, API level 1. */
#define APP_ABI_MAJOR 1u
#define APP_API_LEVEL 1u
/* 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. */
enum {
APP_KEY_0 = 0,
APP_KEY_1 = 1,
APP_KEY_2 = 2,
APP_KEY_3 = 3,
APP_KEY_4 = 4,
APP_KEY_5 = 5,
APP_KEY_6 = 6,
APP_KEY_7 = 7,
APP_KEY_8 = 8,
APP_KEY_9 = 9,
APP_KEY_MENU = 10,
APP_KEY_UP = 11,
APP_KEY_DOWN = 12,
APP_KEY_EXIT = 13,
APP_KEY_STAR = 14,
APP_KEY_F = 15,
APP_KEY_PTT = 16,
APP_KEY_INVALID = 19,
APP_KEY_SAVER = 0xFD, /* virtual: saver active, skip this app frame */
APP_KEY_WAKE = 0xFE, /* virtual: saver dismissed, redraw without action */
};
/* The framebuffer is the resident gFrameBuffer[FRAME_LINES][128]; the app draws
* into it and calls a blit_* to push it to the LCD. */
typedef uint8_t (*app_fb_t)[128];
/* Broadcast FM shared state (mirrors gEeprom.FM_*), read/written via fm_state. */
#define APP_FM_CH_MAX 48
typedef struct {
uint16_t freq_playing; /* current tuned frequency (0.1 MHz) */
uint16_t sel_freq; /* last VFO frequency */
uint8_t band; /* 0..3 */
uint8_t is_mr; /* memory mode */
uint8_t sel_ch; /* selected memory channel 0..47 */
} 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,
APP_TRIVFO_GUI_CLASSIC = 1u << 6,
APP_TRIVFO_PTT_ONEPUSH = 1u << 7,
};
enum {
APP_TRIVFO_SCAN = 0,
APP_TRIVFO_RX = 1,
APP_TRIVFO_HOLD = 2,
APP_TRIVFO_TX_STATE = 3,
};
/* Pointer-free BEAM channel description. The resident bridge translates this
* stable ABI type to/from feature-dependent VFO_Info_t. */
typedef struct {
uint32_t rx_frequency;
uint32_t tx_offset_frequency;
uint8_t rx_code;
uint8_t tx_code;
uint8_t rx_codetype;
uint8_t tx_codetype;
uint8_t modulation;
uint8_t tx_offset_direction;
uint8_t tx_lock;
uint8_t busy_channel_lock;
uint8_t output_power;
uint8_t channel_bandwidth;
uint8_t frequency_reverse;
uint8_t dtmf_ptt_id_mode;
uint8_t dtmf_decoding_enable;
uint8_t step_setting;
uint8_t scrambling_type;
uint8_t band;
uint8_t scanlist;
uint8_t compander;
char name[16]; /* fixed-width wire field; NUL termination is not required */
} app_beam_channel_t;
_Static_assert(sizeof(app_beam_channel_t) == 44u,
"BEAM ABI/wire channel layout changed");
enum {
APP_BEAM_RX_WAIT = 0,
APP_BEAM_RX_READY = 1,
APP_BEAM_RX_ERROR = 2,
};
/* CRITICAL PERSISTENCE RULE
*
* The app executes from the RAM buffer also used as PY25Q16's sector cache.
* Therefore no API callback may erase or write external flash while app_main()
* is running: a read-modify-write would overwrite the executing app. Services
* such as cfg_save, fm_commit and beam_save must only stage resident RAM state;
* APP_LaunchOverlay commits it after app_main() returns. */
typedef struct app_api {
/* Fixed four-byte prefix; services remain naturally pointer-aligned. */
uint8_t abi_major; /* == APP_ABI_MAJOR */
uint8_t api_level; /* == APP_API_LEVEL */
uint16_t api_size; /* sizeof(app_api_t), for optional probing */
app_fb_t fb; /* -> gFrameBuffer */
/* ---- display ---- */
void (*display_clear)(void); /* UI_DisplayClear */
void (*status_clear)(void); /* UI_StatusClear */
void (*draw_line)(app_fb_t fb, int16_t x1, int16_t y1,
int16_t x2, int16_t y2, bool black); /* UI_DrawLineBuffer */
void (*draw_rect)(app_fb_t fb, int16_t x1, int16_t y1,
int16_t x2, int16_t y2, bool black); /* UI_DrawRectangleBuffer */
void (*print_bold)(const char *s, uint8_t start,
uint8_t end, uint8_t line); /* UI_PrintStringSmallBold */
void (*print_tiny)(const char *s, uint8_t x, uint8_t y,
bool statusbar, bool fill); /* GUI_DisplaySmallest */
void (*blit_full)(void); /* ST7565_BlitFullScreen */
void (*blit_line)(unsigned line); /* ST7565_BlitLine */
void (*blit_status)(void); /* ST7565_BlitStatusLine */
/* ---- input / system ---- */
uint8_t (*get_key)(void); /* KEYBOARD_GetKey, returns an APP_KEY_* code */
void (*delay_ms)(uint32_t ms); /* SYSTEM_DelayMs */
/* ---- audio / indicator ---- */
void (*play_tone)(uint16_t tone, uint16_t ms); /* full BK4819 tone burst + AF path */
void (*led)(bool on); /* green GPIO indicator */
/* ---- extra text drawing ---- */
void (*print_normal)(const char *s, uint8_t start, uint8_t end, uint8_t line); /* UI_PrintStringSmallNormal */
void (*print_inverse)(const char *s, uint8_t x, uint8_t line,
bool statusbar, bool fill, uint8_t endX); /* GUI_DisplaySmallestInverse */
void (*display_freq)(const char *s, uint8_t x, uint8_t y, bool statusbar); /* UI_DisplayFrequency (big font) */
/* ---- BK4819 radio access ---- */
int16_t (*rssi_dbm)(void); /* corrected RSSI of the RX VFO, dBm */
uint16_t (*bk_read)(uint8_t reg); /* BK4819_ReadRegister */
void (*bk_write)(uint8_t reg, uint16_t v);/* BK4819_WriteRegister */
void (*set_agc)(bool on); /* BK4819_SetAGC */
void (*set_af)(uint8_t mode); /* BK4819_SetAF (APP_AF_* below) */
void (*audio_path)(bool on); /* AUDIO_AudioPathOn/Off */
void (*prepare_tone)(void); /* BK4819_PrepareToPlayTone(true) */
void (*play_tone_raw)(uint16_t hz, uint16_t ms); /* BK4819_PlayToneRaw */
void (*tones_off_rx)(void); /* BK4819_TurnsOffTones_TurnsOnRX */
uint32_t (*rx_freq)(void); /* current RX VFO frequency (x10 Hz) */
/* ---- config persistence (deferred: staged now, committed on exit) ---- */
void (*cfg_load)(uint8_t *buf, uint8_t len); /* read the app's saved config bytes */
void (*cfg_save)(const uint8_t *buf, uint8_t len); /* stage bytes; resident commits after the app returns */
/* ---- battery + backlight ---- */
void (*draw_battery)(void); /* UI_DrawStatusBattery into the status line */
void (*battery_sample)(void); /* periodic ADC sample so the level stays live */
void (*backlight_on)(void); /* BACKLIGHT_TurnOn */
void (*backlight_update)(void); /* resident fade + BLTime service */
void (*audio_scope)(uint8_t line, bool active); /* shared MAIN microphone scope */
uint8_t *status_line; /* -> gStatusLine (for status-bar icons) */
/* ---- TX (beacon) ---- */
uint8_t (*tx_state)(void); /* 0 = OK to transmit, else a denial code */
void (*tx_set_params)(void); /* RADIO_SetTxParameters (key up: carrier+PA) */
void (*tx_tone)(uint16_t hz);/* BK4819_TransmitTone prime (MCW tone) */
void (*tx_mute)(bool on); /* key the tone on(false)/off(true) via TxMute */
void (*tx_end)(void); /* PA off + RADIO_SetupRegisters (back to RX) */
void (*tx_carrier)(bool on); /* gate the PA on/off (beacon CARR carrier keying) */
uint32_t (*tx_freq)(void); /* current TX VFO frequency (x10 Hz) */
void (*boot_callsign)(char *buf, uint8_t len); /* sanitised callsign from the boot message */
void (*print_string)(const char *s, uint8_t start, uint8_t end,
uint8_t line, uint8_t width); /* UI_PrintString (big font) */
/* ---- broadcast FM (BK1080), sovereign: no BK4819 dual-watch ---- */
void (*fm_enter)(uint16_t freq, uint8_t band); /* BK1080_Init + antenna filter + audio on */
void (*fm_exit)(void); /* audio off + BK1080_Init0 + restore filter */
void (*fm_set_freq)(uint16_t freq, uint8_t band);/* BK1080_SetFrequency (freq in 0.1 MHz) */
uint16_t (*fm_lo)(uint8_t band); /* band low limit (0.1 MHz) */
uint16_t (*fm_hi)(uint8_t band); /* band high limit (0.1 MHz) */
void (*fm_mute)(bool mute); /* BK1080_Mute */
int8_t (*fm_valid)(uint16_t freq, uint16_t lo); /* FM_CheckFrequencyLock: 0 = station */
uint16_t *fm_channels; /* -> gFM_Channels[APP_FM_CH_MAX], shared RAM r/w */
void (*fm_state)(app_fm_state_t *s, bool write);/* read/write the resident gEeprom.FM_* */
void (*fm_commit)(void); /* deferred SETTINGS_SaveFM (config + channels) */
/* ---- navigation (API level 1 baseline) ----
* Convert a raw APP_KEY_UP/DOWN into a semantic value direction:
* UV-K5 UP/DOWN -> +1/-1
* UV-K1 LEFT/RIGHT -> -1/+1
* Returns 0 for any other key. Keep get_key() raw for spatial controls. */
int8_t (*nav_dir)(uint8_t key);
/* ---- triple VFO (optional resident capability APP_CAP_TRIVFO) ----
* 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); /* physical PTT edge; resident applies SetPTT */
/* ---- BEAM channel transfer (optional resident capability APP_CAP_BEAM) ---- */
void (*beam_prepare)(void); /* tune the fixed narrow-band FSK channel */
void (*beam_leave)(void); /* defensively stop FSK before app return */
void (*beam_get)(app_beam_channel_t *channel); /* export selected VFO */
uint16_t (*beam_save)(const app_beam_channel_t *channel); /* first free MR, or 0xffff */
void (*beam_send)(uint16_t *packet); /* transmit one 36-word FSK packet */
void (*beam_rx)(bool start); /* arm or stop FSK reception */
uint8_t (*beam_rx_poll)(uint16_t *packet); /* APP_BEAM_RX_* */
void (*beam_draw)(const char *status); /* MAIN display with one BEAM center line */
} app_api_t;
/* BK4819 AF modes for set_af (mirror driver/bk4819.h values). */
enum { APP_AF_MUTE = 0, APP_AF_FM = 1, APP_AF_AM = 7 };
/* Register ids the apps use (mirror driver/bk4819-regs.h). */
enum { APP_BK_REG_13 = 0x13 };
/* Entry point every app blob exports, placed at blob offset 0. */
typedef void (*app_entry_t)(const app_api_t *api);
#endif /* APPS_APP_API_H */
+335
View File
@@ -0,0 +1,335 @@
/* Copyright 2026 Armel F4HWN
* https://github.com/armel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "apps/app_overlay.h"
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_APPS
#include <string.h>
#include "apps/app_menu.h"
#include "app/app.h"
#include "driver/backlight.h"
#include "driver/st7565.h"
#include "driver/keyboard.h"
#include "driver/system.h"
#include "driver/gpio.h"
#include "ui/helper.h"
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
#include "k5viewer.h"
#endif
/* "F4HWN APPS" banner and the same thin separator used by the multiboot
* selector. Bit 3 leaves room for the selected-row capsule's top edge. */
static void app_status_bar(void)
{
UI_StatusClear();
GUI_DisplaySmallestInverse("F4HWN APPS", 44, 0, true, true, 84);
for (uint8_t x = 2u; x < LCD_WIDTH - 2u; x++)
gFrameBuffer[0][x] |= 0x08u;
}
/* Bottom key hints, matching the multiboot selector. */
static void app_key_hints(void)
{
const uint8_t sp = 6u;
const char *act_exit = "QUIT";
const uint8_t ae = (uint8_t)strlen(act_exit);
const uint8_t xm = 4u;
const uint8_t xe = (uint8_t)(124u - ae * 4u - sp - 16u);
GUI_DisplaySmallestInverse("MENU", xm, 6, false, true, (uint8_t)(xm + 16u));
GUI_DisplaySmallest("RUN", (uint8_t)(xm + 16u + sp), 49, false, true);
GUI_DisplaySmallestInverse("EXIT", xe, 6, false, true, (uint8_t)(xe + 16u));
GUI_DisplaySmallest(act_exit, (uint8_t)(xe + 16u + sp), 49, false, true);
}
/* Fixed selection capsule around the primary information (the app name).
* Slot number stays in the normal font; size is plain 3x5 metadata. */
#define APP_NAME_BOX_START 12u
#define APP_NAME_BOX_END 102u
#define APP_NAME_TEXT_X 14u
static void app_wait_release(void);
static void app_invert_name(uint8_t line)
{
gFrameBuffer[line][APP_NAME_BOX_START] ^= 0x7Fu;
for (uint8_t x = APP_NAME_BOX_START + 1u; x < APP_NAME_BOX_END; x++)
{
gFrameBuffer[line][x] ^= 0xFFu;
gFrameBuffer[line - 1u][x] ^= 0x80u;
}
gFrameBuffer[line][APP_NAME_BOX_END] ^= 0x7Fu;
}
/* Debounced blocking key read, then wait for release (mirrors mb_get_key). */
static KEY_Code_t app_get_key(void)
{
for (;;)
{
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
/* APP_MenuOpen() is modal and does not return to APP_Update(). Keep
* serial key injection and the viewer connection alive while waiting. */
K5VIEWER_ParseInput();
#endif
APP_ModalBacklightTick(true);
if (APP_IsScreenSaverDisplayed())
{
if (KEYBOARD_GetKey() != KEY_INVALID)
{
APP_ModalScreenSaverExit();
BACKLIGHT_TurnOn();
app_wait_release();
return KEY_INVALID;
}
SYSTEM_DelayMs(10);
continue;
}
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
K5VIEWER_Update(false);
#endif
KEY_Code_t key = KEYBOARD_Poll();
if (key != KEY_INVALID)
{
SYSTEM_DelayMs(30);
if (KEYBOARD_Poll() == key)
{
BACKLIGHT_TurnOn();
while (KEYBOARD_Poll() != KEY_INVALID)
{
SYSTEM_DelayMs(10);
APP_ModalBacklightTick(false);
}
return key;
}
}
SYSTEM_DelayMs(10);
}
}
static void app_wait_release(void)
{
uint8_t stable = 0;
while (stable < 10u)
{
if (!GPIO_IsPttPressed() && KEYBOARD_Poll() == KEY_INVALID)
stable++;
else
stable = 0;
SYSTEM_DelayMs(10);
APP_ModalBacklightTick(true);
}
}
static void app_copy(char *dst, uint8_t cap, const char *src, uint8_t src_cap)
{
uint8_t n = 0;
while (n + 1u < cap && n < src_cap && src[n])
{
dst[n] = src[n];
n++;
}
dst[n] = '\0';
}
/* Display the code payload rounded up to 0.1 KiB, so the compact value never
* understates the space occupied by the app. The space before "KB" is omitted
* because this secondary value is rendered in the tiny 3x5 font. */
static void app_format_size(char out[6], uint32_t bytes)
{
if (bytes > APP_OVERLAY_MAX)
{
memcpy(out, "--KB", 5u);
return;
}
const uint16_t tenths = (uint16_t)((bytes * 10u + 1023u) / 1024u);
out[0] = (char)('0' + tenths / 10u);
out[1] = '.';
out[2] = (char)('0' + tenths % 10u);
out[3] = 'K';
out[4] = 'B';
out[5] = '\0';
}
/* Human-readable action for an APP_LaunchOverlay / APP_ValidateSlot failure. */
static const char *app_err_text(const app_header_t *header, uint8_t rc)
{
switch (rc)
{
case APP_ERR_SLOT: return "BAD SLOT";
case APP_ERR_MAGIC: return "NO APP";
case APP_ERR_ABI:
/* api_min == 0 also identifies pre-reset development blobs whose
* former uint16_t ABI value occupies these two bytes. */
if (header->api_min == 0u || header->abi_major < APP_ABI_MAJOR)
return "UPDATE APP";
return "UPDATE FIRMWARE";
case APP_ERR_NOT_COMMITTED: return "REINSTALL APP";
case APP_ERR_SIZE: return "UPDATE APP";
case APP_ERR_CRC: return "REINSTALL APP";
case APP_ERR_VMA: return "UPDATE APP";
case APP_ERR_AUTH: return "AUTH";
case APP_ERR_CAP: return "NOT SUPPORTED";
default: return "ERROR";
}
}
/* A launch failed: name the app and the action to take, then wait for a key. Without this
* an incompatible app would silently "do nothing" when selected. */
static void app_show_error(const app_header_t *header, uint8_t rc)
{
char nm[19];
app_copy(nm, sizeof(nm), header->name, APP_NAME_LEN);
UI_DisplayClear();
UI_StatusClear();
GUI_DisplaySmallestInverse("APP ERROR", 46, 0, true, true, 82);
UI_PrintStringSmallNormal(nm, 2, 0, 2); /* which app */
UI_PrintStringSmallNormal(app_err_text(header, rc), 2, 0, 4); /* action */
UI_PrintStringSmallNormal("Press any key", 2, 0, 6);
ST7565_BlitStatusLine();
ST7565_BlitFullScreen();
app_get_key(); /* blocking: dismiss on any key */
}
/* Five visible slots; line 0 holds the separator and line 6 the key hints. */
#define APP_MENU_ROWS 5u
#define APP_MENU_SLOT_COUNT 8u
_Static_assert(APP_MENU_SLOT_COUNT <= APP_SLOT_COUNT,
"APP_MENU_SLOT_COUNT exceeds the physical app slot count");
void APP_MenuOpen(void)
{
APP_ModalScreenSaverExit();
BACKLIGHT_TurnOn();
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
/* Detach the modal selector from the key state that triggered F+7. The
* normal K5Viewer updater suppresses frames while a key is held; without
* clearing this stale state, the selector could never publish its first
* frame to the viewer. */
gKeyReading0 = KEY_INVALID;
gKeyReading1 = KEY_INVALID;
#endif
/* Apps are installed from UV Studio (0x073x) into physical slots 0..N-1,
* shown here as 1..N. Keep empty slots in the list so their location is
* visible and selectable while scrolling. */
app_header_t hdr[APP_MENU_SLOT_COUNT];
bool installed[APP_MENU_SLOT_COUNT];
for (uint8_t slot = 0; slot < APP_MENU_SLOT_COUNT; slot++)
{
installed[slot] = APP_SlotInfo(slot, &hdr[slot]) == APP_OK &&
(hdr[slot].flags & APP_FLAG_COMMITTED);
}
/* Remember the physical slot and scrolling window across menu openings. */
static uint8_t sel = 0;
static uint8_t top = 0; /* first visible row of the scrolling window */
if (sel >= APP_MENU_SLOT_COUNT || top > APP_MENU_SLOT_COUNT - APP_MENU_ROWS)
sel = top = 0u;
app_wait_release();
for (;;)
{
UI_DisplayClear();
app_status_bar(); /* also wipes the VFO status line (DW, battery, ...) */
/* Slide [top, top+APP_MENU_ROWS) so it always contains the selection. */
if (sel < top)
top = sel;
else if (sel >= (uint8_t)(top + APP_MENU_ROWS))
top = (uint8_t)(sel - APP_MENU_ROWS + 1u);
for (uint8_t slot = top;
slot < APP_MENU_SLOT_COUNT && (uint8_t)(slot - top) < APP_MENU_ROWS;
slot++)
{
char number[2];
char name[14];
char size[6];
const uint8_t visible_number = (uint8_t)(slot + 1u);
const uint8_t fbLine = (uint8_t)(slot - top + 1u);
number[0] = (char)('0' + visible_number);
number[1] = '\0';
UI_PrintStringSmallNormal(number, 2u, 0, fbLine);
if (installed[slot])
{
app_copy(name, sizeof(name), hdr[slot].name, APP_NAME_LEN);
app_format_size(size, hdr[slot].code_size);
UI_PrintStringSmallNormal(name, APP_NAME_TEXT_X, 0, fbLine);
GUI_DisplaySmallest(size,
(uint8_t)(LCD_WIDTH - 2u - strlen(size) * 4u),
(uint8_t)(fbLine * 8u + 1u), false, true);
}
else
{
UI_PrintStringSmallNormal("Empty", APP_NAME_TEXT_X, 0, fbLine);
}
if (slot == sel)
app_invert_name(fbLine);
}
app_key_hints();
ST7565_BlitStatusLine();
ST7565_BlitFullScreen();
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
K5VIEWER_Update(false);
#endif
const KEY_Code_t key = app_get_key();
if (key == KEY_EXIT)
return;
switch (key)
{
case KEY_UP:
sel = (sel == 0u) ? (uint8_t)(APP_MENU_SLOT_COUNT - 1u) : (uint8_t)(sel - 1u);
break;
case KEY_DOWN:
sel = (uint8_t)((sel + 1u) % APP_MENU_SLOT_COUNT);
break;
case KEY_MENU:
{
if (!installed[sel])
break;
const uint8_t rc = APP_LaunchOverlay(sel); /* runs until the app exits */
if (rc != APP_OK)
app_show_error(&hdr[sel], rc); /* no longer silent */
app_wait_release();
break;
}
default:
break;
}
}
}
#endif /* ENABLE_FEAT_F4HWN_OVERLAY_APPS */
+9 -19
View File
@@ -1,6 +1,5 @@
/* Copyright 2023 OneOfEleven
* https://github.com/DualTachyon
/* Copyright 2026 Armel F4HWN
* https://github.com/armel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,21 +14,12 @@
* limitations under the License.
*/
#ifndef AM_FIXH
#ifndef APPS_APP_MENU_H
#define APPS_APP_MENU_H
#include <stdint.h>
#include <stdbool.h>
/* Blocking "Apps" selector: scans the overlay-app slots, lists the committed
* ones by name, and launches the chosen one. UP/DOWN move, MENU launches, EXIT
* closes. Returns when the user leaves. */
void APP_MenuOpen(void);
#ifdef ENABLE_AM_FIX
void AM_fix_init(void);
void AM_fix_reset(const unsigned vfo);
void AM_fix_10ms(const unsigned vfo);
#ifdef ENABLE_AM_FIX_SHOW_DATA
void AM_fix_print_data(const unsigned vfo, char *s);
#endif
int8_t AM_fix_get_gain_diff();
void AM_fix_enable(bool on);
#endif
#endif
#endif /* APPS_APP_MENU_H */
File diff suppressed because it is too large. Load diff
+135
View File
@@ -0,0 +1,135 @@
/* Copyright 2026 Armel F4HWN
* https://github.com/armel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Overlay-app loader (POC).
*
* A leaf, modal app is a self-contained blob held in the external SPI flash. To
* run it the loader validates the header + code CRC, copies the code into the
* 4 KiB overlay (the PY25Q16 sector cache, shared VMA with the multiboot RAM
* stub), and calls its entry point. The blob runs from RAM and reaches every
* resident service through app_api_t. The loader never touches the internal
* application flash, so a corrupt or incompatible blob is only refused (header/CRC
* mismatch) or crashes into a watchdog reset.
*
* This is NOT a security sandbox. The CRC is an integrity check, not authentication,
* and a launched app is TRUSTED NATIVE CODE: it runs privileged with full access to
* memory and peripherals (no MPU, no crypto auth), so a malicious or buggy app CAN
* brick the radio - e.g. by driving the internal-flash controller itself. Only
* install apps you trust.
*
* External-flash "Apps" region (carved from the free space in the PY25Q16 map,
* after the multiboot markers, before the RX/TX log):
*
* 0x102000 slot 0 ] 16 slots x 8 KiB = 128 KiB
* 0x104000 slot 1 ] each slot: 4 KiB header sector + 4 KiB code
* ... ]
* 0x120000 slot 15 ]
*
* Host tooling (APP_SlotErase/Write/Info) touches the external flash only and is
* never brick-critical; a bad slot is simply refused at launch by the CRC check.
*/
#ifndef APPS_APP_OVERLAY_H
#define APPS_APP_OVERLAY_H
#include <stdint.h>
#include <stdbool.h>
#include "app_api.h"
/* ---- external-flash Apps region ---- */
#define APP_REGION_BASE 0x00102000u /* first app slot */
#define APP_SLOT_STRIDE 0x00002000u /* 8 KiB per slot */
#define APP_CODE_OFFSET 0x00001000u /* code starts after the 4 KiB header sector */
#define APP_SECTOR_SIZE 0x00001000u /* external NOR erase granularity */
#define APP_SLOT_COUNT 16u
#define APP_SLOT_BASE(s) (APP_REGION_BASE + (uint32_t)(s) * APP_SLOT_STRIDE)
/* ---- overlay RAM budget: the PY25Q16 4 KiB sector cache reused as workspace.
* The link VMA itself is pinned in Core/py32f071xb.ld (ORIGIN+0x280) and passed
* via compile-app.sh; the loader checks each blob's link_vma against it. ---- */
#define APP_OVERLAY_MAX 0x00001000u /* 4 KiB */
/* ---- blob header (64 bytes, little-endian; see App/apps/pack_app.py) ---- */
#define APP_MAGIC 0x31504146u /* "FAP1" */
#define APP_HDR_VERSION 1u
#define APP_FLAG_COMMITTED 0x0001u
#define APP_FLAG_SCREEN_SAVER 0x0002u
#define APP_FLAG_SHORTCUT_SHIFT 8u
#define APP_FLAG_SHORTCUT_MASK 0x0F00u
#define APP_NAME_LEN 16
#define APP_VERSION_LEN 16
#define APP_SHORTCUT_FM 0x01u
#define APP_SHORTCUT_FOXHUNT 0x02u
#define APP_SHORTCUT_BEACON 0x04u
#define APP_SHORTCUT_BEAM 0x08u
/* Optional resident facilities an app may require. Requirements live in the
* previously reserved header bytes, so app_header_t remains 64 bytes. */
#define APP_CAP_FM 0x00000001u
#define APP_CAP_TRIVFO 0x00000002u
#define APP_CAP_BEAM 0x00000004u
typedef struct __attribute__((packed)) {
uint32_t magic; /* APP_MAGIC */
uint16_t hdr_version; /* APP_HDR_VERSION */
uint8_t abi_major; /* required ABI family */
uint8_t api_min; /* minimum append-only API level */
uint32_t code_size; /* bytes of code, <= APP_OVERLAY_MAX */
uint32_t code_crc32; /* CRC-32 (zlib) over code_size bytes */
uint16_t entry_off; /* entry offset within the code (0) */
uint16_t flags; /* APP_FLAG_COMMITTED, ... */
char name[APP_NAME_LEN]; /* human-readable, NUL-terminated */
char version[APP_VERSION_LEN];/* app version string */
uint32_t link_vma; /* RAM VMA the code was linked at */
uint32_t required_caps; /* APP_CAP_* required by this app */
uint8_t reserved[4]; /* pad to 64 bytes */
} app_header_t;
enum {
APP_OK = 0,
APP_ERR_SLOT, /* slot index out of range */
APP_ERR_MAGIC, /* no/invalid header */
APP_ERR_ABI, /* ABI family/API level mismatch */
APP_ERR_NOT_COMMITTED, /* image not marked complete */
APP_ERR_SIZE, /* code_size out of range */
APP_ERR_CRC, /* code CRC-32 mismatch */
APP_ERR_VMA, /* overlay buffer not at the link VMA */
APP_ERR_AUTH, /* host write refused: timestamp mismatch */
APP_ERR_CAP, /* required firmware capability missing */
};
/* Read + validate a slot header (no CRC of the code). */
uint8_t APP_ValidateSlot(uint8_t slot, app_header_t *out_header);
/* Validate, load into the overlay, verify the code CRC, and run the app.
* Returns when the app exits; the internal flash is never touched. */
uint8_t APP_LaunchOverlay(uint8_t slot);
/* Launch the first installed app advertising this shortcut bit. */
uint8_t APP_LaunchOverlayShortcut(uint8_t shortcut);
/* Cached availability of apps exposed as resident quick actions. */
uint8_t APP_OverlayShortcutMask(void);
/* Host-tool slot management (external flash only, never brick-critical). */
uint8_t APP_SlotInfo(uint8_t slot, app_header_t *out_header);
uint8_t APP_SlotErase(uint8_t slot);
uint8_t APP_SlotWrite(uint8_t slot, uint32_t offset, const uint8_t *data, uint32_t len);
#endif /* APPS_APP_OVERLAY_H */
+43
View File
@@ -0,0 +1,43 @@
/* Overlay-app link script (POC).
*
* The blob is linked to run at the PY25Q16 sector-cache overlay VMA (shared with
* the multiboot RAM stub — never both active at once). Code + rodata + data +
* bss must all fit in the 4 KiB overlay. objcopy -O binary emits text+rodata+
* data only; the loader zeroes the overlay before copying, so bss starts clean.
*
* VMA must match ADDR(.mb_workspace) in Core/py32f071xb.ld. If the firmware RAM
* layout shifts, update APP_VMA and the loader's compile-time assert catches a
* mismatch. */
/* APP_VMA is the target firmware's __mb_workspace_start (read from its .map). It
* varies with the RAM layout, so pass it from the build: -Wl,--defsym,APP_VMA=0x...
* The default is only a fallback for a standalone size check. */
APP_VMA = DEFINED(APP_VMA) ? APP_VMA : 0x20000280;
APP_LENGTH = 0x1000; /* 4 KiB sector-cache overlay */
ENTRY(app_main)
MEMORY {
APP (rwx) : ORIGIN = APP_VMA, LENGTH = APP_LENGTH
}
SECTIONS {
.app APP_VMA : {
KEEP(*(.text.entry)) /* app_main pinned to offset 0 */
*(.text .text.*)
*(.rodata .rodata.*)
. = ALIGN(4);
*(.data .data.*)
. = ALIGN(4);
__app_bss_start = .;
*(.bss .bss.* COMMON)
. = ALIGN(4);
__app_bss_end = .;
} > APP
__app_end = .;
ASSERT(__app_end <= APP_VMA + APP_LENGTH,
"Beacon overlay app overflows the 4 KiB overlay")
/DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.eh_frame*) *(.comment) *(.note.*) }
}
+333
View File
@@ -0,0 +1,333 @@
/* Copyright 2026 Armel F4HWN
* https://github.com/armel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Beacon (fox) — overlay app. Ported from App/app/foxhunt.c, beacon sub-mode only.
* Turns the radio into a hidden ARDF transmitter: keys up on the TX VFO and repeats
* a CW fox identifier (MOE..MO5 / MO / "<call> MOE") for the TX window, then stays
* silent for the idle gap. Two keying modes (key 4): TONE (default) keeps the carrier
* up and keys only the tone (MCW / F2A); CARR keys the PA together with the tone, so
* between elements the carrier itself is gone (carrier interruption, ARDF field pattern).
*
* Keys: 1 TX window · 2 idle gap · 3 fox id · 4 keying mode TONE/CARR (F reverses)
* MENU restart idle · long F keypad lock · EXIT quit.
*/
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include "../app_api.h"
#define LCD_WIDTH 128
#define TICK_MS 50
#define LOCK_HOLD_MS 500
#define MORSE_UNIT 100
#define TONE_HZ 1000
#define CALL_MAX 12
#define IDLE_DEF 30
#define IDLE_MIN 5
#define IDLE_MAX 240
#define IDLE_STEP 5
#define TX_DEF 30
#define TX_MIN 5
#define TX_MAX 60
#define TX_STEP 5
#define FOX_MO 5
#define FOX_CALL 6
#define FOX_COUNT 7
#define MSG_CALL 1
#define MSG_ID 3
#define MSG_ONE 2
#define CFG_MAGIC 0xF4
/* Sentinel-prefixed Morse (leading 1 sentinel, then elements MSB-first: 0 dit, 1 dah). */
static const uint8_t M_LET[26] = {0x05,0x18,0x1A,0x0C,0x02,0x12,0x0E,0x10,0x04,0x17,0x0D,0x14,0x07,0x06,0x0F,0x16,0x1D,0x0A,0x08,0x03,0x09,0x11,0x0B,0x19,0x1B,0x1C};
static const uint8_t M_DIG[10] = {0x3F,0x2F,0x27,0x23,0x21,0x20,0x30,0x38,0x3C,0x3E};
static const char FOX_TAIL[5] = {'E','I','S','H','5'};
static const uint8_t BMP_TX[16] = {0x1c,0x22,0x41,0x1c,0x22,0x00,0x08,0x1c,0x1c,0x08,0x00,0x22,0x1c,0x41,0x22,0x1c};
static const uint8_t FONT_LOCK[9] = {0x7c,0x46,0x45,0x45,0x45,0x45,0x45,0x46,0x7c};
static const uint8_t FONT_F[9] = {0x3e,0x7f,0x41,0x75,0x75,0x75,0x7d,0x7f,0x3e};
static const app_api_t *A;
static bool foxLocked, fArm, fLongDone, running, phaseTx, carrier;
static uint16_t fHoldMs;
static uint8_t beaconIdle, idleLeft, idleTick, beaconTx, secShown, charsSent, foxFox;
static uint16_t txMsLeft;
static char foxCall[CALL_MAX+1];
static char msg[17];
static uint8_t prevKey;
static char str[16];
/* ---- tiny formatting ---- */
static uint8_t slen(const char *s){ uint8_t n=0; while(s[n])n++; return n; }
static char *put(char *o,const char *s){ while(*s)*o++=*s++; return o; }
static char *putu(char *o,uint32_t v){ char t[6]; int8_t n=0; do{t[n++]=(char)('0'+v%10);v/=10;}while(v&&n<6); while(n--)*o++=t[n]; return o; }
static void put2(char **o,uint8_t v){ if(v<10)*(*o)++='0'; *o=putu(*o,v); }
static void cpy(uint8_t *d,const uint8_t *s,uint8_t n){ while(n--)*d++=*s++; }
static const char *findsp(const char *s){ while(*s){ if(*s==' ')return s; s++; } return NULL; }
static uint8_t mmin(uint8_t a,uint8_t b){ return a<b?a:b; }
/* ---- settings cycles ---- */
static uint8_t rangeStep(uint8_t v,uint8_t lo,uint8_t hi,uint8_t step,int8_t dir){
if(dir>0) return (v>=hi)?lo:(uint8_t)(v+step);
return (v<=lo)?hi:(uint8_t)(v-step);
}
static bool settingKey(uint8_t key,int8_t dir){
switch(key){
case APP_KEY_1: beaconTx =rangeStep(beaconTx, TX_MIN, TX_MAX, TX_STEP, dir); return true;
case APP_KEY_2: beaconIdle=rangeStep(beaconIdle,IDLE_MIN,IDLE_MAX,IDLE_STEP,dir); return true;
case APP_KEY_3:
if(dir>0){ if(++foxFox>=FOX_COUNT) foxFox=0; }
else foxFox=foxFox?(uint8_t)(foxFox-1u):(uint8_t)(FOX_COUNT-1u);
return true;
case APP_KEY_4: carrier=!carrier; return true; /* TONE <-> CARR (dir moot) */
default: return false;
}
}
/* ---- message ---- */
static void buildMsg(void){
char *o=msg;
if(foxFox==FOX_CALL){
if(foxCall[0]){ o=put(o,foxCall); o=put(o," MOE"); }
else o=put(o,"MOE");
} else if(foxFox==FOX_MO){ o=put(o,"MO"); }
else { o=put(o,"MO"); *o++=FOX_TAIL[foxFox]; }
*o='\0';
}
static void foxLabel(char *out){
char *o=put(out,"FOX ");
if(foxFox==FOX_CALL) o=put(o,"CALL");
else if(foxFox==FOX_MO) o=put(o,"MO");
else { o=put(o,"MO"); *o++=FOX_TAIL[foxFox]; }
*o='\0';
}
static uint8_t morseByte(char c){
if(c>='a'&&c<='z') c-=32;
if(c>='A'&&c<='Z') return M_LET[c-'A'];
if(c>='0'&&c<='9') return M_DIG[c-'0'];
if(c=='/') return 0x32;
return 0;
}
/* ---- drawing ---- */
static void tag(const char *s,uint8_t x,uint8_t line){ A->print_inverse(s,x,line,false,true,(uint8_t)(x+slen(s)*4)); }
static void chrome(void){
A->display_clear();
A->status_clear();
A->print_inverse("BEACON",2,0,true,true,26);
A->draw_battery();
if(foxLocked) cpy(A->status_line+70,FONT_LOCK,sizeof(FONT_LOCK));
else if(fArm) cpy(A->status_line+70,FONT_F,sizeof(FONT_F));
{ char *o=putu(str,A->tx_freq()/100000u); *o++='.'; uint32_t fr=A->tx_freq()%100000u;
for(int8_t d=4;d>=0;d--){ uint32_t p=1; for(int8_t k=0;k<d;k++)p*=10; *o++=(char)('0'+(fr/p)%10);} *o='\0'; }
A->print_normal(str,(uint8_t)(126-slen(str)*7),0,6);
}
static void drawTxSeconds(void){
uint8_t sec=(uint8_t)((txMsLeft+999u)/1000u);
for(uint8_t x=0;x<LCD_WIDTH;x++) A->fb[0][x]=0;
A->print_normal("TX",1,0,0);
char *o=str; put2(&o,sec); *o++='s'; *o='\0';
A->print_normal(str,(uint8_t)(127-slen(str)*7),0,0);
secShown=sec;
}
static void drawMessage(uint8_t vis){
const char *sp=findsp(msg);
if(!sp){
uint8_t idLen=slen(msg), v=mmin(vis,idLen);
char id[17]; cpy((uint8_t*)id,(const uint8_t*)msg,v); id[v]='\0';
A->print_string(id,(uint8_t)((LCD_WIDTH-idLen*8u)/2u),0,MSG_ONE,8);
return;
}
uint8_t callLen=(uint8_t)(sp-msg), vc=mmin(vis,callLen);
char call[CALL_MAX+1]; cpy((uint8_t*)call,(const uint8_t*)msg,vc); call[vc]='\0';
A->print_string(call,(uint8_t)((LCD_WIDTH-callLen*8u)/2u),0,MSG_CALL,8);
if(vis>callLen+1){
const char *id=sp+1; uint8_t idLen=slen(id), vi=mmin((uint8_t)(vis-callLen-1),idLen);
char idb[8]; cpy((uint8_t*)idb,(const uint8_t*)id,vi); idb[vi]='\0';
A->print_string(idb,(uint8_t)((LCD_WIDTH-idLen*8u)/2u),0,MSG_ID,8);
}
}
static void updateProgress(uint8_t vis){
const char *sp=findsp(msg); uint8_t top;
charsSent=vis;
if(!sp) top=MSG_ONE;
else { uint8_t callLen=(uint8_t)(sp-msg); if(vis==callLen+1) return; top=(vis<=callLen)?MSG_CALL:MSG_ID; }
for(uint8_t x=0;x<LCD_WIDTH;x++){ A->fb[top][x]=0; A->fb[top+1][x]=0; }
drawMessage(charsSent);
A->blit_line(top); A->blit_line(top+1);
}
static void beaconDraw(bool txNow,uint8_t il){
chrome();
if(txNow){
cpy(A->status_line+48,BMP_TX,16);
drawTxSeconds();
drawMessage(charsSent);
} else {
A->print_string("IDLE",0,127,1,10);
char *o=str; put2(&o,il); *o++='s'; *o='\0';
A->print_string(str,0,127,3,10);
}
char *o=put(str,"TX "); o=putu(o,beaconTx); o=put(o,"s"); *o='\0'; tag(str,4,5);
o=put(str,"IDLE "); o=putu(o,beaconIdle); o=put(o,"s"); *o='\0'; tag(str,4,6);
foxLabel(str); tag(str,66,5);
{ const char *m=carrier?"CARR":"TONE"; tag(m,(uint8_t)(125-slen(m)*4),5); }
}
static void blit(void){ A->blit_status(); A->blit_full(); }
/* ---- keypad-lock long-press ---- */
/* true exactly on the lock/unlock toggle, so a caller that does not redraw every tick
* (the TX loop) can refresh the padlock at once. */
static bool lockTrack(uint8_t key,uint16_t ms){
if(key!=APP_KEY_F){ fHoldMs=0; fLongDone=false; return false; }
if(fLongDone) return false;
fHoldMs+=ms;
if(fHoldMs>=LOCK_HOLD_MS){ fLongDone=true; foxLocked=!foxLocked; fArm=false; A->backlight_on(); return true; }
return false;
}
/* ---- interactive TX delay: drains the window, keys, live seconds. true = abort ---- */
static bool txDelay(uint16_t ms){
while(ms){
uint16_t slice=(ms>10)?10:ms;
A->delay_ms(slice); ms-=slice;
txMsLeft=(txMsLeft>slice)?(uint16_t)(txMsLeft-slice):0;
A->backlight_update();
if((uint8_t)((txMsLeft+999u)/1000u)!=secShown){ drawTxSeconds(); A->blit_line(0); }
uint8_t key=A->get_key();
if(lockTrack(key,slice)){ beaconDraw(true,0); blit(); } /* show the padlock at once, mid-TX */
if(key==APP_KEY_INVALID||key==prevKey){ prevKey=key; continue; }
prevKey=key;
A->backlight_on();
if(foxLocked) continue;
if(key==APP_KEY_EXIT){ running=false; return true; }
if(key==APP_KEY_MENU) return true; /* cut short, restart idle */
if(key==APP_KEY_F){ fArm=!fArm; beaconDraw(true,0); blit(); continue; }
if(settingKey(key,fArm?-1:1)){ fArm=false; beaconDraw(true,0); blit(); }
}
return false;
}
/* Keying: both modes key the tone; CARR gates the PA in lockstep so the carrier is truly
* gone between elements (tone muted too, else it bleeds through residual PA leakage). */
static void keyOn(void){ A->tx_mute(false); A->tx_carrier(true); } /* carrier up both modes; re-arms after a mid-window switch */
static void keyOff(void){ A->tx_mute(true); if(carrier) A->tx_carrier(false); }
static bool morseChar(char c,uint8_t vis){
uint8_t code=morseByte(c);
if(code==0){ if(txDelay(MORSE_UNIT*4)) return true; updateProgress(vis); return false; }
uint8_t bit=0x80; while(!(code&bit))bit>>=1;
for(bit>>=1;bit;bit>>=1){
uint16_t on=(code&bit)?(MORSE_UNIT*3):MORSE_UNIT;
keyOn();
if(txDelay(on)){ keyOff(); return true; }
keyOff();
if(txDelay(MORSE_UNIT)) return true;
}
updateProgress(vis);
return txDelay(MORSE_UNIT*2);
}
static void transmit(void){
A->tx_set_params();
A->tx_tone(TONE_HZ);
A->tx_mute(true); /* open muted (both modes) */
if(carrier) A->tx_carrier(false); /* CARR: carrier off until first element */
txMsLeft=(uint16_t)beaconTx*1000u;
bool stop=false;
while(!stop && txMsLeft>0){
buildMsg();
charsSent=0;
beaconDraw(true,0); blit();
for(uint8_t i=0;!stop && msg[i];i++) stop=morseChar(msg[i],(uint8_t)(i+1));
if(!stop && txMsLeft>0) stop=txDelay(MORSE_UNIT*7);
}
A->tx_mute(true);
A->tx_end(); /* PA off + restore RX */
}
/* ---- idle-phase keys ---- */
static void idleKeys(void){
uint8_t key=A->get_key();
lockTrack(key,TICK_MS);
if(key==APP_KEY_INVALID||key==prevKey){ prevKey=key; return; }
prevKey=key;
A->backlight_on();
if(foxLocked) return;
if(key==APP_KEY_F){ fArm=!fArm; return; }
switch(key){
case APP_KEY_EXIT: running=false; break;
case APP_KEY_MENU: idleLeft=beaconIdle; idleTick=0; break;
default: settingKey(key,fArm?-1:1); break;
}
fArm=false;
}
static void tickDelay(void){ for(uint8_t i=0;i<TICK_MS/10;i++){ A->delay_ms(10); A->backlight_update(); } }
/* ---- config (deferred) ---- */
static void loadConfig(void){
uint8_t c[5]; A->cfg_load(c,5);
if(c[0]==CFG_MAGIC){
if(c[1]>=IDLE_MIN&&c[1]<=IDLE_MAX&&(c[1]%IDLE_STEP)==0) beaconIdle=c[1];
if(c[2]<FOX_COUNT) foxFox=c[2];
if(c[3]>=TX_MIN&&c[3]<=TX_MAX&&(c[3]%TX_STEP)==0) beaconTx=c[3];
if(c[4]<=1) carrier=c[4]; /* erased (0xFF) legacy config -> keep default */
}
}
static void saveConfig(void){ uint8_t c[5]={CFG_MAGIC,beaconIdle,foxFox,beaconTx,(uint8_t)carrier}; A->cfg_save(c,5); }
static void txDenied(void){
chrome();
A->print_string("TX OFF",0,127,1,8);
blit();
for(uint8_t i=0;i<20;i++) tickDelay();
}
__attribute__((section(".text.entry"),used))
void app_main(const app_api_t *api){
A=api;
foxLocked=fArm=fLongDone=carrier=false; fHoldMs=0;
beaconIdle=IDLE_DEF; beaconTx=TX_DEF; foxFox=FOX_CALL;
prevKey=APP_KEY_INVALID;
A->boot_callsign(foxCall,sizeof(foxCall));
loadConfig();
A->backlight_on();
phaseTx=true; idleLeft=beaconIdle; idleTick=0;
running=true;
while(running){
if(phaseTx){
if(A->tx_state()!=0){ txDenied(); phaseTx=false; idleLeft=beaconIdle; idleTick=0; continue; }
transmit();
phaseTx=false; idleLeft=beaconIdle; idleTick=0;
continue;
}
idleKeys();
if(!running) break;
beaconDraw(false,idleLeft); blit();
if(++idleTick>=(1000/TICK_MS)){ idleTick=0; if(idleLeft>0)idleLeft--; if(idleLeft==0)phaseTx=true; }
A->battery_sample();
tickDelay();
}
saveConfig();
A->tx_end(); /* safety: PA off + RX restored */
A->audio_path(false);
}
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# Build one overlay-app blob (.app). Invoked by ../../../compile-app.sh inside
# the uvk1-uvk5v3 Docker image, run from this app's directory. Everything is
# derived from the directory name; only APP_NAME - the human label baked into
# the 64-byte blob header - is per-app. The .app file is named after it (spaces
# stripped), so "Broadcast FM" -> BroadcastFM.app.
set -euo pipefail
APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ...
APP_NAME="Beacon" # <-- the only per-app line
APP_VER="1.0"
APP_API_MIN=1
APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld)
OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM)
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 # drop stale artifacts so discovery is unambiguous
step() { printf '\r 🔨 %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; }
trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
--name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" \
--shortcut beacon >/dev/null
trap - ERR
BYTES=$(wc -c < "${APP}.bin")
if [ "$BYTES" -gt 4096 ]; then
printf '\r 🚨 %-13s OVERFLOWS 4 KiB (%d B) \n' "$APP_NAME" "$BYTES"; exit 1
fi
printf '\r ✅ %-13s %4d B (%d%% of 4 KiB) -> %s.app \n' \
"$APP_NAME" "$BYTES" "$(( BYTES * 100 / 4096 ))" "$OUT"
+30
View File
@@ -0,0 +1,30 @@
/* Overlay-app link script. */
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);
__app_bss_start = .;
*(.bss .bss.* COMMON)
. = ALIGN(4);
__app_bss_end = .;
} > APP
__app_end = .;
ASSERT(__app_end <= APP_VMA + APP_LENGTH,
"BEAM overlay app overflows the 4 KiB overlay")
/DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.eh_frame*) *(.comment) *(.note.*) }
}
+244
View File
@@ -0,0 +1,244 @@
/* Copyright 2026 Armel F4HWN
* Licensed under the Apache License, Version 2.0.
*
* BEAM overlay app. Packet version 2 remains wire-compatible with the
* resident implementation: one selected VFO is sent and a received VFO is
* stored in the first free memory channel.
*/
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include "../app_api.h"
#define PACKET_MAGIC 0xBEA5u
#define PACKET_VERSION 2u
#define PACKET_START 0xABCDu
#define PACKET_END 0xDCBAu
#define PACKET_WORDS 36u
enum {
STATUS_READY = 0,
STATUS_TX_WAIT,
STATUS_TX_DONE,
STATUS_RX_WAIT,
STATUS_RX_SAVED,
STATUS_RX_FULL,
STATUS_ERROR,
};
typedef struct {
uint16_t magic;
uint8_t version;
uint8_t pad;
app_beam_channel_t channel;
} beam_payload_t;
_Static_assert(sizeof(beam_payload_t) == 48u,
"BEAM v2 wire payload layout changed");
static const uint16_t OBFUSCATION[8] = {
0x6C16, 0xE614, 0x912E, 0x400D, 0x3521, 0x40D5, 0x0313, 0x80E9
};
static const app_api_t *A;
static uint32_t packet_store[18]; /* 72 bytes, 4-byte aligned for the payload */
static uint16_t copiedChannel;
static uint8_t mode, status;
static bool receiving, running, dirty;
static uint16_t *packet(void) { return (uint16_t *)packet_store; }
static beam_payload_t *payload(void) { return (beam_payload_t *)&packet()[2]; }
static void clear_bytes(void *ptr, uint8_t count)
{
uint8_t *p = (uint8_t *)ptr;
while (count--) *p++ = 0;
}
static uint16_t crc16(const void *buffer, uint16_t size)
{
const uint8_t *data = (const uint8_t *)buffer;
uint16_t crc = 0;
while (size--) {
crc ^= (uint16_t)*data++ << 8;
for (uint8_t bit = 0; bit < 8u; bit++)
crc = (crc & 0x8000u) ? (uint16_t)((crc << 1) ^ 0x1021u)
: (uint16_t)(crc << 1);
}
return crc;
}
static void obfuscate(void)
{
uint16_t *p = packet();
for (uint8_t i = 0; i < 32u; i++)
p[i + 1u] ^= OBFUSCATION[i & 7u];
}
static void draw(void)
{
const char *state;
switch (status) {
case STATUS_TX_WAIT: state = "SENDING"; break;
case STATUS_TX_DONE: state = "SENT"; break;
case STATUS_RX_WAIT: state = "WAITING"; break;
case STATUS_RX_SAVED: state = "RECEIVED"; break;
case STATUS_RX_FULL: state = "MEM FULL"; break;
case STATUS_ERROR: state = "ERROR"; break;
default: state = mode ? "BEAM RX" : "BEAM TX"; break;
}
A->beam_draw(state);
A->blit_status();
A->blit_full();
}
static void stop_rx(void)
{
if (receiving) {
A->beam_rx(false);
receiving = false;
}
}
static void send_packet(void)
{
uint16_t *p = packet();
clear_bytes(packet_store, sizeof(packet_store));
p[0] = PACKET_START;
payload()->magic = PACKET_MAGIC;
payload()->version = PACKET_VERSION;
A->beam_get(&payload()->channel);
p[34] = crc16(&p[1], 2u + 64u);
p[35] = PACKET_END;
obfuscate();
status = STATUS_TX_WAIT;
draw();
A->beam_send(p);
status = STATUS_TX_DONE;
for (uint8_t i = 0; i < 3u; i++) {
A->play_tone(880, 60);
if (i < 2u) A->delay_ms(20);
}
dirty = true;
}
static void start(void)
{
stop_rx();
/* A channel save is committed only after app_main() returns. Do not let a
second RX overwrite that pending save; exit and relaunch to receive more. */
if (mode && copiedChannel != 0xFFFFu)
return;
A->beam_prepare();
if (!mode) {
send_packet();
return;
}
A->beam_rx(true);
receiving = true;
status = STATUS_RX_WAIT;
dirty = true;
}
static bool valid_packet(void)
{
uint16_t *p = packet();
if (p[0] != PACKET_START || p[35] != PACKET_END)
return false;
obfuscate();
if (p[34] != crc16(&p[1], 2u + 64u))
return false;
return payload()->magic == PACKET_MAGIC && payload()->version == PACKET_VERSION;
}
static void poll_rx(void)
{
if (!receiving)
return;
const uint8_t result = A->beam_rx_poll(packet());
if (result == APP_BEAM_RX_WAIT)
return;
if (result == APP_BEAM_RX_ERROR || !valid_packet()) {
status = STATUS_ERROR;
A->backlight_on();
dirty = true;
return;
}
copiedChannel = A->beam_save(&payload()->channel);
A->beam_rx(false);
receiving = false;
status = copiedChannel == 0xFFFFu ? STATUS_RX_FULL : STATUS_RX_SAVED;
A->backlight_on();
dirty = true;
}
static void key_press(uint8_t key)
{
switch (key) {
case APP_KEY_UP:
case APP_KEY_DOWN:
stop_rx();
mode ^= 1u;
status = STATUS_READY;
dirty = true;
break;
case APP_KEY_MENU:
start();
break;
case APP_KEY_EXIT:
running = false;
break;
case APP_KEY_PTT:
break;
default:
A->play_tone(500, 60);
break;
}
}
__attribute__((section(".text.entry"), used))
void app_main(const app_api_t *api)
{
A = api;
mode = 0;
status = STATUS_READY;
copiedChannel = 0xFFFFu;
receiving = false;
running = true;
dirty = true;
A->backlight_on();
uint8_t previous = APP_KEY_INVALID;
uint8_t batteryTicks = 0;
while (running) {
const uint8_t key = A->get_key();
if (key == APP_KEY_SAVER) {
previous = APP_KEY_INVALID;
} else if (key == APP_KEY_WAKE) {
previous = APP_KEY_INVALID;
dirty = true;
} else {
if (key != previous && key != APP_KEY_INVALID) {
A->backlight_on();
key_press(key);
}
previous = key;
}
poll_rx();
if (dirty) { draw(); dirty = false; }
A->delay_ms(10);
A->backlight_update();
if (++batteryTicks >= 50u) {
batteryTicks = 0;
A->battery_sample();
dirty = true;
}
}
stop_rx();
A->beam_leave();
}
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
set -euo pipefail
APP="$(basename "$PWD")"
APP_NAME="Beam"
APP_VER="1.0"
APP_API_MIN=1
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
step() { printf '\r 🔨 %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; }
trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
--name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" \
--shortcut beam --require beam >/dev/null
trap - ERR
BYTES=$(wc -c < "${APP}.bin")
if [ "$BYTES" -gt 4096 ]; then
printf '\r 🚨 %-13s OVERFLOWS 4 KiB (%d B) \n' "$APP_NAME" "$BYTES"; exit 1
fi
printf '\r ✅ %-13s %4d B (%d%% of 4 KiB) -> %s.app \n' \
"$APP_NAME" "$BYTES" "$(( BYTES * 100 / 4096 ))" "$OUT"
+43
View File
@@ -0,0 +1,43 @@
/* Overlay-app link script (POC).
*
* The blob is linked to run at the PY25Q16 sector-cache overlay VMA (shared with
* the multiboot RAM stub — never both active at once). Code + rodata + data +
* bss must all fit in the 4 KiB overlay. objcopy -O binary emits text+rodata+
* data only; the loader zeroes the overlay before copying, so bss starts clean.
*
* VMA must match ADDR(.mb_workspace) in Core/py32f071xb.ld. If the firmware RAM
* layout shifts, update APP_VMA and the loader's compile-time assert catches a
* mismatch. */
/* APP_VMA is the target firmware's __mb_workspace_start (read from its .map). It
* varies with the RAM layout, so pass it from the build: -Wl,--defsym,APP_VMA=0x...
* The default is only a fallback for a standalone size check. */
APP_VMA = DEFINED(APP_VMA) ? APP_VMA : 0x20000280;
APP_LENGTH = 0x1000; /* 4 KiB sector-cache overlay */
ENTRY(app_main)
MEMORY {
APP (rwx) : ORIGIN = APP_VMA, LENGTH = APP_LENGTH
}
SECTIONS {
.app APP_VMA : {
KEEP(*(.text.entry)) /* app_main pinned to offset 0 */
*(.text .text.*)
*(.rodata .rodata.*)
. = ALIGN(4);
*(.data .data.*)
. = ALIGN(4);
__app_bss_start = .;
*(.bss .bss.* COMMON)
. = ALIGN(4);
__app_bss_end = .;
} > APP
__app_end = .;
ASSERT(__app_end <= APP_VMA + APP_LENGTH,
"Breakout overlay app overflows the 4 KiB overlay")
/DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.eh_frame*) *(.comment) *(.note.*) }
}
+269
View File
@@ -0,0 +1,269 @@
/* Copyright 2025 Armel F4HWN
* https://github.com/armel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Breakout — overlay-app port (POC).
*
* Self-contained: no firmware headers, no libc. Every resident service is
* reached through the app_api_t table; the entry point app_main() is forced to
* blob offset 0 via section ".text.entry". The loader zeroes the 4 KiB overlay,
* copies this blob in, and calls app_main(&api).
*/
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include "../app_api.h"
#define LCD_WIDTH 128
#define BRICK_NUMBER 18
#define BALL_NUMBER 5
#define BRICK_WIDTH 14
#define BRICK_HEIGHT 5
#define BALL_WIDTH 3
#define BALL_HEIGHT 3
#define RACKET_WIDTH 24
#define RACKET_HEIGHT 2
#define RACKET_Y 50
typedef struct { uint8_t x; uint8_t y; bool destroy; } Brick;
typedef struct { int8_t x; uint8_t p; } Racket;
typedef struct { int16_t x; int8_t y; int8_t dx; int8_t dy; } Ball;
/* ---- tiny freestanding helpers (replace libc / libgcc-free where cheap) ---- */
void *memset(void *d, int c, size_t n) {
uint8_t *p = d; while (n--) *p++ = (uint8_t)c; return d;
}
void *memcpy(void *d, const void *s, size_t n) {
uint8_t *p = d; const uint8_t *q = s; while (n--) *p++ = *q++; return d;
}
static int iabs(int v) { return v < 0 ? -v : v; }
static int imin(int a, int b) { return a < b ? a : b; }
/* zero-padded unsigned -> string, width w (<= 5). Replaces sprintf. */
static void u2str(char *out, const char *label, uint16_t v, uint8_t w) {
char *o = out;
while (*label) *o++ = *label++;
char tmp[6]; int8_t n = 0;
do { tmp[n++] = (char)('0' + v % 10); v /= 10; } while (v && n < 6);
while (n < w) tmp[n++] = '0';
while (n--) *o++ = tmp[n];
*o = '\0';
}
/* ---- app-wide handle to the resident ABI, set once at entry ---- */
static const app_api_t *A;
/* ---- game state (lives in the overlay; re-initialised at entry) ---- */
static uint32_t randSeed;
static uint8_t blockAnim;
static bool isInitialized;
static bool isPaused;
static uint8_t levelCountBreakout;
static uint16_t tone;
static uint16_t score;
static int16_t ballCount;
static char str[12];
static uint8_t kbdPrev, kbdCur;
static Brick brick[BRICK_NUMBER];
static Racket racket;
static Ball ball;
static const uint8_t BRICK_ANIM_PATTERNS[4] =
{0b00110001, 0b00101001, 0b00100101, 0b00100011};
static void srand_custom(uint32_t seed) { randSeed = seed ? seed : 1; }
static int rand_custom(void) { randSeed = randSeed * 1103515245u + 12345u; return (randSeed >> 16) & 0x7FFF; }
static int randInt(int min, int max) { return min + (rand_custom() % (max - min + 1)); }
static void reset(void) { ballCount = BALL_NUMBER; levelCountBreakout = 1; score = 0; }
static void playBeep(uint16_t t) { A->play_tone(t, 100); }
static void drawScore(void) {
A->status_clear();
u2str(str, "Level ", levelCountBreakout, 2); A->print_tiny(str, 0, 1, true, true);
u2str(str, "Ball ", (ballCount < 0) ? 0 : ballCount, 2); A->print_tiny(str, 45, 1, true, true);
u2str(str, "Score ", score, 4); A->print_tiny(str, 88, 1, true, true);
}
static void renderBall(bool state) {
A->draw_rect(A->fb, ball.x, ball.y, ball.x + BALL_WIDTH - 1, ball.y + BALL_HEIGHT - 1, state);
A->draw_line(A->fb, ball.x - 1, ball.y + 1, ball.x + BALL_WIDTH, ball.y + 1, state);
}
static void initBall(void) { ball.x = 62; ball.y = 30; ball.dx = 0; ball.dy = 1; renderBall(true); }
static void directionBall(int16_t x, uint8_t w, int8_t num) {
ball.dx = (int16_t)(x + w - ball.x) * (-num - num) / w + num;
ball.dy *= -1;
}
static void initWall(void) {
Brick *current = brick;
for (uint8_t y = 0; y < 24; y += 8)
for (uint8_t x = 6; x < 126; x += 20) {
current->x = x; current->y = y; current->destroy = false; current++;
}
}
static void initRacket(void);
static void drawBall(void) {
renderBall(false);
ball.x += ball.dx; ball.y += ball.dy;
if (ball.y <= 0) { ball.dx = randInt(-3, 3); ball.dy = 1; }
else if (ball.x <= 2) { ball.dx = iabs(ball.dx); }
else if (ball.x >= 124) { ball.dx = -iabs(ball.dx); }
if (ball.y == 47) {
if (ball.x + 1 >= racket.x && ball.x - 1 <= racket.x + RACKET_WIDTH) {
directionBall(racket.x, RACKET_WIDTH, 3); tone = 400;
}
} else if (ball.y > 49) {
ballCount--;
A->display_clear();
drawScore();
tone = 800;
if (ballCount < 0) {
reset(); initWall(); /* drawWall drawn by main loop */
isPaused = true;
A->print_bold("GAME OVER", 0, LCD_WIDTH - 1, 4);
}
initRacket(); initBall();
}
renderBall(true);
}
static void drawWall(void) {
for (uint8_t i = 0; i < BRICK_NUMBER; i++) {
if (brick[i].destroy) continue;
uint8_t *fb_ptr = A->fb[brick[i].y / 8] + brick[i].x;
fb_ptr[0] = 0b00011110;
fb_ptr[14] = 0b00011110;
if ((ball.x + 1 >= brick[i].x && ball.x - 1 <= brick[i].x + BRICK_WIDTH) &&
(ball.y + 1 >= brick[i].y && ball.y - 1 <= brick[i].y + BRICK_HEIGHT)) {
brick[i].destroy = true; score++;
directionBall(brick[i].x, BRICK_WIDTH, 2);
A->led(true);
memset(fb_ptr + 1, 0b00111111, 13);
A->blit_line(brick[i].y / 8);
playBeep(600);
memset(fb_ptr + 0, 0b00000000, 15);
A->blit_line(brick[i].y / 8);
A->led(false);
if (score % BRICK_NUMBER == 0) {
levelCountBreakout++; ballCount++; initWall(); return;
}
} else {
for (uint8_t k = 0; k < 13; k++)
fb_ptr[k + 1] = BRICK_ANIM_PATTERNS[(blockAnim + k) % 4];
}
}
}
static void renderRacket(int x, bool state) {
A->draw_rect(A->fb, x + 1, RACKET_Y, x + RACKET_WIDTH - 2, RACKET_Y + RACKET_HEIGHT, state);
A->draw_line(A->fb, x, RACKET_Y + 1, x + RACKET_WIDTH - 1, RACKET_Y + 1, state);
}
static void initRacket(void) { racket.x = 64 - (RACKET_WIDTH / 2); racket.p = racket.x; renderRacket(racket.x, true); }
static void drawRacket(void) {
if (racket.p != racket.x) {
renderRacket(racket.p, false);
racket.p = racket.x;
renderRacket(racket.x, true);
}
}
static void OnKeyDown(uint8_t key) {
bool wasPaused = isPaused;
switch (key) {
case APP_KEY_4:
case APP_KEY_UP: if (!isPaused && racket.x > 0) racket.x -= 2; isPaused = false; break;
case APP_KEY_0:
case APP_KEY_DOWN: if (!isPaused && racket.x < 102) racket.x += 2; isPaused = false; break;
case APP_KEY_MENU:
isPaused = !isPaused;
if (isPaused) A->print_bold("PAUSE", 0, LCD_WIDTH - 1, 4);
break;
case APP_KEY_EXIT: isPaused = false; isInitialized = false; break;
}
if (wasPaused && !isPaused)
for (uint8_t i = 0; i < 8; i++)
A->draw_line(A->fb, 32, 32 + i, 96, 32 + i, false);
}
static void handleInput(void) {
kbdPrev = kbdCur;
kbdCur = A->get_key();
if (kbdCur == APP_KEY_INVALID) return;
if (kbdCur == APP_KEY_UP || kbdCur == APP_KEY_DOWN ||
kbdCur == APP_KEY_4 || kbdCur == APP_KEY_0 || kbdCur != kbdPrev)
OnKeyDown(kbdCur);
}
/* ---- entry point, pinned to blob offset 0 ---- */
__attribute__((section(".text.entry"), used))
void app_main(const app_api_t *api) {
A = api;
#ifdef APP_POC_HELLO
/* Minimal bisection test: prove jump + ABI + return work, no game body. */
A->display_clear();
A->print_bold("OVL HELLO", 0, LCD_WIDTH - 1, 3);
A->blit_full();
A->delay_ms(2000);
return;
#endif
/* reset all state (the overlay is not persistent across launches) */
blockAnim = 0; isPaused = false; tone = 0; score = 0;
kbdPrev = APP_KEY_INVALID; kbdCur = APP_KEY_INVALID;
uint8_t swap = 0;
/* The low BK4819 counter bits vary continuously; mix them with the tuned
* frequency instead of carrying a launch-only seed field in every ABI
* table. */
srand_custom(((uint32_t)A->bk_read(0x67u) << 16) ^ A->rx_freq());
A->led(false);
A->backlight_on();
A->display_clear();
reset(); initWall(); initRacket(); initBall();
A->status_clear();
isInitialized = true;
while (isInitialized) {
handleInput();
if (!isPaused) {
if (swap == 0) blockAnim = (blockAnim + 1) % 4;
swap = (swap + 1) % 4;
drawScore(); drawWall(); drawRacket(); drawBall();
if (tone != 0) { playBeep(tone); tone = 0; }
else A->delay_ms(40 - imin(levelCountBreakout - 1, 20));
}
A->blit_status();
A->blit_full();
}
}
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# Build one overlay-app blob (.app). Invoked by ../../../compile-app.sh inside
# the uvk1-uvk5v3 Docker image, run from this app's directory. Everything is
# derived from the directory name; only APP_NAME - the human label baked into
# the 64-byte blob header - is per-app. The .app file is named after it (spaces
# stripped), so "Broadcast FM" -> BroadcastFM.app.
set -euo pipefail
APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ...
APP_NAME="Breakout" # <-- the only per-app line
APP_VER="1.0"
APP_API_MIN=1
APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld)
OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM)
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 # drop stale artifacts so discovery is unambiguous
step() { printf '\r 🔨 %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; }
trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
--name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" >/dev/null
trap - ERR
BYTES=$(wc -c < "${APP}.bin")
if [ "$BYTES" -gt 4096 ]; then
printf '\r 🚨 %-13s OVERFLOWS 4 KiB (%d B) \n' "$APP_NAME" "$BYTES"; exit 1
fi
printf '\r ✅ %-13s %4d B (%d%% of 4 KiB) -> %s.app \n' \
"$APP_NAME" "$BYTES" "$(( BYTES * 100 / 4096 ))" "$OUT"
+43
View File
@@ -0,0 +1,43 @@
/* Overlay-app link script (POC).
*
* The blob is linked to run at the PY25Q16 sector-cache overlay VMA (shared with
* the multiboot RAM stub — never both active at once). Code + rodata + data +
* bss must all fit in the 4 KiB overlay. objcopy -O binary emits text+rodata+
* data only; the loader zeroes the overlay before copying, so bss starts clean.
*
* VMA must match ADDR(.mb_workspace) in Core/py32f071xb.ld. If the firmware RAM
* layout shifts, update APP_VMA and the loader's compile-time assert catches a
* mismatch. */
/* APP_VMA is the target firmware's __mb_workspace_start (read from its .map). It
* varies with the RAM layout, so pass it from the build: -Wl,--defsym,APP_VMA=0x...
* The default is only a fallback for a standalone size check. */
APP_VMA = DEFINED(APP_VMA) ? APP_VMA : 0x20000280;
APP_LENGTH = 0x1000; /* 4 KiB sector-cache overlay */
ENTRY(app_main)
MEMORY {
APP (rwx) : ORIGIN = APP_VMA, LENGTH = APP_LENGTH
}
SECTIONS {
.app APP_VMA : {
KEEP(*(.text.entry)) /* app_main pinned to offset 0 */
*(.text .text.*)
*(.rodata .rodata.*)
. = ALIGN(4);
*(.data .data.*)
. = ALIGN(4);
__app_bss_start = .;
*(.bss .bss.* COMMON)
. = ALIGN(4);
__app_bss_end = .;
} > APP
__app_end = .;
ASSERT(__app_end <= APP_VMA + APP_LENGTH,
"Cube3D overlay app overflows the 4 KiB overlay")
/DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.eh_frame*) *(.comment) *(.note.*) }
}
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# Build one overlay-app blob (.app). Invoked by ../../../compile-app.sh inside
# the uvk1-uvk5v3 Docker image, run from this app's directory. Everything is
# derived from the directory name; only APP_NAME - the human label baked into
# the 64-byte blob header - is per-app. The .app file is named after it (spaces
# stripped), so "Broadcast FM" -> BroadcastFM.app.
set -euo pipefail
APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ...
APP_NAME="Cube3D" # <-- the only per-app line
APP_VER="1.0"
APP_API_MIN=1
APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld)
OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM)
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 # drop stale artifacts so discovery is unambiguous
step() { printf '\r 🔨 %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; }
trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
--name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" --screensaver >/dev/null
trap - ERR
BYTES=$(wc -c < "${APP}.bin")
if [ "$BYTES" -gt 4096 ]; then
printf '\r 🚨 %-13s OVERFLOWS 4 KiB (%d B) \n' "$APP_NAME" "$BYTES"; exit 1
fi
printf '\r ✅ %-13s %4d B (%d%% of 4 KiB) -> %s.app \n' \
"$APP_NAME" "$BYTES" "$(( BYTES * 100 / 4096 ))" "$OUT"
+299
View File
@@ -0,0 +1,299 @@
/* Copyright 2026 Armel F4HWN
* https://github.com/armel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Cube3D — overlay app. A real-time rotating solid on the 1-bit 128x64 LCD.
* Vertices are spun by three axis rotations in Q14 fixed point (Cortex-M0+ has
* no FPU and no hardware divide), then perspective-projected with a single
* divide per vertex. Two looks, toggled with F:
* - SOLID: hidden-line removal by back-face culling (a face is drawn only when
* the signed area of its projected polygon shows it facing us).
* - WIRE : every edge, with the far hemisphere dotted for a depth cue.
* Edges use a self-clipped Bresenham writing the full 64 rows directly, since the
* resident pixel helper does not bound-check. The eight solids' faces (with a
* uniform outward winding) are generated offline by a convex-hull extractor, so
* nothing here has to be hand-wound. Pure compute, no radio.
*
* Keys: UP/DOWN speed · 1-8 shape · STAR next shape · F solid/wire ·
* MENU pause · EXIT quit.
*/
#include <stdint.h>
#include <stdbool.h>
#include "../app_api.h"
#define W 128
#define H 64
#define CX 64 /* projection centre x */
#define CY 32 /* projection centre y */
#define DIST 150 /* camera distance along +z (keeps zc > 0) */
#define FOCAL 80 /* focal length / field-of-view scale */
#define MAXV 12 /* largest vertex count across the solids */
static const app_api_t *A;
/* One Q14 sine quadrant. Symmetry recovers the full 256-step wave while saving
* 382 bytes for the renderer. */
static const int16_t SIN_Q[65] = {
0, 402, 804, 1205, 1606, 2006, 2404, 2801, 3196, 3590, 3981,
4370, 4756, 5139, 5520, 5897, 6270, 6639, 7005, 7366, 7723, 8076,
8423, 8765, 9102, 9434, 9760,10080,10394,10702,11003,11297,11585,
11866,12140,12406,12665,12916,13160,13395,13623,13842,14053,14256,
14449,14635,14811,14978,15137,15286,15426,15557,15679,15791,15893,
15986,16069,16143,16207,16261,16305,16340,16364,16379,16384
};
static int sin8(uint8_t angle)
{
const uint8_t quadrant = angle >> 6;
uint8_t i = angle & 63u;
if (quadrant & 1u)
i = (uint8_t)(64u - i);
const int value = SIN_Q[i];
return quadrant >= 2u ? -value : value;
}
/* A face is a polygon of up to 6 vertex indices, wound CCW as seen from outside
* (generated offline by the hull extractor, so the signed-area cull sign is the
* same for every solid). Unused slots are 0-padded and ignored (n gives length). */
typedef struct { uint8_t n; uint8_t v[6]; } face_t;
static const int8_t CUBE_V[8][3]={{-26,-26,-26},{26,-26,-26},{26,26,-26},{-26,26,-26},{-26,-26,26},{26,-26,26},{26,26,26},{-26,26,26}};
static const face_t CUBE_F[6]={{4,{2,1,0,3,0,0}},{4,{4,0,1,5,0,0}},{4,{7,3,0,4,0,0}},{4,{5,1,2,6,0,0}},{4,{6,2,3,7,0,0}},{4,{7,4,5,6,0,0}}};
static const int8_t OCTAHEDRON_V[6][3]={{38,0,0},{-38,0,0},{0,38,0},{0,-38,0},{0,0,38},{0,0,-38}};
static const face_t OCTAHEDRON_F[8]={{3,{4,0,2,0,0,0}},{3,{2,0,5,0,0,0}},{3,{3,0,4,0,0,0}},{3,{5,0,3,0,0,0}},{3,{2,1,4,0,0,0}},{3,{5,1,2,0,0,0}},{3,{4,1,3,0,0,0}},{3,{3,1,5,0,0,0}}};
static const int8_t TETRAHEDRON_V[4][3]={{28,28,28},{28,-28,-28},{-28,28,-28},{-28,-28,28}};
static const face_t TETRAHEDRON_F[4]={{3,{2,0,1,0,0,0}},{3,{1,0,3,0,0,0}},{3,{3,0,2,0,0,0}},{3,{2,1,3,0,0,0}}};
static const int8_t DIAMOND_V[8][3]={{30,0,0},{14,26,0},{-14,26,0},{-30,0,0},{-14,-26,0},{14,-26,0},{0,0,40},{0,0,-40}};
static const face_t DIAMOND_F[12]={{3,{6,0,1,0,0,0}},{3,{1,0,7,0,0,0}},{3,{5,0,6,0,0,0}},{3,{7,0,5,0,0,0}},{3,{6,1,2,0,0,0}},{3,{2,1,7,0,0,0}},{3,{6,2,3,0,0,0}},{3,{3,2,7,0,0,0}},{3,{6,3,4,0,0,0}},{3,{4,3,7,0,0,0}},{3,{6,4,5,0,0,0}},{3,{5,4,7,0,0,0}}};
static const int8_t ICOSAHEDRON_V[12][3]={{0,18,29},{0,18,-29},{0,-18,29},{0,-18,-29},{18,29,0},{18,-29,0},{-18,29,0},{-18,-29,0},{29,0,18},{29,0,-18},{-29,0,18},{-29,0,-18}};
static const face_t ICOSAHEDRON_F[20]={{3,{8,0,2,0,0,0}},{3,{2,0,10,0,0,0}},{3,{6,0,4,0,0,0}},{3,{4,0,8,0,0,0}},{3,{10,0,6,0,0,0}},{3,{3,1,9,0,0,0}},{3,{11,1,3,0,0,0}},{3,{4,1,6,0,0,0}},{3,{9,1,4,0,0,0}},{3,{6,1,11,0,0,0}},{3,{5,2,7,0,0,0}},{3,{8,2,5,0,0,0}},{3,{7,2,10,0,0,0}},{3,{7,3,5,0,0,0}},{3,{5,3,9,0,0,0}},{3,{11,3,7,0,0,0}},{3,{9,4,8,0,0,0}},{3,{8,5,9,0,0,0}},{3,{10,6,11,0,0,0}},{3,{11,7,10,0,0,0}}};
static const int8_t CUBOCTA_V[12][3]={{-24,-24,0},{-24,24,0},{24,-24,0},{24,24,0},{-24,0,-24},{-24,0,24},{24,0,-24},{24,0,24},{0,-24,-24},{0,-24,24},{0,24,-24},{0,24,24}};
static const face_t CUBOCTA_F[14]={{4,{4,0,5,1,0,0}},{4,{2,9,0,8,0,0}},{3,{8,0,4,0,0,0}},{3,{5,0,9,0,0,0}},{4,{10,1,11,3,0,0}},{3,{4,1,10,0,0,0}},{3,{11,1,5,0,0,0}},{4,{3,7,2,6,0,0}},{3,{6,2,8,0,0,0}},{3,{9,2,7,0,0,0}},{3,{10,3,6,0,0,0}},{3,{7,3,11,0,0,0}},{4,{8,4,10,6,0,0}},{4,{7,11,5,9,0,0}}};
static const int8_t HEXPRISM_V[12][3]={{26,0,24},{12,22,24},{-12,22,24},{-26,0,24},{-12,-22,24},{12,-22,24},{26,0,-24},{12,22,-24},{-12,22,-24},{-26,0,-24},{-12,-22,-24},{12,-22,-24}};
static const face_t HEXPRISM_F[8]={{6,{4,5,0,1,2,3}},{4,{7,1,0,6,0,0}},{4,{6,0,5,11,0,0}},{4,{8,2,1,7,0,0}},{4,{9,3,2,8,0,0}},{4,{10,4,3,9,0,0}},{4,{11,5,4,10,0,0}},{6,{9,8,7,6,11,10}}};
static const int8_t PENTAGEM_V[7][3]={{28,0,0},{8,26,0},{-22,16,0},{-22,-16,0},{8,-26,0},{0,0,42},{0,0,-42}};
static const face_t PENTAGEM_F[10]={{3,{5,0,1,0,0,0}},{3,{1,0,6,0,0,0}},{3,{4,0,5,0,0,0}},{3,{6,0,4,0,0,0}},{3,{5,1,2,0,0,0}},{3,{2,1,6,0,0,0}},{3,{5,2,3,0,0,0}},{3,{3,2,6,0,0,0}},{3,{5,3,4,0,0,0}},{3,{4,3,6,0,0,0}}};
typedef struct {
const int8_t (*v)[3];
const face_t *f;
uint8_t nv;
uint8_t nf;
const char *name;
uint8_t namelen;
} shape_t;
#define NSHAPE 8
static const shape_t SHAPES[NSHAPE] = {
{ CUBE_V, CUBE_F, 8, 6, "CUBE", 4 },
{ OCTAHEDRON_V, OCTAHEDRON_F, 6, 8, "OCTAHEDRON", 10 },
{ TETRAHEDRON_V, TETRAHEDRON_F, 4, 4, "TETRAHEDRON", 11 },
{ DIAMOND_V, DIAMOND_F, 8, 12, "DIAMOND", 7 },
{ ICOSAHEDRON_V, ICOSAHEDRON_F, 12, 20, "ICOSAHEDRON", 11 },
{ CUBOCTA_V, CUBOCTA_F, 12, 14, "CUBOCTA", 7 },
{ HEXPRISM_V, HEXPRISM_F, 12, 8, "HEXPRISM", 8 },
{ PENTAGEM_V, PENTAGEM_F, 7, 10, "PENTAGEM", 8 },
};
/* Per-frame projected screen coords + rotated depth of each vertex. */
static int16_t px[MAXV], py[MAXV], pz[MAXV];
/* Quarter-half-units per frame. The low end has fractional angular steps;
* level 16 reaches the old 16 half-units/frame once divided by four. */
static const uint8_t ROT_RATE[16] = {
1, 2, 3, 4, 6, 8, 10, 12,
16, 20, 24, 30, 36, 44, 52, 64
};
/* Set one pixel across the full 64 rows: 0..7 -> status line, 8..63 -> fb. */
static void set_pixel(int x, int y)
{
if ((unsigned)x >= W || (unsigned)y >= H)
return;
const uint8_t bit = (uint8_t)(1u << (y & 7));
if (y < 8)
A->status_line[x] |= bit;
else
A->fb[(y >> 3) - 1][x] |= bit;
}
/* Integer Bresenham; dotted skips every other step for the depth cue. */
static void draw_edge(int x0, int y0, int x1, int y1, bool dotted)
{
const int dx = (x1 > x0 ? x1 - x0 : x0 - x1);
const int dy = -(y1 > y0 ? y1 - y0 : y0 - y1);
const int sx = (x0 < x1 ? 1 : -1);
const int sy = (y0 < y1 ? 1 : -1);
int err = dx + dy;
unsigned step = 0;
for (;;) {
if (!dotted || (step & 1u) == 0u)
set_pixel(x0, y0);
if (x0 == x1 && y0 == y1)
break;
const int e2 = 2 * err;
if (e2 >= dy) { err += dy; x0 += sx; }
if (e2 <= dx) { err += dx; y0 += sy; }
step++;
}
}
static void clear_screen(void)
{
for (uint8_t x = 0; x < W; x++) {
A->status_line[x] = 0;
for (uint8_t p = 0; p < 7u; p++)
A->fb[p][x] = 0;
}
}
/* Signed area of a face's projected polygon (<0 == facing us, calibrated). */
static int face_area(const face_t *f)
{
int sa = 0;
for (uint8_t k = 0; k < f->n; k++) {
const uint8_t a = f->v[k];
const uint8_t b = f->v[(k + 1u == f->n) ? 0u : k + 1u];
sa += (int)px[a] * py[b] - (int)px[b] * py[a];
}
return sa;
}
__attribute__((section(".text.entry"), used))
void app_main(const app_api_t *api)
{
A = api;
A->backlight_on();
A->status_clear();
uint16_t ax = 0, ay = 0, az = 0; /* Q2 half-units: 2048 = full turn */
uint8_t shape = 0;
uint8_t speed = 4; /* 1..16, shared by all three rotation axes */
bool paused = false;
bool wire = true; /* false = solid (hidden-line) */
bool running = true;
uint8_t prevKey = APP_KEY_INVALID;
while (running) {
uint8_t key = A->get_key();
if (key == APP_KEY_SAVER) {
prevKey = APP_KEY_INVALID;
A->delay_ms(10);
A->backlight_update();
continue;
}
if (key == APP_KEY_WAKE)
key = APP_KEY_INVALID;
if (key != prevKey && key != APP_KEY_INVALID) {
A->backlight_on();
switch (key) {
case APP_KEY_EXIT:
running = false;
break;
case APP_KEY_UP:
case APP_KEY_DOWN: {
const int8_t dir = A->nav_dir(key);
if (dir > 0 && speed < 16u) speed++;
if (dir < 0 && speed > 1u) speed--;
break;
}
case APP_KEY_MENU:
paused = !paused;
break;
case APP_KEY_STAR:
shape = (uint8_t)((shape + 1u) % NSHAPE);
break;
case APP_KEY_F:
wire = !wire;
break;
case APP_KEY_1: case APP_KEY_2: case APP_KEY_3: case APP_KEY_4:
case APP_KEY_5: case APP_KEY_6: case APP_KEY_7: case APP_KEY_8:
if ((uint8_t)(key - APP_KEY_1) < NSHAPE)
shape = (uint8_t)(key - APP_KEY_1);
break;
default:
break;
}
}
prevKey = key;
if (!running)
break;
const shape_t *s = &SHAPES[shape];
const uint8_t ia = (uint8_t)(ax >> 3), ib = (uint8_t)(ay >> 3), ic = (uint8_t)(az >> 3);
const int cx = sin8((uint8_t)(ia + 64u)), sxr = sin8(ia);
const int cy = sin8((uint8_t)(ib + 64u)), syr = sin8(ib);
const int cz = sin8((uint8_t)(ic + 64u)), szr = sin8(ic);
for (uint8_t i = 0; i < s->nv; i++) {
int x = s->v[i][0], y = s->v[i][1], z = s->v[i][2];
int ny = (y * cx - z * sxr) >> 14; /* Rx */
int nz = (y * sxr + z * cx) >> 14;
y = ny; z = nz;
int nx = (x * cy + z * syr) >> 14; /* Ry */
nz = (z * cy - x * syr) >> 14;
x = nx; z = nz;
nx = (x * cz - y * szr) >> 14; /* Rz */
ny = (x * szr + y * cz) >> 14;
x = nx; y = ny;
const int zc = z + DIST; /* always > 0 */
px[i] = (int16_t)(CX + (x * FOCAL) / zc);
py[i] = (int16_t)(CY + (y * FOCAL) / zc);
pz[i] = (int16_t)z;
}
clear_screen();
for (uint8_t i = 0; i < s->nf; i++) {
const face_t *f = &s->f[i];
if (!wire && face_area(f) >= 0)
continue; /* hidden face culled */
for (uint8_t k = 0; k < f->n; k++) {
const uint8_t a = f->v[k];
const uint8_t b = f->v[(k + 1u == f->n) ? 0u : k + 1u];
const bool dotted = wire && (pz[a] + pz[b] > 0); /* far half */
draw_edge(px[a], py[a], px[b], py[b], dotted);
}
}
/* Shape name: inverse label, top-left of the status bar (scan-list look). */
const uint8_t end = (uint8_t)(2u + 4u * s->namelen);
for (uint8_t i = 0; i <= end; i++)
A->status_line[i] = 0;
A->print_inverse(s->name, 2, 0, true, true, end);
A->blit_status();
A->blit_full();
if (!paused) {
const uint16_t rate = ROT_RATE[speed - 1u];
ax += rate;
ay += (uint16_t)(rate + rate / 2u);
az += (uint16_t)((rate + 1u) / 2u);
}
A->backlight_update();
A->delay_ms((uint32_t)(32u - speed * 2u)); /* slow low end, no added delay at level 16 */
}
}
+43
View File
@@ -0,0 +1,43 @@
/* Overlay-app link script (POC).
*
* The blob is linked to run at the PY25Q16 sector-cache overlay VMA (shared with
* the multiboot RAM stub — never both active at once). Code + rodata + data +
* bss must all fit in the 4 KiB overlay. objcopy -O binary emits text+rodata+
* data only; the loader zeroes the overlay before copying, so bss starts clean.
*
* VMA must match ADDR(.mb_workspace) in Core/py32f071xb.ld. If the firmware RAM
* layout shifts, update APP_VMA and the loader's compile-time assert catches a
* mismatch. */
/* APP_VMA is the target firmware's __mb_workspace_start (read from its .map). It
* varies with the RAM layout, so pass it from the build: -Wl,--defsym,APP_VMA=0x...
* The default is only a fallback for a standalone size check. */
APP_VMA = DEFINED(APP_VMA) ? APP_VMA : 0x20000280;
APP_LENGTH = 0x1000; /* 4 KiB sector-cache overlay */
ENTRY(app_main)
MEMORY {
APP (rwx) : ORIGIN = APP_VMA, LENGTH = APP_LENGTH
}
SECTIONS {
.app APP_VMA : {
KEEP(*(.text.entry)) /* app_main pinned to offset 0 */
*(.text .text.*)
*(.rodata .rodata.*)
. = ALIGN(4);
*(.data .data.*)
. = ALIGN(4);
__app_bss_start = .;
*(.bss .bss.* COMMON)
. = ALIGN(4);
__app_bss_end = .;
} > APP
__app_end = .;
ASSERT(__app_end <= APP_VMA + APP_LENGTH,
"Broadcast FM overlay app overflows the 4 KiB overlay")
/DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.eh_frame*) *(.comment) *(.note.*) }
}
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# Build one overlay-app blob (.app). Invoked by ../../../compile-app.sh inside
# the uvk1-uvk5v3 Docker image, run from this app's directory. Everything is
# derived from the directory name; only APP_NAME - the human label baked into
# the 64-byte blob header - is per-app. The .app file is named after it (spaces
# stripped), so "Broadcast FM" -> BroadcastFM.app.
set -euo pipefail
APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ...
APP_NAME="Broadcast FM" # <-- the only per-app line
APP_VER="1.0"
APP_API_MIN=1
APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld)
OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM)
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 # drop stale artifacts so discovery is unambiguous
step() { printf '\r 🔨 %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; }
trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
--name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" \
--shortcut fm --require fm --screensaver >/dev/null
trap - ERR
BYTES=$(wc -c < "${APP}.bin")
if [ "$BYTES" -gt 4096 ]; then
printf '\r 🚨 %-13s OVERFLOWS 4 KiB (%d B) \n' "$APP_NAME" "$BYTES"; exit 1
fi
printf '\r ✅ %-13s %4d B (%d%% of 4 KiB) -> %s.app \n' \
"$APP_NAME" "$BYTES" "$(( BYTES * 100 / 4096 ))" "$OUT"
+378
View File
@@ -0,0 +1,378 @@
/* Copyright 2026 Armel F4HWN
* https://github.com/armel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Broadcast FM — overlay app. Sovereign: drives the BK1080 directly and owns the
* radio while running (NO BK4819 dual-watch, unlike the resident FM). Reimplements
* the full resident FM (fm.c / UI_DisplayFM) - VFO + 48 memories - sharing the
* resident channel table (gFM_Channels) and config (gEeprom.FM_*), committed to
* EEPROM on exit. The BK1080 audio is a hardware path, so playback needs no CPU loop.
*
* Keys (iso legacy): 0-9 = frequency (VFO) / channel (MR) entry · UP/DOWN = tune /
* channel step · STAR = manual scan · F+STAR = auto-scan (stores 48) · MENU = save
* (VFO) / delete (MR) · F+1 = band · F+3 = VFO<->MR · F+0 / EXIT = quit.
*/
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include "../app_api.h"
#define STEP 1
#define SCAN_SETTLE 100
#define CHMAX APP_FM_CH_MAX
static const char BAND_NAME[4][10] = { "87.5-108M", "76-108M", "76-90M", "64-76M" };
static const uint8_t FONT_F[9] = {0x3e,0x7f,0x41,0x75,0x75,0x75,0x7d,0x7f,0x3e};
static void cpy(uint8_t *d, const uint8_t *s, uint8_t n){ while(n--)*d++=*s++; }
static const app_api_t *A;
static uint16_t *ch; /* shared gFM_Channels[48] */
static app_fm_state_t st; /* freq_playing, sel_freq, band, is_mr, sel_ch */
static uint16_t lo, hi;
static int8_t scanState; /* 0 = off, +1 up, -1 down (non-blocking) */
static bool autoScan;
static uint8_t chPos;
static uint16_t scanTimer; /* ms until the next scan step */
static bool foundFreq;
static bool askSave, askDelete;
static uint8_t savePos;
static bool fArm;
static uint8_t inBox[4], inIdx;
static char str[16];
static char *putu(char *o, uint32_t v){ char t[6]; int8_t n=0; do{t[n++]=(char)('0'+v%10);v/=10;}while(v&&n<6); while(n--)*o++=t[n]; return o; }
static char *put(char *o, const char *s){ while(*s)*o++=*s++; return o; }
static void bandLimits(void){ lo=A->fm_lo(st.band); hi=A->fm_hi(st.band); }
static uint16_t wrap(uint16_t f){ if(f<lo)return hi; if(f>hi)return lo; return f; }
static void tune(void){ A->fm_set_freq(st.freq_playing, st.band); }
static void dirty(void){ /* nothing yet; committed on exit */ }
static bool validCh(uint8_t c){ return c<CHMAX && ch[c]>=lo && ch[c]<hi; }
static uint8_t findNext(uint8_t c, int8_t dir)
{
for(uint8_t i=0;i<CHMAX;i++){
if(c==0xFF) c=CHMAX-1;
else if(c>=CHMAX) c=0;
if(validCh(c)) return c;
c=(uint8_t)(c+dir);
}
return 0xFF;
}
static int configChannel(void)
{
st.freq_playing=st.sel_freq;
if(st.is_mr){
uint8_t c=findNext(st.sel_ch,+1);
if(c==0xFF){ st.is_mr=false; return -1; }
st.sel_ch=c; st.freq_playing=ch[c];
}
return 0;
}
/* ---- drawing (mirror UI_DisplayFM) ---- */
static void freqBig(uint16_t f)
{
uint16_t mhz=f/10u; char *o=str;
if(mhz<100u)*o++=' ';
o=putu(o,mhz); *o++='.'; *o++=(char)('0'+f%10u); *o='\0';
A->display_freq(str,36,1,true);
}
/* append a zero-padded 2-digit number (1..99), return the end pointer */
static char *num2(char *o, uint8_t n){ if(n<10)*o++='0'; return putu(o,n); }
static void draw(void)
{
char b[12], *o;
A->display_clear();
A->status_clear();
A->draw_battery();
if(fArm) cpy(A->status_line+70,FONT_F,sizeof(FONT_F)); /* F armed indicator */
A->print_string("FM",2,0,0,8);
A->print_normal(BAND_NAME[st.band],1,0,6);
/* ---- status (line 3) ---- */
if(askSave) { o=put(b,"SAVE?"); *o='\0'; }
else if(askDelete) { o=put(b,"DEL?"); *o='\0'; }
else if(scanState) {
if(autoScan){ o=put(b,"A-SCAN("); o=putu(o,chPos); *o++=')'; } /* count like UI_DisplayFM */
else o=put(b,"M-SCAN");
*o='\0';
}
else if(st.is_mr) { o=put(b,"MR(CH"); o=num2(o,(uint8_t)(st.sel_ch+1)); *o++=')'; *o='\0'; }
else {
o=put(b,"VFO"); *o='\0';
for(uint8_t i=0;i<CHMAX;i++) if(ch[i]==st.freq_playing){ o=put(b,"VFO(CH"); o=num2(o,(uint8_t)(i+1)); *o++=')'; *o='\0'; break; }
}
A->print_string(b,0,127,3,10);
/* ---- line 1: input in progress / channel prompt / frequency ---- */
if(inIdx>0){
if(st.is_mr||askSave){ /* channel number: "CH-XX" */
o=put(b,"CH-");
*o++=(char)('0'+inBox[0]);
*o++=(inIdx>1)?(char)('0'+inBox[1]):'-';
*o='\0';
A->print_string(b,0,127,1,10);
} else { /* VFO frequency: "ddd.d" */
b[0]=(char)('0'+inBox[0]);
b[1]=(inIdx>1)?(char)('0'+inBox[1]):'-';
b[2]=(inIdx>2)?(char)('0'+inBox[2]):'-';
b[3]='.';
b[4]=(inIdx>3)?(char)('0'+inBox[3]):'-';
b[5]='\0';
A->display_freq(b,36,1,false);
}
} else if(askSave){ /* pick a save slot */
o=put(b,"CH-"); o=num2(o,(uint8_t)(savePos+1)); *o='\0';
A->print_string(b,0,127,1,10);
} else if(askDelete){
o=put(b,"CH-"); o=num2(o,(uint8_t)(st.sel_ch+1)); *o='\0';
A->print_string(b,0,127,1,10);
} else {
freqBig(st.freq_playing);
}
}
static void show(void){ draw(); A->blit_status(); A->blit_full(); }
/* ---- non-blocking scan state machine (mirrors ACTION_Scan_FM / FM_Play / FM_Tune) ---- */
static void scanTuneNext(void) /* FM_Tune(freq, scanState, false): step + mute + settle */
{
A->fm_mute(true);
foundFreq=false;
st.freq_playing=wrap((uint16_t)(st.freq_playing+scanState));
tune();
scanTimer=SCAN_SETTLE;
}
static void stopScan(void) /* FM_PlayAndUpdate */
{
if(autoScan){ st.is_mr=true; st.sel_ch=0; }
scanState=0; autoScan=false;
configChannel();
tune();
A->fm_mute(false);
}
static void scanStep(void) /* FM_Play: one step when the settle timer expires */
{
if(A->fm_valid(st.freq_playing,lo)==0){ /* station locked */
if(!autoScan){
scanState=0; foundFreq=true;
if(!st.is_mr) st.sel_freq=st.freq_playing;
A->fm_mute(false); /* audio on */
return;
}
if(chPos<CHMAX) ch[chPos++]=st.freq_playing;
if(chPos>=CHMAX){ stopScan(); return; }
}
if(autoScan && st.freq_playing>=A->fm_hi(1)){ stopScan(); return; }
scanTuneNext();
}
static void startManual(int8_t dir)
{
if(scanState){ stopScan(); return; } /* STAR toggles */
autoScan=false; chPos=0; scanState=dir;
scanTuneNext();
}
static void startAuto(void)
{
if(scanState){ stopScan(); return; }
autoScan=true; chPos=0; scanState=+1;
for(uint8_t i=0;i<CHMAX;i++) ch[i]=0xFFFF;
st.freq_playing=lo;
A->fm_mute(true);
tune(); /* FM_Tune(lo,1,true): no step */
scanTimer=SCAN_SETTLE;
}
/* ---- digit entry ---- */
static void digit(uint8_t d)
{
if(askDelete||scanState) return;
if(askSave || st.is_mr){ /* 2-digit channel */
if(inIdx<2) inBox[inIdx++]=d;
if(inIdx>=2){
uint8_t c=(uint8_t)(inBox[0]*10+inBox[1]-1); inIdx=0;
if(askSave){ if(c<CHMAX) savePos=c; }
else if(validCh(c)){ st.sel_ch=c; st.freq_playing=ch[c]; tune(); }
}
return;
}
/* VFO frequency: 4 digits, with the leading-digit>1 zero-pad trick */
inBox[inIdx++]=d;
if(inIdx==1 && inBox[0]>1){ inBox[1]=inBox[0]; inBox[0]=0; inIdx=2; }
else if(inIdx>3){
uint16_t f=(uint16_t)(inBox[0]*1000+inBox[1]*100+inBox[2]*10+inBox[3]); inIdx=0;
if(f>=lo && f<=hi){ st.sel_freq=f; st.freq_playing=f; tune(); }
}
}
/* ---- MENU: save (VFO) / delete (MR) ---- */
static void menu(void)
{
inIdx=0;
if(!st.is_mr){ /* VFO: save */
if(askSave){ ch[savePos]=st.freq_playing; dirty(); }
askSave=!askSave;
} else { /* MR: delete */
if(askDelete){ ch[st.sel_ch]=0xFFFF; configChannel(); tune(); dirty(); }
askDelete=!askDelete;
}
}
/* ---- UP/DOWN: tune (VFO) or channel step (MR) ---- */
static void upDown(int8_t step)
{
if(scanState){ scanState=step; scanTuneNext(); return; } /* continue scan, new direction */
if(askSave){
if(step>0){ if(++savePos>=CHMAX) savePos=0; }
else savePos=savePos?(uint8_t)(savePos-1u):(uint8_t)(CHMAX-1u);
return;
}
if(st.is_mr){
uint8_t c=findNext((uint8_t)(st.sel_ch+step),step);
if(c!=0xFF && c!=st.sel_ch){ st.sel_ch=c; st.freq_playing=ch[c]; tune(); }
} else {
st.sel_freq=wrap((uint16_t)(st.sel_freq+step));
st.freq_playing=st.sel_freq; tune();
}
}
static void toggleMr(void)
{
st.is_mr=!st.is_mr;
if(configChannel()!=0){ /* no valid channel: stayed VFO */ }
tune();
askSave=askDelete=false; inIdx=0;
}
static bool running;
static void cycleBand(void)
{
st.band=(uint8_t)((st.band+1)&3u); bandLimits();
if(st.freq_playing<lo||st.freq_playing>hi){ st.freq_playing=lo; st.sel_freq=lo; }
tune();
}
/* F+key and long-press functions (band / VFO<->MR / auto-scan / quit). */
static void func(uint8_t key)
{
switch(key){
case APP_KEY_0: running=false; break;
case APP_KEY_1: cycleBand(); break;
case APP_KEY_3: toggleMr(); break;
case APP_KEY_STAR: startAuto(); break;
default: break;
}
}
static void onShort(uint8_t key)
{
if(key==APP_KEY_F){ fArm=!fArm; return; }
if(fArm){ fArm=false; func(key); return; } /* F + key */
if(key<=APP_KEY_9){ digit(key); return; }
switch(key){
case APP_KEY_UP:
case APP_KEY_DOWN: upDown(A->nav_dir(key)); break;
case APP_KEY_STAR: startManual(+1); break;
case APP_KEY_MENU: menu(); break;
case APP_KEY_EXIT:
if(inIdx) inIdx--;
else if(askSave||askDelete) askSave=askDelete=false;
else running=false;
break;
default: break;
}
}
static void onLong(uint8_t key){ fArm=false; func(key); } /* long band / MR / scan / quit */
__attribute__((section(".text.entry"),used))
void app_main(const app_api_t *api)
{
A=api;
A->backlight_on();
ch=A->fm_channels;
A->fm_state(&st,false); /* inherit the resident FM state */
if(st.band>3) st.band=0;
bandLimits();
if(st.freq_playing<lo||st.freq_playing>hi) st.freq_playing=lo;
if(st.sel_freq<lo||st.sel_freq>hi) st.sel_freq=st.freq_playing;
if(st.is_mr) configChannel();
askSave=askDelete=fArm=false; inIdx=0; savePos=0;
A->fm_enter(st.freq_playing,st.band);
/* short = on release (held < ~0.5 s) · long = at the 0.5 s threshold ·
UP/DOWN = immediate step on press, then auto-repeat while held. */
running=true;
uint8_t held=APP_KEY_INVALID;
uint16_t heldMs=0;
bool firedLong=false;
bool dirty=true; /* redraw only on change */
while(running){
uint8_t key=A->get_key();
bool rep=(key==APP_KEY_UP||key==APP_KEY_DOWN);
if(key==APP_KEY_SAVER){
held=APP_KEY_INVALID; heldMs=0; firedLong=false;
A->delay_ms(10); A->backlight_update();
continue;
} else if(key==APP_KEY_WAKE||key==APP_KEY_PTT){
held=APP_KEY_INVALID; heldMs=0; firedLong=false; dirty=true;
} else if(key==APP_KEY_INVALID){
if(held!=APP_KEY_INVALID && !firedLong &&
held!=APP_KEY_UP && held!=APP_KEY_DOWN){
onShort(held); dirty=true;
}
held=APP_KEY_INVALID; heldMs=0; firedLong=false;
} else if(key!=held){
A->backlight_on();
held=key; heldMs=0; firedLong=false;
if(rep){ onShort(key); dirty=true; } /* immediate first step */
} else {
heldMs=(uint16_t)(heldMs+50u);
if(rep){
if(heldMs>=300){ onShort(held); dirty=true; } /* auto-repeat */
} else if(!firedLong && heldMs>=400){
firedLong=true; onLong(held); dirty=true;
}
}
/* non-blocking scan: one step each time the settle timer expires */
if(scanState){
/* Match resident FM: an active scan keeps the display awake. */
A->backlight_on();
if(scanTimer<=50) scanStep();
else scanTimer=(uint16_t)(scanTimer-50);
dirty=true;
}
if(dirty){ show(); dirty=false; }
/* Keep the resident 10 ms fade cadence while retaining this app's
* existing 50 ms key/scan state-machine tick. */
for(uint8_t i=0;i<5u;i++){
A->delay_ms(10);
A->backlight_update();
}
}
/* push our state back to the resident FM and persist (config + 48 channels) */
if(!st.is_mr) st.sel_freq=st.freq_playing;
A->fm_state(&st,true);
A->fm_commit();
A->fm_exit();
}
+43
View File
@@ -0,0 +1,43 @@
/* Overlay-app link script (POC).
*
* The blob is linked to run at the PY25Q16 sector-cache overlay VMA (shared with
* the multiboot RAM stub — never both active at once). Code + rodata + data +
* bss must all fit in the 4 KiB overlay. objcopy -O binary emits text+rodata+
* data only; the loader zeroes the overlay before copying, so bss starts clean.
*
* VMA must match ADDR(.mb_workspace) in Core/py32f071xb.ld. If the firmware RAM
* layout shifts, update APP_VMA and the loader's compile-time assert catches a
* mismatch. */
/* APP_VMA is the target firmware's __mb_workspace_start (read from its .map). It
* varies with the RAM layout, so pass it from the build: -Wl,--defsym,APP_VMA=0x...
* The default is only a fallback for a standalone size check. */
APP_VMA = DEFINED(APP_VMA) ? APP_VMA : 0x20000280;
APP_LENGTH = 0x1000; /* 4 KiB sector-cache overlay */
ENTRY(app_main)
MEMORY {
APP (rwx) : ORIGIN = APP_VMA, LENGTH = APP_LENGTH
}
SECTIONS {
.app APP_VMA : {
KEEP(*(.text.entry)) /* app_main pinned to offset 0 */
*(.text .text.*)
*(.rodata .rodata.*)
. = ALIGN(4);
*(.data .data.*)
. = ALIGN(4);
__app_bss_start = .;
*(.bss .bss.* COMMON)
. = ALIGN(4);
__app_bss_end = .;
} > APP
__app_end = .;
ASSERT(__app_end <= APP_VMA + APP_LENGTH,
"FoxHunt overlay app overflows the 4 KiB overlay")
/DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.eh_frame*) *(.comment) *(.note.*) }
}
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# Build one overlay-app blob (.app). Invoked by ../../../compile-app.sh inside
# the uvk1-uvk5v3 Docker image, run from this app's directory. Everything is
# derived from the directory name; only APP_NAME - the human label baked into
# the 64-byte blob header - is per-app. The .app file is named after it (spaces
# stripped), so "Broadcast FM" -> BroadcastFM.app.
set -euo pipefail
APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ...
APP_NAME="FoxHunt" # <-- the only per-app line
APP_VER="1.0"
APP_API_MIN=1
APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld)
OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM)
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 # drop stale artifacts so discovery is unambiguous
step() { printf '\r 🔨 %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; }
trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
--name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" \
--shortcut foxhunt >/dev/null
trap - ERR
BYTES=$(wc -c < "${APP}.bin")
if [ "$BYTES" -gt 4096 ]; then
printf '\r 🚨 %-13s OVERFLOWS 4 KiB (%d B) \n' "$APP_NAME" "$BYTES"; exit 1
fi
printf '\r ✅ %-13s %4d B (%d%% of 4 KiB) -> %s.app \n' \
"$APP_NAME" "$BYTES" "$(( BYTES * 100 / 4096 ))" "$OUT"
+375
View File
@@ -0,0 +1,375 @@
/* Copyright 2026 Armel F4HWN
* https://github.com/armel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* FoxHunt (RX) — overlay app. Ported from App/app/foxhunt.c, hunt sub-mode only
* (the beacon becomes a separate app). Signal-strength hunter: corrected dBm with
* a big number, an IARU S-meter staircase or a scrolling history, peak/min hold,
* a 1 s trend, a front-end attenuator ladder, and Geiger/station audio.
*
* Keys: 1 gauge (bar/history) · 2 audio (off/beep/station) · 3 + UP/DOWN attenuator
* MENU reset holds · long F keypad lock · EXIT quit.
*/
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include "../app_api.h"
#define LCD_WIDTH 128
#define BK_REG_13 0x13
/* --- tuning (from foxhunt.c) --- */
#define DBM_FLOOR (-141)
#define DBM_CEIL (-53)
#define TICK_MS 50
#define TREND_TICKS 20
#define LOCK_HOLD_MS 500
#define SILENCE_DBM (-120)
#define TONE_MIN 400
#define TONE_MAX 2400
#define RATE_SLOW 20
#define RATE_FAST 2
#define BLIP_MS 50
#define AUDIO_SETTLE 60
#define ATT_SETTLE 40
#define ATT_COUNT 6
#define ATT_BYP0 4
#define SEG_COUNT 13
#define BAR_X0 6
#define SEG_PITCH 9
#define SEG_W 8
#define SEG_BOTTOM 37
#define HIST_LEN 120
#define HIST_DECIM 3
#define GRAPH_X0 4
#define GRAPH_TOP 27
#define GRAPH_BOT 45
#define GRAPH_FLOOR 2
#define AUDIO_OFF 0
#define AUDIO_BEEP 1
#define AUDIO_STATION 2
#define GRAPH_BAR 0
#define GRAPH_HIST 1
#define CFG_MAGIC 0xF1 /* config validity marker */
static const uint16_t ATT_REG13[ATT_COUNT] = { 0x03DF, 0x03DD, 0x03DB, 0x03D9, 0x0379, 0x0139 };
static const uint8_t ATT_DB[ATT_BYP0] = { 0, 6, 15, 27 };
/* Status-bar / trend icons (bytes copied verbatim from App/bitmaps.c). */
static const uint8_t BMP_BARS[11] = {0x40,0x40,0x00,0x70,0x70,0x00,0x7c,0x7c,0x00,0x7f,0x7f};
static const uint8_t BMP_GRAPH[15] = {0x08,0x04,0x02,0x04,0x08,0x10,0x20,0x10,0x08,0x04,0x02,0x04,0x08,0x10,0x20};
static const uint8_t BMP_SIGNAL[10] = {0x08,0x1c,0x1c,0x08,0x00,0x22,0x1c,0x41,0x22,0x1c};
static const uint8_t BMP_SPEAKER[10] = {0x1c,0x1c,0x3e,0x7f,0x00,0x22,0x1c,0x41,0x22,0x1c};
static const uint8_t BMP_UP[11] = {0x20,0x30,0x38,0x3c,0x3e,0x3f,0x3e,0x3c,0x38,0x30,0x20};
static const uint8_t BMP_DOWN[11] = {0x01,0x03,0x07,0x0f,0x1f,0x3f,0x1f,0x0f,0x07,0x03,0x01};
static const uint8_t BMP_FLAT[11] = {0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12};
static const uint8_t FONT_F[9] = {0x3e,0x7f,0x41,0x75,0x75,0x75,0x7d,0x7f,0x3e};
static const uint8_t FONT_LOCK[9] = {0x7c,0x46,0x45,0x45,0x45,0x45,0x45,0x46,0x7c};
static void cpy(uint8_t *d, const uint8_t *s, uint8_t n){ while(n--)*d++=*s++; }
static const app_api_t *A;
static bool foxLocked, fArm, fLongDone;
static uint16_t fHoldMs;
static uint8_t foxAudioMode, foxGraphMode, attStep, audioTick;
static int16_t curDbm, peakDbm, minDbm, trendRef, trendDelta;
static uint8_t trendTick;
static uint8_t histBuf[HIST_LEN];
static uint8_t histHead, histTick;
static int16_t histEma;
static uint8_t prevKey;
static bool running;
static char str[16];
/* ---- tiny formatting ---- */
static uint8_t slen(const char *s){ uint8_t n=0; while(s[n])n++; return n; }
static char *put(char *o,const char *s){ while(*s)*o++=*s++; return o; }
static char *puti(char *o,int v){
uint32_t u;
if(v<0){*o++='-';u=(uint32_t)(-v);} else u=(uint32_t)v;
char t[6]; int8_t n=0;
do{t[n++]=(char)('0'+u%10u);u/=10u;}while(u&&n<6);
while(n--)*o++=t[n];
return o;
}
static void i2str(char *out,int v){ *puti(out,v)='\0'; }
/* ---- radio helpers ---- */
static int16_t iabs16(int16_t v){ return v<0?-v:v; }
static void applyAtt(void){
uint16_t reg = A->bk_read(BK_REG_13);
reg = (uint16_t)((reg & ~0x03FFu) | ATT_REG13[attStep]);
A->bk_write(BK_REG_13, reg);
}
static void setAudio(void){
if(foxAudioMode==AUDIO_OFF){ A->audio_path(false); return; }
A->audio_path(true);
A->delay_ms(AUDIO_SETTLE);
A->set_af(APP_AF_MUTE);
}
static int32_t lerp(int16_t dbm,int32_t lo,int32_t hi){
if(dbm<=DBM_FLOOR) return lo;
if(dbm>=DBM_CEIL) return hi;
const int32_t delta = hi - lo;
const uint32_t distance = ((uint32_t)(dbm-DBM_FLOOR) *
(uint32_t)(delta < 0 ? -delta : delta)) /
(uint32_t)(DBM_CEIL-DBM_FLOOR);
return lo + (delta < 0 ? -(int32_t)distance : (int32_t)distance);
}
static void blip(uint16_t freq){
A->prepare_tone();
A->play_tone_raw(freq, BLIP_MS);
A->tones_off_rx();
A->set_agc(false);
applyAtt();
}
static uint8_t fillCount(int16_t dbm){
int16_t n;
if(dbm<-141) return 0;
if(dbm<=-93) n=(int16_t)(1u+(uint16_t)(dbm+141)/6u);
else n=(int16_t)(9u+(uint16_t)(dbm+93)/10u);
if(n>SEG_COUNT) n=SEG_COUNT;
return (uint8_t)n;
}
static void buildS(char *out,int16_t dbm){
if(dbm>=-93){ int16_t o=dbm-(-93); if(o>40)o=40; char *p=put(out,"S9+"); if(o<10)*p++='0'; i2str(p,o); }
else if(dbm<-141) put(out,"S0")[0]='\0';
else { char *p=put(out,"S"); i2str(p,(int)((uint16_t)(dbm+147)/6u)); }
}
static int16_t div_trunc_pow2(int32_t v, uint8_t shift)
{
if (v < 0)
return (int16_t)-((uint32_t)(-v) >> shift);
return (int16_t)((uint32_t)v >> shift);
}
static uint8_t cycleIndex(uint8_t value, uint8_t count, int8_t dir)
{
if (dir > 0)
return ++value < count ? value : 0u;
return value > 0u ? (uint8_t)(value - 1u) : (uint8_t)(count - 1u);
}
static void histSample(void){
histEma += div_trunc_pow2((int32_t)curDbm * 8 - histEma, 2u);
if(++histTick<HIST_DECIM) return;
histTick=0;
histBuf[histHead]=fillCount(div_trunc_pow2(histEma, 3u));
if(++histHead>=HIST_LEN) histHead=0;
}
static void rebase(void){
curDbm=A->rssi_dbm();
peakDbm=minDbm=trendRef=curDbm;
trendDelta=0; trendTick=0;
uint8_t lvl=fillCount(curDbm);
for(uint8_t i=0;i<HIST_LEN;i++) histBuf[i]=lvl;
histHead=0; histTick=0; histEma=(int16_t)(curDbm*8);
}
static void attCycle(int8_t dir){
attStep=cycleIndex(attStep,ATT_COUNT,dir);
applyAtt();
A->delay_ms(ATT_SETTLE);
rebase();
}
/* ---- drawing ---- */
static void fillRect(int16_t x0,int16_t y0,int16_t x1,int16_t y1){
for(int16_t x=x0;x<=x1;x++) A->draw_line(A->fb,x,y0,x,y1,true);
}
static void drawBar(void){
uint8_t n=fillCount(curDbm);
for(uint8_t i=0;i<n;i++){
int16_t sx=BAR_X0+i*SEG_PITCH;
fillRect(sx,SEG_BOTTOM-i,sx+SEG_W-1,SEG_BOTTOM);
}
A->draw_line(A->fb,BAR_X0,SEG_BOTTOM+2,BAR_X0+(SEG_COUNT-1)*SEG_PITCH+SEG_W-1,SEG_BOTTOM+2,true);
}
static void drawHist(void){
const uint8_t floorY=GRAPH_BOT-GRAPH_FLOOR;
const uint8_t span=floorY-GRAPH_TOP;
A->draw_line(A->fb,GRAPH_X0,GRAPH_BOT,GRAPH_X0+HIST_LEN-1,GRAPH_BOT,true);
uint8_t prevY=0, idx=histHead;
for(uint8_t c=0;c<HIST_LEN;c++){
uint8_t lvl=histBuf[idx]; if(++idx>=HIST_LEN)idx=0;
if(lvl>SEG_COUNT)lvl=SEG_COUNT;
uint8_t x=GRAPH_X0+c;
uint8_t y=(uint8_t)(floorY-((uint32_t)lvl*span)/(uint32_t)SEG_COUNT);
for(uint8_t yy=y+1;yy<=floorY;yy++) if(((x+yy)&1)==0) A->draw_line(A->fb,x,yy,x,yy,true);
if(c==0){ A->draw_line(A->fb,x,y,x,y,true); }
else { uint8_t lo=(y<prevY)?y:prevY, hi=(y<prevY)?prevY:y; A->draw_line(A->fb,x,lo,x,hi,true); }
prevY=y;
}
}
static void tag(const char *s,uint8_t x,uint8_t line){
A->print_inverse(s,x,line,false,true,(uint8_t)(x+slen(s)*4));
}
static void draw(void){
char big[8], sMeter[8];
A->display_clear();
A->status_clear();
A->print_inverse("FOX HUNT",2,0,true,true,34);
A->draw_battery();
/* status-bar icons: graph mode @38, audio mode @55, F/lock @69 */
if(foxGraphMode==GRAPH_HIST) cpy(A->status_line+38,BMP_GRAPH,15);
else cpy(A->status_line+38,BMP_BARS,11);
if(foxAudioMode==AUDIO_BEEP) cpy(A->status_line+55,BMP_SIGNAL,10);
else if(foxAudioMode==AUDIO_STATION) cpy(A->status_line+55,BMP_SPEAKER,10);
if(foxLocked) cpy(A->status_line+70,FONT_LOCK,9);
else if(fArm) cpy(A->status_line+70,FONT_F,sizeof(FONT_F));
i2str(big,curDbm);
A->display_freq(big,2,0,false);
A->print_normal("dBm",(uint8_t)(slen(big)*13+4),0,1);
/* trend arrow (line 0, right) + signed delta (line 1) */
cpy(A->fb[0]+115, trendDelta>0?BMP_UP:trendDelta<0?BMP_DOWN:BMP_FLAT, 11);
if(trendDelta!=0){
char *p=str; if(trendDelta>0)*p++='+'; else{*p++='-';}
int16_t a=iabs16(trendDelta); if(a<10){*p++='0';} i2str(p,a);
A->print_normal("dBm",127-3*7,0,1);
A->print_normal(str,(uint8_t)(127-3*7-2-slen(str)*7),1,1);
}
i2str(put(str,"PK "),peakDbm);
tag(str,4,2);
i2str(put(str,"MN "),minDbm);
tag(str,(uint8_t)(((uint32_t)LCD_WIDTH-(uint32_t)slen(str)*4u)/2u),2);
buildS(sMeter,curDbm);
A->print_inverse(sMeter,(uint8_t)(126-slen(sMeter)*4),2,false,true,125);
if(foxGraphMode==GRAPH_HIST) drawHist();
else drawBar();
if(attStep<ATT_BYP0){ char *o=put(str,"ATT "); o=puti(o,ATT_DB[attStep]); put(o,"dB")[0]='\0'; }
else put(str,(attStep==ATT_BYP0)?"BYP":"BYP+")[0]='\0';
tag(str,4,6);
{ uint32_t f=A->rx_freq(); char *o=puti(str,(int)(f/100000u)); *o++='.';
uint32_t fr=f%100000u; for(int8_t d=4;d>=0;d--){ uint32_t p=1; for(int8_t k=0;k<d;k++)p*=10; *o++=(char)('0'+(fr/p)%10);} *o='\0'; }
A->print_normal(str,(uint8_t)(126-slen(str)*7),0,6);
}
/* ---- config (deferred) ---- */
static void loadConfig(void){
uint8_t c[4];
A->cfg_load(c,4);
if(c[0]==CFG_MAGIC){
if(c[1]<ATT_COUNT) attStep=c[1];
if(c[2]<=GRAPH_HIST) foxGraphMode=c[2];
if(c[3]<=AUDIO_STATION) foxAudioMode=c[3];
}
}
static void saveConfig(void){
uint8_t c[4]={CFG_MAGIC,attStep,foxGraphMode,foxAudioMode};
A->cfg_save(c,4);
}
/* ---- input ---- */
static void handleKeys(void){
uint8_t key=A->get_key();
/* long-press F -> keypad lock */
if(key==APP_KEY_F){
if(!fLongDone){ fHoldMs+=TICK_MS; if(fHoldMs>=LOCK_HOLD_MS){ fLongDone=true; foxLocked=!foxLocked; fArm=false; A->backlight_on(); } }
} else { fHoldMs=0; fLongDone=false; }
if(key==APP_KEY_INVALID||key==prevKey){ prevKey=key; return; }
prevKey=key;
A->backlight_on();
if(foxLocked){
if(key==APP_KEY_UP||key==APP_KEY_DOWN) attCycle(A->nav_dir(key));
return;
}
if(key==APP_KEY_F){ fArm=!fArm; return; }
int8_t dir=fArm?-1:1;
switch(key){
case APP_KEY_EXIT: running=false; break;
case APP_KEY_1: foxGraphMode^=1; break;
case APP_KEY_2:
foxAudioMode=cycleIndex(foxAudioMode,3u,dir); setAudio();
if(foxAudioMode==AUDIO_BEEP){ audioTick=RATE_SLOW; }
break;
case APP_KEY_3: attCycle(dir); break;
case APP_KEY_UP:
case APP_KEY_DOWN: attCycle(A->nav_dir(key)); break;
case APP_KEY_MENU: peakDbm=minDbm=trendRef=curDbm; break;
default: break;
}
fArm=false;
}
static void tickDelay(void){
for(uint8_t i=0;i<TICK_MS/10;i++){ A->delay_ms(10); A->backlight_update(); }
}
__attribute__((section(".text.entry"),used))
void app_main(const app_api_t *api){
A=api;
foxLocked=fArm=fLongDone=false; fHoldMs=0;
attStep=0; foxGraphMode=GRAPH_BAR; foxAudioMode=AUDIO_OFF;
prevKey=APP_KEY_INVALID;
loadConfig();
A->backlight_on();
A->set_agc(false);
applyAtt();
setAudio();
A->delay_ms(ATT_SETTLE);
rebase();
running=true;
while(running){
handleKeys();
if(!running) break;
curDbm=A->rssi_dbm();
if(curDbm>peakDbm) peakDbm=curDbm;
if(curDbm<minDbm) minDbm=curDbm;
if(++trendTick>=TREND_TICKS){ trendDelta=curDbm-trendRef; trendRef=curDbm; trendTick=0; }
histSample();
draw();
A->blit_status();
A->blit_full();
if(foxAudioMode==AUDIO_BEEP && curDbm>=SILENCE_DBM){
if(++audioTick>=(uint8_t)lerp(curDbm,RATE_SLOW,RATE_FAST)){ audioTick=0; blip((uint16_t)lerp(curDbm,TONE_MIN,TONE_MAX)); }
} else if(foxAudioMode==AUDIO_STATION){
A->set_af(curDbm>=SILENCE_DBM?APP_AF_FM:APP_AF_MUTE);
}
A->battery_sample();
tickDelay();
}
saveConfig();
A->audio_path(false);
}
+109
View File
@@ -0,0 +1,109 @@
#!/usr/bin/env python3
# Pack an overlay-app raw binary into a .app blob: 64-byte header + code.
# Header layout mirrors app_overlay.h : app_header_t (little-endian, packed).
# The format/version constants (magic, header + ABI/API levels, overlay budget) are
# read straight from the C headers the firmware itself compiles, so there is a
# SINGLE source of truth - the packer can never silently drift from the loader.
# CRC-32 is zlib/PKZIP (init 0xFFFFFFFF, poly 0xEDB88320, final XOR) to match
# the firmware's mb_ext_image_crc32 / mb_crc32_bytes.
#
# ./pack_app.py breakout/breakout.bin breakout/breakout.app --name Breakout \
# --ver 1.0 --vma 0x20000280 --api-min 1
import argparse, os, re, struct, zlib, sys
HERE = os.path.dirname(os.path.abspath(__file__))
def cdefine(header: str, name: str) -> int:
"""Value of a `#define <name> <int-literal>` in a sibling C header.
Accepts a decimal or 0x-hex literal with optional u/U/l/L suffixes. Keeps the
blob format tied to the firmware's own headers (single source of truth)."""
pat = re.compile(r"^\s*#define\s+" + re.escape(name) +
r"\s+(0[xX][0-9a-fA-F]+|\d+)[uUlL]*\b")
with open(os.path.join(HERE, header)) as f:
for line in f:
m = pat.match(line)
if m:
return int(m.group(1), 0)
sys.exit(f"{header}: #define {name} not found")
# Single source of truth: the C headers the firmware also compiles.
MAGIC = cdefine("app_overlay.h", "APP_MAGIC").to_bytes(4, "little") # 0x31504146 -> b"FAP1"
HDR_VERSION = cdefine("app_overlay.h", "APP_HDR_VERSION")
ABI_MAJOR = cdefine("app_api.h", "APP_ABI_MAJOR")
API_LEVEL = cdefine("app_api.h", "APP_API_LEVEL")
OVERLAY_MAX = cdefine("app_overlay.h", "APP_OVERLAY_MAX") # 4 KiB overlay budget
FLAG_COMMITTED = cdefine("app_overlay.h", "APP_FLAG_COMMITTED")
FLAG_SCREEN_SAVER = cdefine("app_overlay.h", "APP_FLAG_SCREEN_SAVER")
FLAG_SHORTCUT_SHIFT = cdefine("app_overlay.h", "APP_FLAG_SHORTCUT_SHIFT")
SHORTCUTS = {
"none": 0,
"fm": cdefine("app_overlay.h", "APP_SHORTCUT_FM"),
"foxhunt": cdefine("app_overlay.h", "APP_SHORTCUT_FOXHUNT"),
"beacon": cdefine("app_overlay.h", "APP_SHORTCUT_BEACON"),
"beam": cdefine("app_overlay.h", "APP_SHORTCUT_BEAM"),
}
CAPABILITIES = {
"fm": cdefine("app_overlay.h", "APP_CAP_FM"),
"trivfo": cdefine("app_overlay.h", "APP_CAP_TRIVFO"),
"beam": cdefine("app_overlay.h", "APP_CAP_BEAM"),
}
def field(s: str, n: int) -> bytes:
b = s.encode("ascii", "strict")[: n - 1]
return b + b"\x00" * (n - len(b))
def main():
ap = argparse.ArgumentParser()
ap.add_argument("infile")
ap.add_argument("outfile")
ap.add_argument("--name", default="app")
ap.add_argument("--ver", default="1.0")
ap.add_argument("--entry", type=lambda x: int(x, 0), default=0)
ap.add_argument("--api-min", type=int, required=True,
help="minimum append-only firmware API level required by this app")
ap.add_argument("--vma", type=lambda x: int(x, 0), required=True,
help="RAM VMA the app was linked at (must match the firmware overlay)")
ap.add_argument("--screensaver", action="store_true",
help="allow the resident BLTime screen saver while this app is idle")
ap.add_argument("--shortcut", choices=SHORTCUTS, default="none",
help="resident quick action advertised by this app")
ap.add_argument("--require", action="append", choices=CAPABILITIES, default=[],
help="resident capability required by this app (repeatable)")
a = ap.parse_args()
if not 1 <= a.api_min <= API_LEVEL:
sys.exit(f"--api-min must be between 1 and current API level {API_LEVEL}")
code = open(a.infile, "rb").read()
if len(code) == 0:
sys.exit("empty input")
if len(code) > OVERLAY_MAX:
sys.exit(f"code {len(code)} B exceeds overlay budget {OVERLAY_MAX} B")
crc = zlib.crc32(code) & 0xFFFFFFFF
flags = (FLAG_COMMITTED |
(FLAG_SCREEN_SAVER if a.screensaver else 0) |
(SHORTCUTS[a.shortcut] << FLAG_SHORTCUT_SHIFT))
required_caps = 0
for capability in a.require:
required_caps |= CAPABILITIES[capability]
header = struct.pack(
"<4sHBBIIHH16s16sII4s",
MAGIC, HDR_VERSION, ABI_MAJOR, a.api_min,
len(code), crc, a.entry, flags,
field(a.name, 16), field(a.ver, 16), a.vma,
required_caps, b"\x00" * 4,
)
assert len(header) == 64, len(header)
with open(a.outfile, "wb") as f:
f.write(header)
f.write(code)
print(f"{a.outfile}: name={a.name!r} ver={a.ver!r} "
f"abi={ABI_MAJOR} api>={a.api_min} caps=0x{required_caps:08x} "
f"vma=0x{a.vma:08x} "
f"code={len(code)} B crc32=0x{crc:08x} -> blob {64 + len(code)} B")
if __name__ == "__main__":
main()
+43
View File
@@ -0,0 +1,43 @@
/* Overlay-app link script (POC).
*
* The blob is linked to run at the PY25Q16 sector-cache overlay VMA (shared with
* the multiboot RAM stub — never both active at once). Code + rodata + data +
* bss must all fit in the 4 KiB overlay. objcopy -O binary emits text+rodata+
* data only; the loader zeroes the overlay before copying, so bss starts clean.
*
* VMA must match ADDR(.mb_workspace) in Core/py32f071xb.ld. If the firmware RAM
* layout shifts, update APP_VMA and the loader's compile-time assert catches a
* mismatch. */
/* APP_VMA is the target firmware's __mb_workspace_start (read from its .map). It
* varies with the RAM layout, so pass it from the build: -Wl,--defsym,APP_VMA=0x...
* The default is only a fallback for a standalone size check. */
APP_VMA = DEFINED(APP_VMA) ? APP_VMA : 0x20000280;
APP_LENGTH = 0x1000; /* 4 KiB sector-cache overlay */
ENTRY(app_main)
MEMORY {
APP (rwx) : ORIGIN = APP_VMA, LENGTH = APP_LENGTH
}
SECTIONS {
.app APP_VMA : {
KEEP(*(.text.entry)) /* app_main pinned to offset 0 */
*(.text .text.*)
*(.rodata .rodata.*)
. = ALIGN(4);
*(.data .data.*)
. = ALIGN(4);
__app_bss_start = .;
*(.bss .bss.* COMMON)
. = ALIGN(4);
__app_bss_end = .;
} > APP
__app_end = .;
ASSERT(__app_end <= APP_VMA + APP_LENGTH,
"Plasma overlay app overflows the 4 KiB overlay")
/DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.eh_frame*) *(.comment) *(.note.*) }
}
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# Build one overlay-app blob (.app). Invoked by ../../../compile-app.sh inside
# the uvk1-uvk5v3 Docker image, run from this app's directory. Everything is
# derived from the directory name; only APP_NAME - the human label baked into
# the 64-byte blob header - is per-app. The .app file is named after it (spaces
# stripped), so "Broadcast FM" -> BroadcastFM.app.
set -euo pipefail
APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ...
APP_NAME="Plasma" # <-- the only per-app line
APP_VER="1.0"
APP_API_MIN=1
APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld)
OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM)
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 # drop stale artifacts so discovery is unambiguous
step() { printf '\r 🔨 %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; }
trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
--name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" --screensaver >/dev/null
trap - ERR
BYTES=$(wc -c < "${APP}.bin")
if [ "$BYTES" -gt 4096 ]; then
printf '\r 🚨 %-13s OVERFLOWS 4 KiB (%d B) \n' "$APP_NAME" "$BYTES"; exit 1
fi
printf '\r ✅ %-13s %4d B (%d%% of 4 KiB) -> %s.app \n' \
"$APP_NAME" "$BYTES" "$(( BYTES * 100 / 4096 ))" "$OUT"
+180
View File
@@ -0,0 +1,180 @@
/* Copyright 2026 Armel F4HWN
* https://github.com/armel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Plasma — overlay app. A full-screen demoscene plasma on the 1-bit 128x64 LCD.
* The scalar field sums four scrolled sines: horizontal (x), vertical (y),
* diagonal (x+y) and a radial ripple around a slowly drifting centre. Because
* (x-cx)^2 + (y-cy)^2 is separable, the whole field is still built from per-column
* and per-row tables, so each of the 8192 pixels costs only a handful of adds.
* Rendered either with an ordered 4x4 Bayer dither (stipple) or as sweeping bands.
* Pure compute + framebuffer, no radio — it just works.
*
* Keys: UP/DOWN speed · 1-5 pattern · STAR stipple/bands · F auto-cycle ·
* MENU pause · EXIT quit.
*/
#include <stdint.h>
#include <stdbool.h>
#include "../app_api.h"
#define W 128
#define H 64
static const app_api_t *A;
/* 32 * sin(2*pi*i/256): four of these sum to -128..128. */
static const int8_t SIN[256] = {
0, 1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 9, 9, 10, 11, 12,
12, 13, 14, 14, 15, 16, 16, 17, 18, 18, 19, 20, 20, 21, 21, 22,
23, 23, 24, 24, 25, 25, 26, 26, 27, 27, 27, 28, 28, 29, 29, 29,
30, 30, 30, 30, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32,
32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 30, 30, 30,
30, 29, 29, 29, 28, 28, 27, 27, 27, 26, 26, 25, 25, 24, 24, 23,
23, 22, 21, 21, 20, 20, 19, 18, 18, 17, 16, 16, 15, 14, 14, 13,
12, 12, 11, 10, 9, 9, 8, 7, 6, 5, 5, 4, 3, 2, 2, 1,
0, -1, -2, -2, -3, -4, -5, -5, -6, -7, -8, -9, -9,-10,-11,-12,
-12,-13,-14,-14,-15,-16,-16,-17,-18,-18,-19,-20,-20,-21,-21,-22,
-23,-23,-24,-24,-25,-25,-26,-26,-27,-27,-27,-28,-28,-29,-29,-29,
-30,-30,-30,-30,-31,-31,-31,-31,-31,-32,-32,-32,-32,-32,-32,-32,
-32,-32,-32,-32,-32,-32,-32,-32,-31,-31,-31,-31,-31,-30,-30,-30,
-30,-29,-29,-29,-28,-28,-27,-27,-27,-26,-26,-25,-25,-24,-24,-23,
-23,-22,-21,-21,-20,-20,-19,-18,-18,-17,-16,-16,-15,-14,-14,-13,
-12,-12,-11,-10, -9, -9, -8, -7, -6, -5, -5, -4, -3, -2, -2, -1,
};
/* 4x4 ordered-dither matrix, flattened: idx = (y&3)*4 + (x&3), values 0..15. */
static const uint8_t BAYER[16] = { 0,8,2,10, 12,4,14,6, 3,11,1,9, 15,7,13,5 };
/* Pattern presets: {x scale, y scale, diagonal scale, radial ring shift}. */
#define NVAR 5
static const uint8_t VAR[NVAR][4] = {
{4,4,3,5}, {6,3,5,4}, {3,7,2,6}, {5,5,4,5}, {2,8,6,4},
};
/* Per-frame separable tables. */
static int8_t colA[W]; /* horizontal sine, by column x */
static int8_t rowA[H]; /* vertical sine, by row y */
static int8_t diagA[W + H]; /* diagonal sine, by (x + y) */
static uint16_t sqx[W]; /* (x - cx)^2 for the radial term */
static uint16_t sqy[H]; /* (y - cy)^2 */
static uint8_t prevKey;
__attribute__((section(".text.entry"), used))
void app_main(const app_api_t *api)
{
A = api;
A->backlight_on();
A->status_clear();
uint16_t t1 = 0, t2 = 0, t3 = 0, tr = 0, tc = 0, tc2 = 0;
uint16_t autoCtr = 0;
uint8_t speed = 3;
uint8_t var = 0;
bool bands = true;
bool autoc = false;
bool paused = false;
bool running = true;
prevKey = APP_KEY_INVALID;
while (running) {
uint8_t key = A->get_key();
if (key == APP_KEY_SAVER) {
prevKey = APP_KEY_INVALID;
A->delay_ms(10);
A->backlight_update();
continue;
}
if (key == APP_KEY_WAKE)
key = APP_KEY_INVALID;
if (key != prevKey && key != APP_KEY_INVALID) {
A->backlight_on();
switch (key) {
case APP_KEY_EXIT: running = false; break;
case APP_KEY_UP:
case APP_KEY_DOWN: {
const int8_t direction = A->nav_dir(key);
if (direction > 0 && speed < 8u) speed++;
if (direction < 0 && speed > 1u) speed--;
break;
}
case APP_KEY_MENU: paused = !paused; break;
case APP_KEY_STAR: bands = !bands; break;
case APP_KEY_F: autoc = !autoc; break;
case APP_KEY_1: case APP_KEY_2: case APP_KEY_3:
case APP_KEY_4: case APP_KEY_5:
var = (uint8_t)(key - APP_KEY_1); autoc = false; break;
default: break;
}
}
prevKey = key;
if (!running)
break;
const uint8_t sx = VAR[var][0], sy = VAR[var][1];
const uint8_t sd = VAR[var][2], rsh = VAR[var][3];
/* drifting radial centre (gentle Lissajous), then the separable squares */
const int cx = 64 + SIN[(uint8_t)tc]; /* 32..96 */
const int cy = 32 + (SIN[(uint8_t)tc2] >> 1);/* 16..48 */
for (uint8_t x = 0; x < W; x++) {
colA[x] = SIN[(uint8_t)(x * sx + t1)];
int dx = (int)x - cx; sqx[x] = (uint16_t)(dx * dx);
}
for (uint8_t y = 0; y < H; y++) {
rowA[y] = SIN[(uint8_t)(y * sy + t2)];
int dy = (int)y - cy; sqy[y] = (uint16_t)(dy * dy);
}
for (uint16_t s = 0; s < W + H; s++) diagA[s] = SIN[(uint8_t)(s * sd + t3)];
/* render all 8 pages: page 0 -> status line (top), pages 1..7 -> fb[0..6] */
for (uint8_t x = 0; x < W; x++) {
const int8_t cxv = colA[x];
const uint16_t sxv = sqx[x];
const uint8_t bx = x & 3u;
for (uint8_t p = 0; p < 8u; p++) {
uint8_t byte = 0;
for (uint8_t b = 0; b < 8u; b++) {
const uint8_t y = (uint8_t)(p * 8u + b);
int v = cxv + rowA[y] + diagA[x + y]
+ SIN[(uint8_t)(((sxv + sqy[y]) >> rsh) + tr)];
v += 128; /* 0..256 */
bool on;
if (bands) on = ((v >> 4) & 1u) != 0u; /* sweeping stripes */
else {
int lvl = v >> 4; if (lvl > 15) lvl = 15; /* 0..15 stipple */
on = lvl > (int)BAYER[((y & 3u) << 2) | bx];
}
if (on) byte = (uint8_t)(byte | (1u << b));
}
if (p == 0) A->status_line[x] = byte;
else A->fb[p - 1][x] = byte;
}
}
A->blit_status();
A->blit_full();
if (!paused) {
t1 += speed; t2 += (uint16_t)(speed + 1u); t3 += 1u;
tr += speed; tc += 1u; tc2 += 2u;
if (autoc && ++autoCtr >= 400u) { autoCtr = 0; var = (uint8_t)((var + 1u) % NVAR); }
}
A->backlight_update();
}
}
+30
View File
@@ -0,0 +1,30 @@
/* Tetris overlay app: code, constants, data and BSS share the 4 KiB workspace. */
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);
__app_bss_start = .;
*(.bss .bss.* COMMON)
. = ALIGN(4);
__app_bss_end = .;
} > APP
__app_end = .;
ASSERT(__app_end <= APP_VMA + APP_LENGTH,
"Tetris overlay app overflows the 4 KiB overlay")
/DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.eh_frame*) *(.comment) *(.note.*) }
}
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
set -euo pipefail
APP="$(basename "$PWD")"
APP_NAME="Tetris"
APP_VER="1.0"
APP_API_MIN=1
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; }
command -v "$CC" >/dev/null 2>&1 || { echo "❌ ARM compiler not found: $CC"; exit 1; }
command -v "$OBJCOPY" >/dev/null 2>&1 || { echo "❌ ARM objcopy not found: $OBJCOPY"; exit 1; }
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
step() { printf '\r 🔨 %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; }
trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
--name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" \
--vma "${APP_VMA}" --screensaver >/dev/null
trap - ERR
BYTES=$(wc -c < "${APP}.bin")
if [ "$BYTES" -gt 4096 ]; then
printf '\r 🚨 %-13s OVERFLOWS 4 KiB (%d B) \n' "$APP_NAME" "$BYTES"; exit 1
fi
printf '\r ✅ %-13s %4d B (%d%% of 4 KiB) -> %s.app \n' \
"$APP_NAME" "$BYTES" "$(( BYTES * 100 / 4096 ))" "$OUT"
+555
View File
@@ -0,0 +1,555 @@
/* Copyright 2026 Armel F4HWN
* https://github.com/armel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Tetris — compact overlay game for the 128x64 monochrome LCD.
*
* The 16x16 visible well fills the screen height down the left side; two hidden
* spawn rows sit just above it. Rather than solid walls, the well is framed by
* short corner brackets and a floor line. The narrow right-hand panel shows the
* next piece, score, cleared lines, level and persistent best score. Pieces
* come from a shuffled seven-piece bag; a dotted ghost previews the landing.
*
* Keys: LEFT/RIGHT or 4/6 move, MENU or 2 rotates, 8 soft-drops,
* STAR or 0 hard-drops, F pauses and EXIT quits.
*/
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include "../app_api.h"
#define W 128u
#define H 64u
#define COLS 16
#define VISIBLE_ROWS 16
#define HIDDEN_ROWS 2
#define ROWS (VISIBLE_ROWS + HIDDEN_ROWS)
#define CELL 4
#define WELL_X 3 /* physical x of board column 0 */
#define WELL_Y 0 /* physical y of visible row 0 (full height) */
#define WELL_L 1 /* container left rail (corner brackets) */
#define WELL_R 67 /* container right rail (corner brackets) */
#define WELL_B 63 /* solid floor line, physical y */
#define LABEL_X 72 /* left column shared by every panel label */
#define PREVIEW_CX 108 /* centre of the NEXT-piece preview area */
#define PREVIEW_CY 15
#define TICK_MS 20u
#define CFG_MAGIC 0x5445u
typedef struct {
uint16_t magic;
uint16_t version;
uint32_t best;
} config_t;
static const app_api_t *A;
/* GCC may lower aggregate clears to memset even for this freestanding blob. */
void *memset(void *dst, int value, size_t size)
{
uint8_t *p = dst;
while (size--)
*p++ = (uint8_t)value;
return dst;
}
/* Four 4x4 masks for I, O, T, S, Z, J and L. */
static const uint16_t MASK[7][4] = {
{0x00F0, 0x4444, 0x0F00, 0x2222},
{0x0066, 0x0066, 0x0066, 0x0066},
{0x0072, 0x0262, 0x0270, 0x0232},
{0x0036, 0x0462, 0x0360, 0x0231},
{0x0063, 0x0264, 0x0630, 0x0132},
{0x0071, 0x0226, 0x0470, 0x0322},
{0x0074, 0x0622, 0x0170, 0x0223},
};
static const uint16_t LINE_POINTS[5] = {0, 100, 300, 500, 800};
static const uint32_t DECIMAL_PLACE[6] = {100000u, 10000u, 1000u, 100u, 10u, 1u};
static uint16_t board[ROWS];
static uint8_t bag[7], bagPos;
static uint8_t piece, nextPiece, rotation;
static int8_t pieceX, pieceY;
static uint32_t randomState, score, best;
static uint16_t lines, gravityMs;
static uint8_t level;
static bool paused, gameOver, running, saverPaused, saverActive, bestDirty, newBest;
static uint8_t previousKey;
static uint16_t repeatMs;
static char text[9];
static void render(bool showPiece, uint32_t clearRows, uint8_t effect);
static void new_game(void);
/* -------------------------------------------------------------------------- */
/* RNG and the shuffled 7-piece bag */
/* -------------------------------------------------------------------------- */
static uint32_t random_next(void)
{
randomState = randomState * 1664525u + 1013904223u;
return randomState;
}
static void refill_bag(void)
{
for (uint8_t i = 0; i < 7u; i++)
bag[i] = i;
for (uint8_t i = 6u; i > 0u; i--) {
uint8_t j;
do {
j = (uint8_t)(random_next() >> 29);
} while (j > i);
const uint8_t t = bag[i]; bag[i] = bag[j]; bag[j] = t;
}
bagPos = 0;
}
static uint8_t take_piece(void)
{
if (bagPos >= 7u)
refill_bag();
return bag[bagPos++];
}
/* -------------------------------------------------------------------------- */
/* Framebuffer drawing: cells, pieces, side panel and dialogs */
/* -------------------------------------------------------------------------- */
static void set_pixel(int x, int y, bool on)
{
if ((unsigned)x >= W || (unsigned)y >= H)
return;
uint8_t *p;
if (y < 8)
p = &A->status_line[x];
else
p = &A->fb[(y >> 3) - 1][x];
const uint8_t bit = (uint8_t)(1u << (y & 7));
if (on) *p |= bit; else *p &= (uint8_t)~bit;
}
/* Short straight runs, in physical coordinates so they may cross the
* status-line / framebuffer seam that A->draw_line cannot. */
static void hline(int x0, int x1, int y)
{
for (int x = x0; x <= x1; x++) set_pixel(x, y, true);
}
static void vline(int x, int y0, int y1)
{
for (int y = y0; y <= y1; y++) set_pixel(x, y, true);
}
/* Corner brackets plus a solid floor — the well's frame, no vertical walls. */
static void container(void)
{
hline(WELL_L, WELL_R, WELL_B);
hline(WELL_L, WELL_L + 5, WELL_Y);
hline(WELL_R - 5, WELL_R, WELL_Y);
vline(WELL_L, WELL_Y, WELL_Y + 5);
vline(WELL_R, WELL_Y, WELL_Y + 5);
vline(WELL_L, WELL_B - 5, WELL_B);
vline(WELL_R, WELL_B - 5, WELL_B);
}
static void cell(int gx, int gy, uint8_t style)
{
const int visibleY = gy - HIDDEN_ROWS;
if ((unsigned)gx >= COLS || (unsigned)visibleY >= VISIBLE_ROWS)
return;
const int x = WELL_X + gx * CELL;
const int y = WELL_Y + visibleY * CELL;
if (style == 2u) {
set_pixel(x, y + 1, true);
set_pixel(x + 1, y, true);
set_pixel(x + 2, y + 1, true);
set_pixel(x + 1, y + 2, true);
} else {
const uint8_t size = style == 3u ? CELL : CELL - 1u;
for (uint8_t dy = 0; dy < size; dy++)
for (uint8_t dx = 0; dx < size; dx++)
set_pixel(x + dx, y + dy, true);
}
}
static bool collision(uint8_t type, uint8_t rot, int8_t px, int8_t py)
{
const uint16_t mask = MASK[type][rot & 3u];
for (uint8_t i = 0; i < 16u; i++) {
if (!(mask & (uint16_t)(1u << i)))
continue;
const int x = px + (i & 3u);
const int y = py + (i >> 2);
if (x < 0 || x >= COLS || y >= ROWS)
return true;
if (y >= 0 && (board[y] & (uint16_t)(1u << x)))
return true;
}
return false;
}
static void draw_piece(uint8_t type, uint8_t rot, int8_t px, int8_t py, uint8_t style)
{
const uint16_t mask = MASK[type][rot & 3u];
for (uint8_t i = 0; i < 16u; i++)
if (mask & (uint16_t)(1u << i))
cell(px + (i & 3u), py + (i >> 2), style);
}
static void number(uint32_t value, uint8_t width)
{
const uint8_t first = (uint8_t)(6u - width);
for (uint8_t i = 0; i < width; i++) {
const uint32_t place = DECIMAL_PLACE[first + i];
uint8_t digit = 0;
while (value >= place) {
value -= place;
digit++;
}
text[i] = (char)('0' + digit);
}
text[width] = 0;
}
static void preview(void)
{
const uint16_t mask = MASK[nextPiece][0];
const int ox = nextPiece < 2u ? PREVIEW_CX - 7 : PREVIEW_CX - 5;
const int oy = nextPiece == 0u ? PREVIEW_CY - 5 : PREVIEW_CY - 3;
for (uint8_t i = 0; i < 16u; i++) {
if (!(mask & (uint16_t)(1u << i)))
continue;
const int x = ox + (i & 3u) * CELL;
const int y = oy + (i >> 2) * CELL;
for (uint8_t dy = 0; dy < CELL - 1u; dy++)
for (uint8_t dx = 0; dx < CELL - 1u; dx++)
set_pixel(x + dx, y + dy, true);
}
}
/* One "LABEL value" row of the side panel; y is framebuffer-relative
* (physical y - 8), value right-aligned near the screen edge. */
static void stat(const char *label, uint8_t y, uint32_t value,
uint8_t valueX, uint8_t width)
{
A->print_tiny(label, LABEL_X, y, false, true);
number(value, width);
A->print_tiny(text, valueX, y, false, true);
}
static void panel(void)
{
A->print_tiny("NEXT", LABEL_X, 2, false, true);
preview();
if (gameOver)
A->print_inverse(newBest ? "NEW BEST!" : "GAME OVER", 82, 2, false, true, 118);
else if (paused)
A->print_inverse("PAUSE", 90, 2, false, true, 110);
stat("SCORE", 24, score > 999999u ? 999999u : score, 101, 6);
stat("LINES", 32, lines > 999u ? 999u : lines, 113, 3);
stat("LEVEL", 40, level, 117, 2);
stat("BEST", 48, best > 999999u ? 999999u : best, 101, 6);
}
/* effect 0 is normal, 1..8 is the line-clear sweep, 0xFF flashes a lock. */
static void render(bool showPiece, uint32_t clearRows, uint8_t effect)
{
A->display_clear();
A->status_clear();
A->print_inverse("TETRIS", 74, 0, true, true, 101);
container();
for (uint8_t y = 0; y < ROWS; y++)
for (uint8_t x = 0; x < COLS; x++)
if ((board[y] & (uint16_t)(1u << x)) && (!(clearRows & (1u << y)) ||
(x >= effect && x < COLS - effect)))
cell(x, y, 1u);
if (showPiece && !gameOver) {
int8_t ghostY = pieceY;
while (!collision(piece, rotation, pieceX, ghostY + 1))
ghostY++;
if (ghostY != pieceY)
draw_piece(piece, rotation, pieceX, ghostY, 2u);
draw_piece(piece, rotation, pieceX, pieceY, effect == 0xFFu ? 3u : 1u);
}
panel();
}
/* -------------------------------------------------------------------------- */
/* Board mechanics: scoring, line clears, spawn, lock and moves */
/* -------------------------------------------------------------------------- */
static void update_best(void)
{
if (score > best) {
best = score;
bestDirty = true;
newBest = true;
}
}
static uint8_t clear_lines(void)
{
uint32_t full = 0;
uint8_t count = 0;
for (uint8_t y = 0; y < ROWS; y++) {
if (board[y] == 0xFFFFu) { full |= 1u << y; count++; }
}
if (!count)
return 0;
A->led(true);
for (uint8_t step = 1u; step <= COLS / 2u; step++) {
render(false, full, step);
A->blit_status(); A->blit_full(); A->delay_ms(28);
}
A->led(false);
int8_t dst = ROWS - 1;
for (int8_t src = ROWS - 1; src >= 0; src--) {
if (full & (1u << src))
continue;
if (dst != src)
board[dst] = board[src];
dst--;
}
while (dst >= 0) {
board[dst] = 0;
dst--;
}
return count;
}
static void spawn_piece(void)
{
piece = nextPiece;
nextPiece = take_piece();
rotation = 0;
pieceX = COLS / 2 - 2;
pieceY = HIDDEN_ROWS - 1;
if (collision(piece, rotation, pieceX, pieceY)) {
gameOver = true;
update_best();
}
}
static void lock_piece(void)
{
render(true, 0, 0xFFu);
A->blit_status(); A->blit_full(); A->delay_ms(45);
const uint16_t mask = MASK[piece][rotation];
bool above = false;
for (uint8_t i = 0; i < 16u; i++) {
if (!(mask & (uint16_t)(1u << i)))
continue;
const int x = pieceX + (i & 3u);
const int y = pieceY + (i >> 2);
if (y < HIDDEN_ROWS) above = true;
else board[y] |= (uint16_t)(1u << x);
}
if (above) {
gameOver = true;
update_best();
return;
}
const uint8_t cleared = clear_lines();
lines = (uint16_t)(lines + cleared);
while (level < 15u && lines >= (uint16_t)level * 10u)
level++;
score += (uint32_t)LINE_POINTS[cleared] * level;
update_best();
spawn_piece();
}
static bool move_down(bool manual)
{
if (!collision(piece, rotation, pieceX, pieceY + 1)) {
pieceY++;
if (manual) { score++; update_best(); }
return true;
}
lock_piece();
return false;
}
static void rotate_piece(void)
{
const uint8_t next = (uint8_t)((rotation + 1u) & 3u);
static const int8_t kick[5] = {0, -1, 1, -2, 2};
for (uint8_t i = 0; i < 5u; i++)
if (!collision(piece, next, pieceX + kick[i], pieceY)) {
pieceX += kick[i]; rotation = next; return;
}
if (!collision(piece, next, pieceX, pieceY - 1)) {
pieceY--; rotation = next;
}
}
static void hard_drop(void)
{
uint8_t distance = 0;
while (!collision(piece, rotation, pieceX, pieceY + 1)) {
pieceY++; distance++;
}
score += (uint32_t)distance * 2u;
update_best();
lock_piece();
}
/* -------------------------------------------------------------------------- */
/* Input, screensaver handling and the main loop */
/* -------------------------------------------------------------------------- */
static void key_action(uint8_t key, bool repeat)
{
if (key == APP_KEY_EXIT) { running = false; return; }
if (gameOver) {
if (!repeat && (key == APP_KEY_MENU || key == APP_KEY_STAR || key == APP_KEY_0))
new_game();
return;
}
if (!repeat && key == APP_KEY_F) { paused = !paused; return; }
if (paused)
return;
int8_t direction = 0;
if (key == APP_KEY_4) direction = -1;
else if (key == APP_KEY_6) direction = 1;
else if (key == APP_KEY_UP || key == APP_KEY_DOWN) direction = A->nav_dir(key);
if (direction) {
if (!collision(piece, rotation, pieceX + direction, pieceY))
pieceX += direction;
return;
}
if (key == APP_KEY_8) { move_down(true); return; }
if (repeat)
return;
if (key == APP_KEY_MENU || key == APP_KEY_2) rotate_piece();
else if (key == APP_KEY_STAR || key == APP_KEY_0) hard_drop();
}
static void poll_key(void)
{
uint8_t key = A->get_key();
if (key == APP_KEY_SAVER) {
if (!paused && !gameOver) { paused = true; saverPaused = true; }
saverActive = true;
previousKey = APP_KEY_INVALID;
repeatMs = 0;
return;
}
if (key == APP_KEY_WAKE || key == APP_KEY_PTT) {
if (saverPaused) paused = false;
saverPaused = false;
saverActive = false;
key = APP_KEY_INVALID;
}
if (key == APP_KEY_INVALID) {
previousKey = key;
repeatMs = 0;
return;
}
if (key != previousKey) {
key_action(key, false);
repeatMs = 260u;
} else if (repeatMs > TICK_MS) {
repeatMs -= TICK_MS;
} else {
key_action(key, true);
repeatMs = 80u;
}
previousKey = key;
}
static void new_game(void)
{
for (uint8_t y = 0; y < ROWS; y++)
board[y] = 0;
score = 0;
lines = 0;
level = 1;
gravityMs = 0;
paused = false;
gameOver = false;
saverPaused = false;
saverActive = false;
newBest = false;
bagPos = 7;
nextPiece = take_piece();
spawn_piece();
}
__attribute__((section(".text.entry"), used))
void app_main(const app_api_t *api)
{
A = api;
config_t cfg;
A->cfg_load((uint8_t *)&cfg, sizeof(cfg));
best = cfg.magic == CFG_MAGIC && cfg.version == 1u ? cfg.best : 0u;
bestDirty = false;
randomState = ((uint32_t)A->bk_read(0x67u) << 16) ^ A->rx_freq() ^ best;
if (!randomState) randomState = 1;
previousKey = APP_KEY_INVALID;
repeatMs = 0;
running = true;
A->led(false);
A->backlight_on();
new_game();
while (running) {
poll_key();
if (!running)
break;
if (saverActive) {
A->backlight_update();
A->delay_ms(TICK_MS);
continue;
}
if (!paused && !gameOver) {
const uint16_t interval = level >= 15u ? 100u : (uint16_t)(900u - (level - 1u) * 55u);
gravityMs = (uint16_t)(gravityMs + TICK_MS);
if (gravityMs >= interval) {
gravityMs = 0;
move_down(false);
}
}
render(true, 0, 0);
A->blit_status();
A->blit_full();
A->backlight_update();
A->delay_ms(TICK_MS);
}
update_best();
if (bestDirty) {
cfg.magic = CFG_MAGIC;
cfg.version = 1u;
cfg.best = best;
A->cfg_save((const uint8_t *)&cfg, sizeof(cfg));
}
A->led(false);
}
+12
View File
@@ -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.*) }
}
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail
APP="$(basename "$PWD")"
APP_NAME="Triple VFO"
APP_VER="1.0"
APP_API_MIN=1
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" --api-min "$APP_API_MIN" --vma "${APP_VMA}" --require trivfo --screensaver >/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"
+266
View File
@@ -0,0 +1,266 @@
/* 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 STATUS_PTT_X 54u
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 uint8_t batteryTicks;
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
};
/* Same two 2x6 glyphs and same x=54 slot as UI_DisplayStatus(). */
static const uint8_t fontPttOnePush[12] = {
0x00, 0x3e, 0x41, 0x41, 0x41, 0x3e,
0x00, 0x7f, 0x09, 0x09, 0x09, 0x06
};
static const uint8_t fontPttClassic[12] = {
0x00, 0x3e, 0x41, 0x41, 0x41, 0x22,
0x00, 0x7f, 0x40, 0x40, 0x40, 0x40
};
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; }
/* dbm + 147 is non-negative in this branch. Keep the division unsigned:
* otherwise GCC pulls the ~460-byte signed division helper into the 4 KiB
* overlay even though no signed quotient is required. */
else if(dbm>=-141) s=(uint8_t)((uint16_t)(dbm+147)/6u);
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 drawPttStatus(void){
const uint8_t *glyph=(vi[0].flags&APP_TRIVFO_PTT_ONEPUSH)?fontPttOnePush:fontPttClassic;
for(uint8_t i=0;i<12u;i++) A->status_line[STATUS_PTT_X+i]=glyph[i];
}
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":"?";
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;
if(v->flags&APP_TRIVFO_GUI_CLASSIC){
A->print_normal(mod,2,0,techLine);
if(v->modulation==0u&&v->code_type==1u) A->print_normal("CT",22,0,techLine);
else if(v->modulation==0u&&(v->code_type==2u||v->code_type==3u)) A->print_normal("DC",22,0,techLine);
static const char *const powerShort[7]={"L1","L2","L3","L4","L5","M","H"};
A->print_normal(powerShort[p],42,0,techLine);
if(v->flags&APP_TRIVFO_USER_POWER){
A->fb[techLine][38]=0x3e; A->fb[techLine][39]=0x1c; A->fb[techLine][40]=0x08;
}
if(v->offset_direction==1u) A->print_normal("+",60,0,techLine);
else if(v->offset_direction==2u) A->print_normal("-",60,0,techLine);
if(v->reverse) A->print_normal("R",68,0,techLine);
A->print_normal(v->bandwidth==0u?"W":v->bandwidth==1u?"N":"N+",80,0,techLine);
text[0]='S'; text[1]='Q'; text[2]='L'; text[3]=(char)('0'+(v->squelch%10u)); text[4]='\0';
A->print_normal(text,98,0,techLine);
} else {
A->print_tiny(mod,3,techY,false,true);
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);
drawPttStatus();
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))
A->audio_scope(0u,true);
else {
A->audio_scope(0u,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=batteryTicks=0; loadCfg();
cChannel=A->trivfo_enter(cChannel); A->backlight_on(); draw();
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_SAVER){
state=A->trivfo_tick();
if(state==APP_TRIVFO_RX){
A->backlight_on();
draw();
}
for(uint8_t i=0;i<TICK_MS/10u;i++){
A->delay_ms(10); A->backlight_update();
}
continue;
}
if(key!=APP_KEY_INVALID&&key!=APP_KEY_WAKE) A->backlight_on();
if(key==APP_KEY_WAKE) key=APP_KEY_INVALID;
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||state==APP_TRIVFO_TX_STATE)
A->backlight_on();
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();
}
+8 -8
View File
@@ -15,11 +15,11 @@
* limitations under the License.
*/
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
#include "app/fm.h"
#endif
#include "audio.h"
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
#include "driver/bk1080.h"
#endif
#include "driver/bk4819.h"
@@ -70,7 +70,7 @@ void AUDIO_PlayBeep(BEEP_Type_t Beep)
if (Beep >= ARRAY_SIZE(BEEP_Classic_array))
return;
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
if (gFmRadioMode)
BK1080_Mute(true);
#endif
@@ -112,7 +112,7 @@ void AUDIO_PlayBeep(BEEP_Type_t Beep)
SYSTEM_DelayMs(5);
BK4819_WriteRegister(BK4819_REG_71, ToneConfig);
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
const bool isFmRadio = gFmRadioMode;
if (isFmRadio)
@@ -122,7 +122,7 @@ void AUDIO_PlayBeep(BEEP_Type_t Beep)
if (gEnableSpeaker)
AUDIO_AudioPathOn();
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
if (isFmRadio)
BK1080_Mute(false);
#endif
@@ -275,7 +275,7 @@ void AUDIO_PlaySingleVoice(bool bFlag)
if (FUNCTION_IsRx()) // 1of11
BK4819_SetAF(BK4819_AF_MUTE);
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
if (gFmRadioMode)
BK1080_Mute(true);
#endif
@@ -299,7 +299,7 @@ void AUDIO_PlaySingleVoice(bool bFlag)
if (FUNCTION_IsRx()) // 1of11
RADIO_SetModulation(gRxVfo->Modulation);
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
if (gFmRadioMode)
BK1080_Mute(false);
#endif
@@ -440,7 +440,7 @@ void AUDIO_PlayQueuedVoice(void)
RADIO_SetModulation(gRxVfo->Modulation); // 1of11
}
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
if (gFmRadioMode)
BK1080_Mute(false);
#endif
+10 -9
View File
@@ -22,16 +22,17 @@ const uint8_t gFontPttClassic[2][6] =
{0x00, 0x7f, 0x40, 0x40, 0x40, 0x40},
};
const uint8_t gFontF[8] =
const uint8_t gFontF[9] =
{
0b00111110,
0b01111111,
0b00000000,
0b01110110,
0b01110110,
0b01110110,
0b01110110,
0b01111110,
0b01111111
0b01000001,
0b01110101,
0b01110101,
0b01110101,
0b01111101,
0b01111111,
0b00111110
};
const uint8_t gFontS[6] =
@@ -301,7 +302,7 @@ const uint8_t BITMAP_NOAA[12] =
};
#endif
#ifdef ENABLE_FEAT_F4HWN_FOXHUNT
#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON)
const uint8_t BITMAP_FoxHuntSignal[10] =
{ // point source + the speaker bitmap's two sound waves, oriented horizontally
0b00001000,
+2 -2
View File
@@ -7,7 +7,7 @@
extern const uint8_t gFontPowerSave[2][6];
extern const uint8_t gFontPttOnePush[2][6];
extern const uint8_t gFontPttClassic[2][6];
extern const uint8_t gFontF[8];
extern const uint8_t gFontF[9];
extern const uint8_t gFontS[6];
extern const uint8_t gFontKeyLock[9];
@@ -45,7 +45,7 @@ extern const uint8_t BITMAP_compand[6];
extern const uint8_t BITMAP_NOAA[12];
#ifdef ENABLE_FEAT_F4HWN_FOXHUNT
#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON)
extern const uint8_t BITMAP_FoxHuntSignal[10];
extern const uint8_t BITMAP_FoxHuntSpeaker[10];
extern const uint8_t BITMAP_FoxHuntUp[11];
+13 -4
View File
@@ -71,9 +71,18 @@ void BOARD_GPIO_Init(void)
LL_GPIO_InitTypeDef InitStruct;
LL_GPIO_StructInit(&InitStruct);
InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
InitStruct.Pull = LL_GPIO_PULL_UP;
InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
/* Keep the backlight dark from the first possible GPIO write. Its pin can
* otherwise float while the rest of the board GPIOs are being configured. */
LL_GPIO_ResetOutputPin(GPIOF, LL_GPIO_PIN_8);
InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
InitStruct.Pull = LL_GPIO_PULL_NO;
InitStruct.Pin = LL_GPIO_PIN_8;
LL_GPIO_Init(GPIOF, &InitStruct);
InitStruct.Pull = LL_GPIO_PULL_UP;
// ---------------------
// Input pins
@@ -123,9 +132,8 @@ void BOARD_GPIO_Init(void)
LL_GPIO_Init(GPIOF, &InitStruct);
#endif
// Backlight: PF8
// BK4819 CS: PF9
InitStruct.Pin = LL_GPIO_PIN_9 | LL_GPIO_PIN_8 ;
InitStruct.Pin = LL_GPIO_PIN_9;
LL_GPIO_Init(GPIOF, &InitStruct);
#ifndef ENABLE_SWD
@@ -177,13 +185,14 @@ void BOARD_ADC_GetBatteryInfo(uint16_t *pVoltage, uint16_t *pCurrent)
void BOARD_Init(void)
{
BOARD_GPIO_Init();
/* Blank and initialise the LCD as soon as its GPIOs are available. */
ST7565_Init();
BACKLIGHT_InitHardware();
BOARD_ADC_Init();
#ifdef ENABLE_VOICE
VOICE_Init();
#endif
PY25Q16_Init();
ST7565_Init();
#ifdef ENABLE_FMRADIO
BK1080_Init0();
#endif
-1
View File
@@ -7,7 +7,6 @@
#include "driver/bk4819.h"
#include "string.h"
#include "external/printf/printf.h"
#include "am_fix.h"
static inline void LogUart(const char *const str)
{
+12 -17
View File
@@ -63,6 +63,8 @@ static void BACKLIGHT_SetHardwareBrightness(uint8_t brightness);
// STEPS * STEP_MS ~= total fade duration in ms.
#define BL_STARTUP_FADE_STEPS 40
#define BL_STARTUP_FADE_STEP_MS 12
// First PWM value that produces a non-zero duty cycle with 32 levels.
#define BL_STARTUP_VISIBLE_MIN ((255 + DUTY_CYCLE_LEVELS - 1) / DUTY_CYCLE_LEVELS)
#endif
#ifdef ENABLE_FEAT_F4HWN
@@ -143,14 +145,18 @@ void BACKLIGHT_UpdateTickless(void) {
#ifdef ENABLE_FEAT_F4HWN
// Soft, progressive power-on fade-in.
// Ramps from the current brightness (0 at power-on) to targetBrightness using
// a smoothstep (3x^2 - 2x^3) easing curve. Easing gently at both ends reads as
// a smooth, progressive fade instead of the abrupt linear ramp, and stepping
// through many fine stops keeps it fluid even with the 32-level PWM.
// Starts at the first visible PWM level, then ramps to targetBrightness using
// a smoothstep (3x^2 - 2x^3) easing curve. Avoiding sub-PWM values removes the
// apparent pause between drawing the welcome screen and lighting it.
static void BACKLIGHT_FadeInStartup(void)
{
const int16_t from = currentBrightness; // 0 at power-on
const int16_t span = targetBrightness - from; // ramp amplitude
const int16_t from = targetBrightness < BL_STARTUP_VISIBLE_MIN
? targetBrightness
: BL_STARTUP_VISIBLE_MIN;
const int16_t span = targetBrightness - from;
currentBrightness = from;
BACKLIGHT_SetHardwareBrightness((uint8_t)currentBrightness);
if (span <= 0) {
gUpdateBacklight = false;
@@ -227,18 +233,7 @@ void BACKLIGHT_TurnOn(void)
void BACKLIGHT_TurnOff()
{
#ifdef ENABLE_BLMIN_TMP_OFF
register uint8_t tmp;
if (gEeprom.BACKLIGHT_MIN_STAT == BLMIN_STAT_ON)
tmp = gEeprom.BACKLIGHT_MIN;
else
tmp = 0;
BACKLIGHT_SetBrightness(tmp);
#else
BACKLIGHT_SetBrightness(gEeprom.BACKLIGHT_MIN);
#endif
gBacklightCountdown_500ms = 0;
backlightOn = false;
}
-8
View File
@@ -31,14 +31,6 @@ extern uint8_t gBacklightBrightness;
extern uint16_t gSleepModeCountdown_500ms;
#endif
#ifdef ENABLE_BLMIN_TMP_OFF
typedef enum {
BLMIN_STAT_ON,
BLMIN_STAT_OFF,
BLMIN_STAT_UNKNOWN
} BLMIN_STAT_t;
#endif
void BACKLIGHT_InitHardware();
void BACKLIGHT_UpdateTickless(void);
void BACKLIGHT_TurnOn();
+1 -1
View File
@@ -1095,7 +1095,7 @@ void BK4819_TurnsOffTones_TurnsOnRX(void)
BK4819_REG_30_ENABLE_RX_DSP);
}
#ifdef ENABLE_AIRCOPY
#if defined(ENABLE_AIRCOPY) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
void BK4819_SetupAircopy(void)
{
BK4819_WriteRegister(BK4819_REG_70, 0x00E0); // Enable Tone2, tuning gain 48
+1 -1
View File
@@ -114,7 +114,7 @@ void BK4819_EnterTxMute(void);
void BK4819_ExitTxMute(void);
void BK4819_Sleep(void);
void BK4819_TurnsOffTones_TurnsOnRX(void);
#ifdef ENABLE_AIRCOPY
#if defined(ENABLE_AIRCOPY) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
void BK4819_SetupAircopy(void);
#endif
void BK4819_ResetFSK(void);
+1 -1
View File
@@ -1133,7 +1133,7 @@ void BK4819_TurnsOffTones_TurnsOnRX(void)
BK4819_REG_30_ENABLE_RX_DSP);
}
#ifdef ENABLE_AIRCOPY
#if defined(ENABLE_AIRCOPY) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
void BK4819_SetupAircopy(void)
{
BK4819_WriteRegister(BK4819_REG_70, 0x00C3); // Enable Tone2, tuning gain 48
+21 -16
View File
@@ -39,25 +39,30 @@ void EEPROM_ReadBuffer(uint16_t Address, void *pBuffer, uint8_t Size)
I2C_Stop();
}
void EEPROM_WriteBuffer(uint16_t Address, const void *pBuffer)
void EEPROM_WriteBuffer(uint16_t Address, const void *pBuffer, uint8_t Size)
{
if (pBuffer == NULL || Address >= 0x2000)
if (pBuffer == NULL)
return;
uint8_t buffer[8];
EEPROM_ReadBuffer(Address, buffer, 8);
if (memcmp(pBuffer, buffer, 8) == 0) {
return;
while (Size >= sizeof(buffer) && Address < 0x2000)
{
EEPROM_ReadBuffer(Address, buffer, sizeof(buffer));
if (memcmp(pBuffer, buffer, sizeof(buffer)) != 0)
{
I2C_Start();
I2C_Write(0xA0);
I2C_Write((Address >> 8) & 0xFF);
I2C_Write((Address >> 0) & 0xFF);
I2C_WriteBuffer(pBuffer, sizeof(buffer));
I2C_Stop();
// give the EEPROM time to burn the data in (apparently takes 5ms)
SYSTEM_DelayMs(8);
}
Address += sizeof(buffer);
pBuffer += sizeof(buffer);
Size -= sizeof(buffer);
}
I2C_Start();
I2C_Write(0xA0);
I2C_Write((Address >> 8) & 0xFF);
I2C_Write((Address >> 0) & 0xFF);
I2C_WriteBuffer(pBuffer, 8);
I2C_Stop();
// give the EEPROM time to burn the data in (apparently takes 5ms)
SYSTEM_DelayMs(8);
}
+1 -2
View File
@@ -20,7 +20,6 @@
#include <stdint.h>
void EEPROM_ReadBuffer(uint16_t Address, void *pBuffer, uint8_t Size);
void EEPROM_WriteBuffer(uint16_t Address, const void *pBuffer);
void EEPROM_WriteBuffer(uint16_t Address, const void *pBuffer, uint8_t Size);
#endif
+5 -9
View File
@@ -51,13 +51,12 @@ static const AddrMapping_t ADDR_MAPPINGS[] = {
_MK_MAPPING(0x008000, 0x008000, 0x00886E), // 1024 MR + 7 VFO Attributes * 2 Bytes (ex 0x002000) 0x008000 -> 0x00880E
// List name * 4 Bytes 0x00880E -> 0x00886E
_MK_MAPPING(0x009000, 0x009000, 0x0090E7), // 14 VFO * 16 Bytes = 0x9000 -> 0x90E0 (the old
_MK_MAPPING(0x009000, 0x009000, 0x0090E8), // 14 VFO * 16 Bytes = 0x9000 -> 0x90E0 (the old
// 0x90D6 bound was 10 B short: it truncated the
// 470 MHz VFO1 record at 0x90D0 -> 0x90DF)
// Fox Hunt settings * 7 Bytes 0x90E0 -> 0x90E7
// (ENABLE_FEAT_F4HWN_FOXHUNT, written directly by
// app/foxhunt.c; concatenated here so aircopy clones
// them together with the VFOs)
// Fox Hunt / Beacon settings * 8 Bytes 0x90E0 -> 0x90E8
// (written directly by app/foxhunt.c; concatenated
// here so aircopy clones them with the VFOs)
_MK_MAPPING(0x00A000, 0x00A000, 0x00A170), // Settings * 16 Bytes (ex 0x004000) 0x00A000 -> 0x00A010
// Settings * 16 Bytes (ex 0x005000) 0x00A010 -> 0x00A020
@@ -110,11 +109,8 @@ void EEPROM_ReadBuffer(uint16_t Address, void *pBuffer, uint8_t Size)
}
}
void EEPROM_WriteBuffer(uint16_t Address, const void *pBuffer)
void EEPROM_WriteBuffer(uint16_t Address, const void *pBuffer, uint8_t Size)
{
// Write 8 bytes!!
uint16_t Size = 8;
while (Size)
{
uint32_t PY_Addr;
File diff suppressed because it is too large. Load diff
+252
View File
@@ -0,0 +1,252 @@
/* Copyright 2026 Armel F4HWN
* https://github.com/armel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Multiboot flash programmer.
*
* M1 (validated): brick-critical core - reprogram the internal application flash
* from an image held in the external SPI flash, running from RAM.
*
* M2 (this file): slot format + integrity validation. Each slot starts with a
* header (magic, size, CRC32, name, version); a restore validates the header and
* the image CRC32 *before* erasing anything, so an incompatible or corrupt image
* can never brick the radio. There is a single firmware for both the K1 and the
* K5v3 (the keypad difference is handled at runtime by the hidden SetNav menu),
* so no per-model guard is needed.
*/
#ifndef DRIVER_MB_FLASH_H
#define DRIVER_MB_FLASH_H
#include <stdint.h>
#include <stdbool.h>
/* Internal flash application region (see Core/py32f071xb.ld:
* FLASH origin 0x08002800, length 118 KiB). 0x08002800 is 256-byte aligned, so
* the whole region can be page-erased (256 B granularity) without touching the
* factory bootloader that lives just below it. */
#define MB_INT_APP_BASE 0x08002800u
#define MB_INT_APP_SIZE 0x0001D800u /* 118 KiB */
/* External SPI flash slot layout.
* Each 128 KiB slot = one header sector (4 KiB) followed by the image.
* Slot 0 is the firmware-managed BACKUP of the normally-flashed firmware
* (written by MB_BackupInternalToSlot0, protected from host writes); slots
* 1..4 are the user slots managed from UV Studio. */
#define MB_SLOT_STRIDE 0x00020000u /* 128 KiB per slot */
#define MB_SLOT_IMG_OFFSET 0x00001000u /* image starts after header sector */
#define MB_SLOT0_EXT_BASE 0x00020000u /* slot 0 (backup) header base */
#define MB_SLOT_COUNT 5u /* slot 0 backup + slots 1..4 */
#define MB_SLOT_BACKUP 0u /* slot 0 = firmware base backup */
/* Slot header (stored at the slot base, first 4 KiB sector). 64 bytes. */
#define MB_SLOT_MAGIC 0x31424D46u /* "FMB1" */
#define MB_HDR_VERSION 1u
#define MB_FLAG_COMMITTED (1u << 0) /* image written and verified */
#define MB_NAME_LEN 16
#define MB_VERSION_LEN 16
typedef struct __attribute__((packed)) {
uint32_t magic; /* MB_SLOT_MAGIC */
uint16_t hdr_version; /* MB_HDR_VERSION */
uint16_t flags; /* MB_FLAG_COMMITTED, ... */
uint32_t image_size; /* bytes, <= MB_INT_APP_SIZE */
uint32_t image_crc32; /* CRC-32 (zlib) over image_size B */
char name[MB_NAME_LEN]; /* human-readable, NUL-terminated */
char fw_version[MB_VERSION_LEN]; /* firmware version string */
uint8_t reserved[16]; /* pad to 64 bytes, future use */
} mb_slot_header_t;
/* Multiboot operation result (a successful restore resets before returning). */
enum {
MB_OK = 0,
MB_ERR_MAGIC, /* no/invalid slot header */
MB_ERR_VERSION, /* header format too new */
MB_ERR_NOT_COMMITTED,/* image not marked complete */
MB_ERR_SIZE, /* image_size out of range */
MB_ERR_CRC, /* image CRC32 mismatch */
MB_ERR_SPI, /* external flash read/write timed out*/
MB_ERR_SLOT, /* slot or bank index out of range */
MB_ERR_AUTH, /* write refused: timestamp mismatch */
MB_ERR_RAM_LOAD /* restore stub RAM copy mismatch */
};
/* CRC-32 (zlib) over a resident memory buffer. Multiboot and overlay apps
* share this implementation; keeping it here avoids carrying two identical
* bitwise CRC loops in the MCU flash. */
uint32_t MB_Crc32Bytes(const uint8_t *data, uint32_t len);
/* Multi-slot API used by the boot selector. Validation always covers the full
* image CRC before restore. progress_line may point to a 128-byte LCD page; the
* RAM copier then fills it while reflashing. Pass NULL to disable LCD updates.
* With ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY, the copier is loaded over the
* PY25Q16 sector cache only after validation and immediately before this call. */
uint8_t MB_ValidateSlot(uint8_t slot, mb_slot_header_t *out_header, uint32_t *out_crc);
uint8_t MB_RestoreSlot(uint8_t slot, uint8_t *progress_line);
/*
* Host-tool slot management (M4, "Firmware Slots" in UV Studio). Everything is
* bounds-checked (slot < MB_SLOT_COUNT, offset+len <= MB_SLOT_STRIDE) and writes
* touch the EXTERNAL flash only, so none of this is brick-critical: a bad slot is
* simply refused at restore by the CRC validation above.
*
* - MB_SlotInfo reads the 64-byte header only (fast, no CRC recompute) and
* returns MB_OK / MB_ERR_* describing the header state.
* - MB_SlotErase erases the whole 128 KiB slot region (header + image).
* - MB_SlotWrite programs `len` bytes at slot_base+offset. The slot MUST have
* been erased first (NOR flash only clears 1->0 bits); the host
* writes the image, then the COMMITTED header last.
* Use MB_ValidateSlot afterwards to confirm the full image CRC.
*/
uint8_t MB_SlotInfo(uint8_t slot, mb_slot_header_t *out_header);
uint8_t MB_SlotErase(uint8_t slot);
uint8_t MB_SlotWrite(uint8_t slot, uint32_t offset, const uint8_t *data, uint32_t len);
/* -------------------------------------------------------------------------- */
/* Config banks (one per slot by default, switchable via SetCfg). */
/* */
/* Each firmware slot gets its own config bank by default (memory channels, */
/* names, VFOs, settings) so switching firmware does not implicitly share or */
/* clobber settings between editions. SetCfg deliberately lets the user pick */
/* another bank after confirmation; compatibility is then the user's concern. */
/* The banking itself is a single address offset applied in the flash driver */
/* (PY25Q16_SetBankBase): everything below PY25Q16_BANK_SHARED_FROM */
/* (0x010000, the calibration boundary) is per-bank, at/above stays shared. */
/* Restoring slot N selects bank N initially, but slot and bank are tracked */
/* independently afterwards. Bank 0 reuses the historical config region at */
/* 0x000000 - no migration. */
/* */
/* External SPI flash (PY25Q16, 2 MiB) map: */
/* 0x000000 bank 0 config (channels/settings) ] per-bank */
/* 0x010000 calibration (512 B) ] shared */
/* 0x011000 boot logo (4 KiB) ] shared */
/* 0x020000 slot 0 firmware (BACKUP, 128 KiB) ] firmware-managed */
/* 0x040000 slot 1 firmware (128 KiB) ] */
/* 0x060000 slot 2 firmware ] user (UV Studio) */
/* 0x080000 slot 3 firmware ] */
/* 0x0A0000 slot 4 firmware ] */
/* 0x0C0000 bank 1 config (64 KiB) ] */
/* 0x0D0000 bank 2 config (64 KiB) ] per-bank */
/* 0x0E0000 bank 3 config (64 KiB) ] */
/* 0x0F0000 bank 4 config (64 KiB) ] */
/* 0x100000 multiboot state A (4 KiB marker) ] shared */
/* 0x101000 multiboot state B (4 KiB marker) ] redundant */
/* 0x102000 -- free ~888 KiB -- */
/* 0x1E0000 RX/TX log (32 KiB) ] shared */
/* */
/* Banks are 64 KiB for headroom; the live config footprint is ~44 KiB (max */
/* physical config address 0x00A170). Keep the config banks past the last */
/* slot if MB_SLOT_COUNT ever grows. */
#define MB_BANK_COUNT MB_SLOT_COUNT /* selectable config banks (0..4) */
#define MB_BANK_SIZE 0x00010000u /* 64 KiB per config bank */
#define MB_BANK1_EXT_BASE 0x000C0000u /* banks 1..4, right after slot 4 */
#define MB_STATE_A_BASE 0x00100000u /* redundant marker sector A */
#define MB_STATE_B_BASE 0x00101000u /* redundant marker sector B */
/* Active-state marker: two alternating external-flash sectors, never banked,
* outside the EEPROM logical map. A new record is verified in the inactive
* sector before it supersedes the previous one, so a power loss always leaves
* at least one usable state. The expected firmware identity is stored here as
* well: boot resolution never depends on the slot header remaining readable. */
#define MB_STATE_LEGACY_MAGIC 0x31504D46u /* "FMP1" (single 8-byte record) */
#define MB_STATE_V2_MAGIC 0x32504D46u /* "FMP2" (slot == config bank) */
#define MB_STATE_MAGIC 0x33504D46u /* "FMP3" (slot + bank separated) */
typedef struct __attribute__((packed)) {
uint32_t magic; /* MB_STATE_MAGIC */
uint32_t generation; /* monotonically increasing record version */
uint32_t image_size; /* expected internal image size */
uint32_t image_crc32; /* expected internal image CRC-32 */
uint8_t firmware_slot; /* exact source slot of the running image */
uint8_t slot_inv; /* ~firmware_slot, quick integrity check */
uint8_t config_bank; /* active config bank 0..MB_BANK_COUNT-1 */
uint8_t bank_inv; /* ~config_bank, quick integrity check */
uint32_t state_crc32; /* CRC-32 over all preceding fields */
} mb_state_t;
/* External-flash base of a config bank. Bank 0 -> 0 (historical
* region, identity map); banks 1..N -> past the slots. Out-of-range -> 0. */
uint32_t MB_BankBase(uint8_t bank);
/* Result of reading the active-state marker. A boot must treat these very
* differently: MISSING = fresh radio (adopting the running firmware as Main is
* fine); IO / CORRUPT = uncertain (must NOT overwrite Main). */
typedef enum {
MB_MARK_VALID = 0, /* valid FMP2/FMP3; *state normalized to FMP3 */
MB_MARK_LEGACY, /* valid FMP1 index; identity not stored */
MB_MARK_MISSING, /* read OK but both sectors erased */
MB_MARK_CORRUPT, /* read OK but magic/integrity bad */
MB_MARK_IO, /* could not be read (SPI error) */
} mb_mark_status_t;
/* Read the newest valid active-state marker, distinguishing the states above. */
mb_mark_status_t MB_ReadActiveState(mb_state_t *state);
/* Write the active-state marker into the inactive redundant sector and read it
* back to confirm it landed. The previous valid record is kept intact. The slot
* header supplies the expected internal firmware identity. Use this right before
* reflashing to `slot`: both the exact firmware slot and the initial config
* bank become `slot`. */
uint8_t MB_SetActiveSlot(uint8_t slot);
/* Switch ONLY the active settings bank, keeping the firmware that is running.
* Unlike MB_SetActiveSlot (which records that slot's image as the
* expected identity, for the imminent reflash to that slot), this preserves the
* running firmware's identity taken from the current marker and changes only the
* config bank. The next boot therefore maps a different bank with no
* reflash and is never mistaken for an out-of-multiboot firmware change (which
* would self-backup + reset to bank 0). Requires a currently valid marker -
* what every normal boot leaves behind - else MB_ERR_SPI / MB_ERR_MAGIC. The
* caller resets the MCU afterwards; the new bank takes effect at the next boot. */
uint8_t MB_SetActiveBank(uint8_t bank);
/* Erase a whole config bank (host "Reset config" for a user slot):
* the next boot using that bank reads 0xFF and re-seeds factory defaults. Bank 0
* (the base) is refused - reset it by factory-resetting the base firmware.
* External flash only, never brick-critical. */
uint8_t MB_BankErase(uint8_t bank);
/* -------------------------------------------------------------------------- */
/* Slot 0 self-backup (base firmware). */
/* -------------------------------------------------------------------------- */
/* Progress callback for the (slow) slot-0 self-backup; may be NULL. */
typedef void (*mb_progress_fn)(uint32_t done, uint32_t total);
/* Does the running internal firmware carry the DECLARED identity of `slot`
* (CRC32 over image_size == the slot header's image_crc32)? This checks the
* header's claim, not the stored image itself (that is validated once, right
* after a backup). IO is reported separately so a transient SPI error is never
* mistaken for a mismatch - which would wrongly trigger a self-backup over Main. */
typedef enum {
MB_FW_MATCH = 0, /* internal carries this slot's declared identity */
MB_FW_MISMATCH, /* reliably not this slot (or slot has no header) */
MB_FW_IO, /* slot header unreadable: uncertain */
} mb_fw_match_t;
mb_fw_match_t MB_InternalMatchesSlot(uint8_t slot);
/* Compare internal flash with the identity stored in a validated marker. */
bool MB_InternalMatchesState(const mb_state_t *state);
/* Back up the running internal firmware into slot 0 (erase + full image +
* COMMITTED header, name=edition, version) and validate the stored image by a
* full external CRC read-back. External flash only, never brick-critical; a
* failure (SPI or CRC) leaves the slot uncommitted and does not advance the
* marker, so boot resolution retries it. progress may be NULL. */
uint8_t MB_BackupInternalToSlot0(mb_progress_fn progress);
#endif /* DRIVER_MB_FLASH_H */
+83 -2
View File
@@ -27,6 +27,10 @@
#include "external/printf/printf.h"
#include "misc.h"
/* MBMARK was an on-screen SPI trace used while bringing up multiboot (M1/M2).
* The tracer is gone; keep the call sites as no-ops. */
#define MBMARK(s)
// #define DEBUG
#define SPIx SPI2
@@ -39,10 +43,42 @@
#define PAGE_SIZE 0x100
static uint32_t SectorCacheAddr = 0x1000000;
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY
/* The restore-only RAM stub is copied over this cache immediately before it
* erases internal flash. A reset always follows, so the cache is never needed
* again after the overlay becomes active. */
static uint8_t SectorCache[SECTOR_SIZE]
__attribute__((section(".bss.mb_workspace"), aligned(4), used));
#else
static uint8_t SectorCache[SECTOR_SIZE];
#endif
static uint8_t BlackHole[4] __attribute__((aligned(4)));
static volatile bool TC_Flag;
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
/* Active settings-bank base (see py25q16.h). 0 = bank 0 / historical
* config region, i.e. an identity mapping. */
static uint32_t BankBase = 0;
void PY25Q16_SetBankBase(uint32_t Base)
{
BankBase = Base;
}
/* Redirect config-region accesses (addr < boundary) into the active bank.
* Calibration/logo/slots/marker (addr >= boundary) are returned unchanged.
* BankBase is sector-aligned, so alignment done by callers is preserved. */
static inline uint32_t BankMap(uint32_t Address)
{
return (Address < PY25Q16_BANK_SHARED_FROM) ? (Address + BankBase) : Address;
}
#else
static inline uint32_t BankMap(uint32_t Address)
{
return Address;
}
#endif
static inline void CS_Assert()
{
GPIO_ResetOutputPin(CS_PIN);
@@ -218,6 +254,7 @@ static void WriteEnable();
static void SectorErase(uint32_t Addr);
static void SectorProgram(uint32_t Addr, const uint8_t *Buf, uint32_t Size);
static void PageProgram(uint32_t Addr, const uint8_t *Buf, uint32_t Size);
static void ReadBufferRaw(uint32_t Address, void *pBuffer, uint32_t Size);
void PY25Q16_Init()
{
@@ -225,19 +262,23 @@ void PY25Q16_Init()
SPI_Init();
}
void PY25Q16_ReadBuffer(uint32_t Address, void *pBuffer, uint32_t Size)
static void ReadBufferRaw(uint32_t Address, void *pBuffer, uint32_t Size)
{
MBMARK("RD cmd"); // about to assert CS + send read command
CS_Assert();
SPI_WriteByte(0x03); // Send read command
MBMARK("RD addr"); // command sent, about to send address
WriteAddr(Address); // Send address (3 bytes)
MBMARK("RD flush"); // address sent, about to flush RX FIFO
// CRITICAL: Flush RX FIFO before DMA to remove residual data
while (LL_SPI_RX_FIFO_EMPTY != LL_SPI_GetRxFIFOLevel(SPIx))
{
LL_SPI_ReceiveData8(SPIx); // Read and discard
}
MBMARK("RD data"); // FIFO flushed, about to read the data
if (Size >= 16) {
SPI_ReadBuf((uint8_t *)pBuffer, Size);
} else {
@@ -247,11 +288,31 @@ void PY25Q16_ReadBuffer(uint32_t Address, void *pBuffer, uint32_t Size)
}
}
MBMARK("RD end"); // data read, about to release CS
CS_Release();
}
void PY25Q16_ReadBuffer(uint32_t Address, void *pBuffer, uint32_t Size)
{
ReadBufferRaw(BankMap(Address), pBuffer, Size);
}
// Like PY25Q16_ReadBuffer, but waits for the flash to be idle first (WIP=0),
// exactly as PY25Q16_WriteBuffer does before its internal reads. A standalone
// read issued while the chip is still busy from a prior program/erase never
// returns the expected data.
void PY25Q16_ReadBufferSafe(uint32_t Address, void *pBuffer, uint32_t Size)
{
MBMARK("SAFE wip"); // about to WaitWIP()
WaitWIP();
MBMARK("SAFE rb"); // WaitWIP done, about to ReadBuffer
PY25Q16_ReadBuffer(Address, pBuffer, Size);
}
void PY25Q16_WriteBuffer(uint32_t Address, const void *pBuffer, uint32_t Size, bool Append)
{
Address = BankMap(Address); /* map once; internal reads use *Raw below */
#ifdef DEBUG
printf("spi flash write: %06x %ld %d\n", Address, Size, Append);
#endif
@@ -277,7 +338,9 @@ void PY25Q16_WriteBuffer(uint32_t Address, const void *pBuffer, uint32_t Size, b
if (SecAddr != SectorCacheAddr)
{
PY25Q16_ReadBuffer(SecAddr, SectorCache, SECTOR_SIZE);
/* SecAddr is already in mapped space (Address was mapped above), so
* read raw to avoid mapping a second time. */
ReadBufferRaw(SecAddr, SectorCache, SECTOR_SIZE);
SectorCacheAddr = SecAddr;
}
@@ -337,6 +400,7 @@ void PY25Q16_WriteBuffer(uint32_t Address, const void *pBuffer, uint32_t Size, b
void PY25Q16_SectorErase(uint32_t Address)
{
Address = BankMap(Address);
Address -= (Address % SECTOR_SIZE);
SectorErase(Address);
if (SectorCacheAddr == Address)
@@ -345,6 +409,23 @@ void PY25Q16_SectorErase(uint32_t Address)
}
}
void PY25Q16_InvalidateCache(void)
{
/* Same "no sector cached" sentinel as the initial value: the next write
* re-reads its sector from flash instead of trusting SectorCache. */
SectorCacheAddr = 0x1000000;
}
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_APPS
/* Expose the 4 KiB sector cache as the overlay-app workspace. It lives in
* .bss.mb_workspace (the overlay VMA), so an app blob linked there runs in
* place once copied in. The caller InvalidateCache()s around its use. */
uint8_t *PY25Q16_OverlayBuffer(void)
{
return SectorCache;
}
#endif
static inline void WriteAddr(uint32_t Addr)
{
SPI_WriteByte(0xff & (Addr >> 16));
+38
View File
@@ -22,7 +22,45 @@
void PY25Q16_Init();
void PY25Q16_ReadBuffer(uint32_t Address, void *pBuffer, uint32_t Size);
void PY25Q16_ReadBufferSafe(uint32_t Address, void *pBuffer, uint32_t Size);
void PY25Q16_WriteBuffer(uint32_t Address, const void *pBuffer, uint32_t Size, bool Append);
void PY25Q16_SectorErase(uint32_t Address);
/* Drop the internal single-sector write cache. Call after erasing/programming
* flash behind the driver's back (e.g. the raw multiboot slot/bank ops) so a
* later write cannot skip or resurrect data based on a stale cached sector. It
* is also called before multiboot reuses the cache storage as a RAM overlay. */
void PY25Q16_InvalidateCache(void);
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_APPS
/* The 4 KiB sector cache, reused as the overlay-app execution workspace. */
uint8_t *PY25Q16_OverlayBuffer(void);
#endif
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
/*
* Multiboot per-bank config banking.
*
* Each firmware slot gets its own config bank by default (memory channels,
* names, VFOs, settings), though SetCfg can point the running firmware at a
* different bank. A non-zero bank base transparently shifts every flash access
* BELOW PY25Q16_BANK_SHARED_FROM into the active bank; calibration, boot logo,
* firmware slots and the multiboot marker all live at/above that boundary and
* stay shared across every bank.
*
* This is the single choke point: both the EEPROM emulation (eeprom_compat.c)
* and the firmware's direct config reads/writes (settings.c) end up here, so
* one offset covers them all - no per-call-site patching.
*
* Set once at boot, before any settings read, from
* PY25Q16_SetBankBase(MB_BankBase(MB_BootResolveState()));
* and never changed again during a session (a slot restore or SetCfg selection
* records the next bank and resets first), so the banking itself never needs a
* cache flush. Raw bank erases behind the driver explicitly call
* PY25Q16_InvalidateCache().
*/
#define PY25Q16_BANK_SHARED_FROM 0x00010000u /* calibration boundary (see flash map) */
void PY25Q16_SetBankBase(uint32_t Base);
#endif
#endif
+15 -6
View File
@@ -204,9 +204,11 @@ void ST7565_DrawLine(const unsigned int Column, const unsigned int Line, const u
void ST7565_FillScreen(uint8_t value)
{
CS_Assert();
for (unsigned i = 0; i < 8; i++) {
// TODO: This is wrong
DrawLine(0, i, NULL, value);
for (uint8_t line = 0; line < 8u; line++) {
ST7565_SelectColumnAndLine(4u, line);
A0_Set();
for (uint8_t column = 0; column < LCD_WIDTH; column++)
SPI_WriteByte(value);
}
CS_Release();
}
@@ -333,6 +335,10 @@ void ST7565_Init(void)
SPI_Init();
ST7565_HardwareReset();
CS_Assert();
/* Hide the controller RAM immediately. On K1 there is no usable hardware
* reset pin, so its power-on contents can otherwise briefly reach the LCD. */
ST7565_WriteByte(ST7565_CMD_DISPLAY_ON_OFF | 0);
ST7565_WriteByte(ST7565_CMD_SOFTWARE_RESET); // software reset
SYSTEM_DelayMs(120);
@@ -354,13 +360,16 @@ void ST7565_Init(void)
ST7565_WriteByte(ST7565_CMD_POWER_CIRCUIT | 0b111); // VB=1 VR=1 VF=1
SYSTEM_DelayMs(40);
ST7565_WriteByte(ST7565_CMD_SET_START_LINE | 0); // line 0
ST7565_WriteByte(ST7565_CMD_DISPLAY_ON_OFF | 1); // D=1
ST7565_WriteByte(ST7565_CMD_SET_START_LINE | 0); // line 0
CS_Release();
/* Clear all eight LCD RAM pages while the display is still disabled. */
ST7565_FillScreen(0x00);
CS_Assert();
ST7565_WriteByte(ST7565_CMD_DISPLAY_ON_OFF | 1); // D=1
CS_Release();
}
#ifdef ENABLE_FEAT_F4HWN_SLEEP
+8 -42
View File
@@ -17,13 +17,13 @@
#include <string.h>
#include "app/dtmf.h"
#if defined(ENABLE_FMRADIO)
#if defined(ENABLE_FMRADIO_EMBEDDED)
#include "app/fm.h"
#endif
#include "audio.h"
#include "dcs.h"
#include "driver/backlight.h"
#if defined(ENABLE_FMRADIO)
#if defined(ENABLE_FMRADIO_EMBEDDED)
#include "driver/bk1080.h"
#endif
#include "driver/bk4819.h"
@@ -103,7 +103,7 @@ void FUNCTION_Foreground(const FUNCTION_Type_t PreviousFunction)
return;
}
#if defined(ENABLE_FMRADIO)
#if defined(ENABLE_FMRADIO_EMBEDDED)
if (gFmRadioMode)
gFM_RestoreCountdown_10ms = fm_restore_countdown_10ms;
#endif
@@ -156,34 +156,11 @@ void FUNCTION_Transmit()
gDTMF_RX_live_timeout = 0;
DTMF_clear_input_box_memory();
#if defined(ENABLE_FMRADIO)
#if defined(ENABLE_FMRADIO_EMBEDDED)
if (gFmRadioMode)
BK1080_Init0();
#endif
#ifdef ENABLE_ALARM
if (gAlarmState == ALARM_STATE_SITE_ALARM)
{
GUI_DisplayScreen();
AUDIO_AudioPathOff();
SYSTEM_DelayMs(20);
BK4819_PlayTone(500, 0);
SYSTEM_DelayMs(2);
AUDIO_AudioPathOn();
gEnableSpeaker = true;
SYSTEM_DelayMs(60);
BK4819_ExitTxMute();
gAlarmToneCounter = 0;
return;
}
#endif
gUpdateStatus = true;
GUI_DisplayScreen();
@@ -198,20 +175,9 @@ void FUNCTION_Transmit()
if (gCurrentVfo->DTMF_PTT_ID_TX_MODE == PTT_ID_APOLLO)
BK4819_PlaySingleTone(2525, 250, 0, gEeprom.DTMF_SIDE_TONE);
#if defined(ENABLE_ALARM) || defined(ENABLE_TX1750)
if (gAlarmState != ALARM_STATE_OFF) {
#ifdef ENABLE_TX1750
if (gAlarmState == ALARM_STATE_TX1750)
BK4819_TransmitTone(true, 1750);
#endif
#ifdef ENABLE_ALARM
if (gAlarmState == ALARM_STATE_TXALARM)
BK4819_TransmitTone(true, 500);
gAlarmToneCounter = 0;
#endif
#ifdef ENABLE_TX1750
if (gTx1750Active) {
BK4819_TransmitTone(true, 1750);
SYSTEM_DelayMs(2);
AUDIO_AudioPathOn();
gEnableSpeaker = true;
@@ -318,7 +284,7 @@ void FUNCTION_Select(FUNCTION_Type_t Function)
gBatterySaveCountdown_10ms = battery_save_count_10ms;
gSchedulePowerSave = false;
#if defined(ENABLE_FMRADIO)
#if defined(ENABLE_FMRADIO_EMBEDDED)
if(Function != FUNCTION_INCOMING)
gFM_RestoreCountdown_10ms = 0;
#endif
+11
View File
@@ -17,6 +17,7 @@
#include <assert.h>
#include "battery.h"
#include "board.h"
#include "driver/backlight.h"
#include "driver/st7565.h"
#include "functions.h"
@@ -129,6 +130,16 @@ unsigned int BATTERY_VoltsToPercent(const unsigned int voltage_10mV)
return 0;
}
#if defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) || defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON)
void BATTERY_Sample(const bool bDisplayBatteryLevel)
{
BOARD_ADC_GetBatteryInfo(&gBatteryVoltages[gBatteryVoltageIndex++], &gBatteryCurrent);
if (gBatteryVoltageIndex > 3u)
gBatteryVoltageIndex = 0u;
BATTERY_GetReadings(bDisplayBatteryLevel);
}
#endif
void BATTERY_GetReadings(const bool bDisplayBatteryLevel)
{
const uint8_t PreviousBatteryLevel = gBatteryDisplayLevel;
+3
View File
@@ -45,6 +45,9 @@ typedef enum {
unsigned int BATTERY_VoltsToPercent(unsigned int voltage_10mV);
#if defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) || defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON)
void BATTERY_Sample(bool bDisplayBatteryLevel);
#endif
void BATTERY_GetReadings(bool bDisplayBatteryLevel);
void BATTERY_TimeSlice500ms(void);
+27 -2
View File
@@ -29,20 +29,40 @@
#include "settings.h"
#include "ui/menu.h"
#include "ui/ui.h"
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
#include "ui/multiboot.h"
#endif
BOOT_Mode_t BOOT_GetMode(void)
{
unsigned int i;
KEY_Code_t Keys[2];
bool PttPressed[2];
/* Poll the keypad even without PTT: holding MENU alone enters multiboot.
* The two samples keep the same debounce rule as the legacy boot modes. */
for (i = 0; i < 2; i++)
{
if (!GPIO_IsPttPressed())
return BOOT_MODE_NORMAL; // PTT not pressed
PttPressed[i] = GPIO_IsPttPressed();
Keys[i] = KEYBOARD_Poll();
SYSTEM_DelayMs(20);
}
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
if (!PttPressed[0] && !PttPressed[1] &&
Keys[0] == KEY_MENU && Keys[1] == KEY_MENU)
{
gKeyReading0 = Keys[0];
gKeyReading1 = Keys[0];
gDebounceCounter = 2;
return BOOT_MODE_MULTIBOOT;
}
#endif
/* All historical special modes still require PTT for both samples. */
if (!PttPressed[0] || !PttPressed[1])
return BOOT_MODE_NORMAL;
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
if (Keys[0] == (10 + gEeprom.SET_KEY))
{
@@ -125,5 +145,10 @@ void BOOT_ProcessMode(BOOT_Mode_t Mode)
}
#endif
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
else if (Mode == BOOT_MODE_MULTIBOOT)
UI_MultibootSelector();
#endif
GUI_SelectNextDisplay(display);
}
+4 -1
View File
@@ -28,7 +28,10 @@ enum BOOT_Mode_t
BOOT_MODE_RESCUE_OPS,
#endif
#ifdef ENABLE_AIRCOPY
BOOT_MODE_AIRCOPY
BOOT_MODE_AIRCOPY,
#endif
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
BOOT_MODE_MULTIBOOT,
#endif
};
+21 -1
View File
@@ -18,7 +18,12 @@
#include "driver/st7565.h"
#include "k5viewer.h"
#include "misc.h"
#ifdef ENABLE_UART
#include "driver/uart.h"
#endif
#ifdef ENABLE_USB
#include "driver/vcp.h"
#endif
#include "driver/keyboard.h"
#include "driver/bk4819.h"
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
@@ -67,26 +72,41 @@ void K5VIEWER_ParseInput(void)
if (K5VIEWER_IsLocked())
return;
#ifdef ENABLE_UART
if (UART_IsCableConnected()) {
keepAlive = 15;
hasConnectionPing = true;
gUSB_K5ViewerEnabled = false;
return;
}
else if (VCP_K5ViewerPing()) {
#endif
#ifdef ENABLE_USB
if (VCP_K5ViewerPing()) {
keepAlive = 15;
hasConnectionPing = true;
gUSB_K5ViewerEnabled = true;
}
#endif
}
static void K5VIEWER_Send(const uint8_t *buf, uint16_t len)
{
#if defined(ENABLE_UART) && defined(ENABLE_USB)
if (gUSB_K5ViewerEnabled) {
cdc_acm_data_send_with_dtr(buf, len);
} else {
UART_Send(buf, len);
}
#elif defined(ENABLE_USB)
cdc_acm_data_send_with_dtr(buf, len);
#elif defined(ENABLE_UART)
UART_Send(buf, len);
#else
(void)buf;
(void)len;
#endif
}
enum {
+22 -9
View File
@@ -18,10 +18,6 @@
#include <string.h>
#include <stdio.h> // NULL
#ifdef ENABLE_AM_FIX
#include "am_fix.h"
#endif
#include "audio.h"
#include "board.h"
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
@@ -52,6 +48,10 @@
#include "driver/system.h"
#include "driver/systick.h"
#include "driver/py25q16.h"
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
#include "driver/mb_flash.h"
#include "ui/multiboot.h"
#endif
#ifdef ENABLE_UART
#include "driver/uart.h"
#endif
@@ -81,6 +81,14 @@ void Main(void)
SYSTICK_Init();
BOARD_Init();
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
/* Resolve the active settings bank BEFORE any EEPROM/settings access
* below. This also adopts a normally-flashed firmware as slot 0 (discreet
* self-backup) when the running image isn't the slot the marker points to.
* Calibration stays shared regardless of the selected bank. */
PY25Q16_SetBankBase(MB_BankBase(MB_BootResolveState()));
#endif
boot_counter_10ms = 250; // 2.5 sec
#ifdef ENABLE_UART
@@ -111,7 +119,6 @@ void Main(void)
gCB = gEeprom.CROSS_BAND_RX_TX;
#endif
SETTINGS_WriteBuildOptions();
SETTINGS_LoadCalibration();
RADIO_ConfigureChannel(0, VFO_CONFIGURE_RELOAD);
@@ -126,12 +133,18 @@ void Main(void)
BATTERY_GetReadings(false);
#ifdef ENABLE_AM_FIX
AM_fix_init();
#endif
BOOT_Mode_t BootMode = BOOT_GetMode();
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
/* Run before the welcome screen and the normal application UI. EXIT from
* the selector simply resumes this boot as if no special mode was held. */
if (BootMode == BOOT_MODE_MULTIBOOT)
{
BOOT_ProcessMode(BootMode);
BootMode = BOOT_MODE_NORMAL;
}
#endif
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
if (BootMode == BOOT_MODE_RESCUE_OPS)
{
+5 -16
View File
@@ -107,10 +107,6 @@ bool gSetting_ScrambleEnable;
enum BacklightOnRxTx_t gSetting_backlight_on_tx_rx;
#ifdef ENABLE_AM_FIX
bool gSetting_AM_fix = true;
#endif
#ifdef ENABLE_FEAT_F4HWN_SLEEP
uint8_t gSetting_set_off = 1;
bool gWakeUp = false;
@@ -148,9 +144,6 @@ enum BacklightOnRxTx_t gSetting_backlight_on_tx_rx;
uint8_t gDW = 0;
uint8_t gCB = 0;
bool gSaveRxMode = false;
uint8_t crc[15] = { 0 };
uint8_t lErrorsDuringAirCopy = 0;
uint8_t gAircopyStep = 0;
uint8_t gAircopyCurrentMapIndex = 0;
bool gAirCopyBootMode = 0;
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
@@ -244,8 +237,8 @@ bool gCssBackgroundScan;
volatile bool gScheduleScanListen = true;
volatile uint16_t gScanPauseDelayIn_10ms;
#if defined(ENABLE_ALARM) || defined(ENABLE_TX1750)
AlarmState_t gAlarmState;
#ifdef ENABLE_TX1750
bool gTx1750Active;
#endif
uint16_t gMenuCountdown;
bool gPttWasReleased;
@@ -258,7 +251,7 @@ bool gFlagResetVfos;
bool gRequestSaveVFO;
uint16_t gRequestSaveChannel;
bool gRequestSaveSettings;
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
bool gRequestSaveFM;
#endif
bool gFlagPrepareTX;
@@ -266,7 +259,7 @@ bool gFlagPrepareTX;
bool gFlagAcceptSetting;
bool gFlagRefreshSetting;
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
bool gFlagSaveFM;
#endif
bool g_CDCSS_Lost;
@@ -288,10 +281,6 @@ uint16_t gNextMrChannel;
ReceptionMode_t gRxReceptionMode;
bool gRxVfoIsActive;
#ifdef ENABLE_ALARM
uint8_t gAlarmToneCounter;
uint16_t gAlarmRunningCounter;
#endif
bool gKeyBeingHeld;
bool gPttIsPressed;
uint8_t gPttDebounceCounter;
@@ -323,7 +312,7 @@ volatile bool gNextTimeslice40ms;
volatile bool gScheduleNOAA = true;
#endif
volatile bool gFlagTailNoteEliminationComplete;
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
volatile bool gScheduleFM;
#endif
+5 -22
View File
@@ -65,14 +65,6 @@ enum {
VFO_CONFIGURE_RELOAD
};
enum AlarmState_t {
ALARM_STATE_OFF = 0,
ALARM_STATE_TXALARM,
ALARM_STATE_SITE_ALARM,
ALARM_STATE_TX1750
};
typedef enum AlarmState_t AlarmState_t;
enum ReceptionMode_t {
RX_MODE_NONE = 0, // squelch close ?
RX_MODE_DETECTED, // signal detected
@@ -167,10 +159,6 @@ extern bool gSetting_ScrambleEnable;
extern enum BacklightOnRxTx_t gSetting_backlight_on_tx_rx;
#ifdef ENABLE_AM_FIX
extern bool gSetting_AM_fix;
#endif
#ifdef ENABLE_FEAT_F4HWN_SLEEP
extern uint8_t gSetting_set_off;
extern bool gWakeUp;
@@ -231,9 +219,6 @@ extern enum BacklightOnRxTx_t gSetting_backlight_on_tx_rx;
extern uint8_t gDW;
extern uint8_t gCB;
extern bool gSaveRxMode;
extern uint8_t crc[15];
extern uint8_t lErrorsDuringAirCopy;
extern uint8_t gAircopyStep;
extern uint8_t gAircopyCurrentMapIndex;
extern bool gAirCopyBootMode;
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
@@ -393,7 +378,7 @@ enum
extern volatile bool gScheduleScanListen;
extern volatile uint16_t gScanPauseDelayIn_10ms;
extern AlarmState_t gAlarmState;
extern bool gTx1750Active;
extern uint16_t gMenuCountdown;
extern bool gPttWasReleased;
extern bool gPttWasPressed;
@@ -404,7 +389,7 @@ extern bool gFlagResetVfos;
extern bool gRequestSaveVFO;
extern uint16_t gRequestSaveChannel;
extern bool gRequestSaveSettings;
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
extern bool gRequestSaveFM;
#endif
extern uint8_t gKeypadLocked;
@@ -413,7 +398,7 @@ extern bool gFlagPrepareTX;
extern bool gFlagAcceptSetting; // accept menu setting
extern bool gFlagRefreshSetting; // refresh menu display
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
extern bool gFlagSaveFM;
#endif
extern bool g_CDCSS_Lost;
@@ -438,8 +423,6 @@ extern ReceptionMode_t gRxReceptionMode;
//TRUE when dual watch is momentarly suspended and RX_VFO is locked to either last TX or RX
extern bool gRxVfoIsActive;
extern uint8_t gAlarmToneCounter;
extern uint16_t gAlarmRunningCounter;
extern bool gKeyBeingHeld;
extern bool gPttIsPressed;
extern uint8_t gPttDebounceCounter;
@@ -454,7 +437,7 @@ extern uint8_t gFSKWriteIndex;
extern volatile bool gNextTimeslice;
extern bool gUpdateDisplay;
extern bool gF_LOCK;
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
extern uint8_t gFM_ChannelPosition;
#endif
extern uint8_t gShowChPrefix;
@@ -470,7 +453,7 @@ extern volatile bool gNextTimeslice40ms;
#endif
extern volatile bool gFlagTailNoteEliminationComplete;
extern volatile uint8_t gVFOStateResumeCountdown_500ms;
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
extern volatile bool gScheduleFM;
#endif
extern uint8_t gIsLocked;
+9 -33
View File
@@ -17,12 +17,11 @@
#include "driver/bk4819-regs.h"
#include <string.h>
#include "am_fix.h"
#include "app/dtmf.h"
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
#include "app/rxtx_log.h"
#endif
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
#include "app/fm.h"
#endif
#include "audio.h"
@@ -767,12 +766,7 @@ void RADIO_SetupRegisters(bool switchToForeground)
case BK4819_FILTER_BW_WIDE:
case BK4819_FILTER_BW_NARROW:
case BK4819_FILTER_BW_NARROWER:
#ifdef ENABLE_AM_FIX
// BK4819_SetFilterBandwidth(Bandwidth, gRxVfo->Modulation == MODULATION_AM && gSetting_AM_fix);
BK4819_SetFilterBandwidth(Bandwidth, true);
#else
BK4819_SetFilterBandwidth(Bandwidth, false);
#endif
BK4819_SetFilterBandwidth(Bandwidth, false);
break;
}
}
@@ -893,7 +887,7 @@ void RADIO_SetupRegisters(bool switchToForeground)
#ifdef ENABLE_NOAA
&& !IS_NOAA_CHANNEL(gCurrentVfo->CHANNEL_SAVE)
#endif
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
&& !gFmRadioMode
#endif
){
@@ -994,12 +988,7 @@ void RADIO_SetTxParameters(void)
case BK4819_FILTER_BW_WIDE:
case BK4819_FILTER_BW_NARROW:
case BK4819_FILTER_BW_NARROWER:
#ifdef ENABLE_AM_FIX
// BK4819_SetFilterBandwidth(Bandwidth, gCurrentVfo->Modulation == MODULATION_AM && gSetting_AM_fix);
BK4819_SetFilterBandwidth(Bandwidth, true);
#else
BK4819_SetFilterBandwidth(Bandwidth, false);
#endif
BK4819_SetFilterBandwidth(Bandwidth, false);
break;
}
@@ -1150,14 +1139,6 @@ void RADIO_SetupAGC(bool listeningAM, bool disable)
return;
lastSettings = newSettings;
#ifdef ENABLE_AM_FIX
if (listeningAM && gSetting_AM_fix) {
BK4819_SetAGC(0);
AM_fix_enable(!disable);
return;
}
#endif
BK4819_SetAGC(!disable);
BK4819_InitAGC(listeningAM);
}
@@ -1208,9 +1189,6 @@ void RADIO_PrepareTX(void)
if(TX_freq_check(gCurrentVfo->pTX->Frequency) != 0
#ifdef ENABLE_FEAT_F4HWN
&& gCurrentVfo->TX_LOCK == true
#endif
#if defined(ENABLE_ALARM) || defined(ENABLE_TX1750)
&& gAlarmState != ALARM_STATE_SITE_ALARM
#endif
){
// TX frequency not allowed
@@ -1235,19 +1213,17 @@ void RADIO_PrepareTX(void)
State = VFO_STATE_TX_DISABLE;
}
#endif
#ifndef ENABLE_TX_WHEN_AM
else if (gCurrentVfo->Modulation != MODULATION_FM) {
// not allowed to TX if in AM mode
// AM and other non-FM modes are receive-only.
State = VFO_STATE_TX_DISABLE;
}
#endif
if (State != VFO_STATE_NORMAL) {
// TX not allowed
RADIO_SetVfoState(State);
#if defined(ENABLE_ALARM) || defined(ENABLE_TX1750)
gAlarmState = ALARM_STATE_OFF;
#ifdef ENABLE_TX1750
gTx1750Active = false;
#endif
#ifdef ENABLE_DTMF_CALLING
@@ -1281,8 +1257,8 @@ void RADIO_PrepareTX(void)
gTxTimerCountdown_500ms = 0; // no timeout
#if defined(ENABLE_ALARM) || defined(ENABLE_TX1750)
if (gAlarmState == ALARM_STATE_OFF)
#ifdef ENABLE_TX1750
if (!gTx1750Active)
#endif
{
-1
View File
@@ -50,7 +50,6 @@ enum VfoState_t
VFO_STATE_BAT_LOW,
VFO_STATE_TX_DISABLE,
VFO_STATE_TIMEOUT,
VFO_STATE_ALARM,
VFO_STATE_VOLTAGE_HIGH,
_VFO_STATE_LAST_ELEMENT
};
+2 -2
View File
@@ -16,7 +16,7 @@
#include "scheduler.h"
#include "app/chFrScanner.h"
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
#include "app/fm.h"
#endif
#include "app/scanner.h"
@@ -136,7 +136,7 @@ void SysTick_Handler(void)
DECREMENT_AND_TRIGGER(gCountdownToPlayNextVoice_10ms, gFlagPlayQueuedVoice);
#endif
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
if (gFM_ScanState != FM_SCAN_OFF && gCurrentFunction != FUNCTION_MONITOR)
if (gCurrentFunction != FUNCTION_TRANSMIT && gCurrentFunction != FUNCTION_RECEIVE)
DECREMENT_AND_TRIGGER(gFmPlayCountdown_10ms, gScheduleFM);
+19 -90
View File
@@ -90,8 +90,10 @@ void SETTINGS_InitEEPROM(void)
// 4. Reset logo lines (clear to null for strlen() == 0)
char logoLines[32];
PY25Q16_ReadBuffer(0x00A0C8, logoLines, sizeof(logoLines));
/* The two boot-message lines are contiguous in external flash. */
char bootMessageLines[32];
PY25Q16_ReadBuffer(SETTINGS_BOOT_MESSAGE_LINE1_ADDR,
bootMessageLines, sizeof(bootMessageLines));
bool needsWrite = false;
@@ -99,12 +101,12 @@ void SETTINGS_InitEEPROM(void)
int offset = line * 16;
for (int i = 0; i < 16; i++) {
char c = logoLines[offset + i];
char c = bootMessageLines[offset + i];
if (c == 0) {
break;
}
if (c < 0x20 || c > 0x7E) {
memset(logoLines + offset, 0, 16);
memset(bootMessageLines + offset, 0, 16);
needsWrite = true;
break;
}
@@ -112,7 +114,8 @@ void SETTINGS_InitEEPROM(void)
}
if (needsWrite) {
PY25Q16_WriteBuffer(0x00A0C8, logoLines, sizeof(logoLines), false);
PY25Q16_WriteBuffer(SETTINGS_BOOT_MESSAGE_LINE1_ADDR,
bootMessageLines, sizeof(bootMessageLines), false);
}
// 5. Reset dBmCorrTable
@@ -152,7 +155,9 @@ void SETTINGS_InitEEPROM(void)
gEeprom.SET_KEY = ((Data[4] >> 2) & 0x0F) > 4 ? 0 : (Data[4] >> 2) & 0x0F;
gEeprom.SET_NAV = (Data[4] & 0x40) != 0;
#else
gEeprom.KEY_LOCK = (Data[4] < 2) ? Data[4] : false;
// RescueOps fields can be present in a config bank shared with another
// preset. Read only KEY_LOCK here and leave the other bits untouched.
gEeprom.KEY_LOCK = (Data[4] & 0x01) != 0;
#endif
#ifdef ENABLE_VOX
gEeprom.VOX_SWITCH = (Data[5] < 2) ? Data[5] : false;
@@ -164,9 +169,6 @@ void SETTINGS_InitEEPROM(void)
PY25Q16_ReadBuffer(0x00A008, Data, 8);
gEeprom.BACKLIGHT_MAX = (Data[0] & 0xF) <= 10 ? (Data[0] & 0xF) : 10;
gEeprom.BACKLIGHT_MIN = (Data[0] >> 4) < gEeprom.BACKLIGHT_MAX ? (Data[0] >> 4) : 0;
#ifdef ENABLE_BLMIN_TMP_OFF
gEeprom.BACKLIGHT_MIN_STAT = BLMIN_STAT_ON;
#endif
gEeprom.CHANNEL_DISPLAY_MODE = (Data[1] < 4) ? Data[1] : MDF_FREQUENCY; // 4 instead of 3 - extra display mode
gEeprom.CROSS_BAND_RX_TX = (Data[2] < 3) ? Data[2] : CROSS_BAND_OFF;
gEeprom.BATTERY_SAVE = (Data[3] < 6) ? Data[3] : 4;
@@ -288,9 +290,6 @@ gEeprom.FreqChannel[1] = IS_FREQ_CHANNEL(Data16[5]) ? Data16[5] : (FREQ_CHANNE
// 0EA8..0EAF
PY25Q16_ReadBuffer(0x00A0A8 + 0x18, Data, 8);
#ifdef ENABLE_ALARM
gEeprom.ALARM_MODE = (Data[0] < 2) ? Data[0] : true;
#endif
gEeprom.ROGER = (Data[1] < 3) ? Data[1] : ROGER_MODE_OFF;
gEeprom.REPEATER_TAIL_TONE_ELIMINATION = (Data[2] < 11) ? Data[2] : 0;
gEeprom.TX_VFO = (Data[3] < 2) ? Data[3] : 0;
@@ -381,11 +380,7 @@ gEeprom.FreqChannel[1] = IS_FREQ_CHANNEL(Data16[5]) ? Data16[5] : (FREQ_CHANNE
#ifdef ENABLE_AUDIO_BAR
gSetting_mic_bar = !!(Data[7] & (1u << 4));
#endif
#ifndef ENABLE_FEAT_F4HWN
#ifdef ENABLE_AM_FIX
gSetting_AM_fix = !!(Data[7] & (1u << 5));
#endif
#endif
// Data[7] bit 5 is reserved (legacy ENABLE_AM_FIX).
gSetting_backlight_on_tx_rx = (Data[7] >> 6) & 3u;
if (!gEeprom.VFO_OPEN)
@@ -906,7 +901,11 @@ void SETTINGS_SaveSettings(void)
((gEeprom.SET_KEY & 0x0F) << 2) |
(gEeprom.SET_NAV ? 0x40 : 0);
#else
State[4] = gEeprom.KEY_LOCK;
// A non-RescueOps preset owns KEY_LOCK only. Preserve Set RescueOps,
// SetKEY, SetNav and reserved bits from a config created by another
// preset while updating bit 0.
PY25Q16_ReadBuffer(0x00A004, &State[4], 1);
State[4] = (State[4] & 0xFEu) | (gEeprom.KEY_LOCK ? 0x01u : 0u);
#endif
#ifdef ENABLE_VOX
@@ -1000,11 +999,7 @@ void SETTINGS_SaveSettings(void)
// 0x0EA8
State = SecBuf + 0x18;
#if defined(ENABLE_ALARM) || defined(ENABLE_TX1750)
State[0] = gEeprom.ALARM_MODE;
#else
State[0] = false;
#endif
State[0] = false;
State[1] = gEeprom.ROGER;
State[2] = gEeprom.REPEATER_TAIL_TONE_ELIMINATION;
State[3] = gEeprom.TX_VFO;
@@ -1086,11 +1081,7 @@ void SETTINGS_SaveSettings(void)
#ifdef ENABLE_AUDIO_BAR
if (!gSetting_mic_bar) State[7] &= ~(1u << 4);
#endif
#ifndef ENABLE_FEAT_F4HWN
#ifdef ENABLE_AM_FIX
if (!gSetting_AM_fix) State[7] &= ~(1u << 5);
#endif
#endif
// State[7] bit 5 is preserved (legacy ENABLE_AM_FIX).
State[7] = (State[7] & ~(3u << 6)) | ((gSetting_backlight_on_tx_rx & 3u) << 6);
PY25Q16_WriteBuffer(0x00A150, SecBuf, 8, false);
@@ -1269,68 +1260,6 @@ void SETTINGS_UpdateChannel(uint16_t channel, const VFO_Info_t *pVFO, bool keep)
SETTINGS_SaveChannelName(channel, "");
}
void SETTINGS_WriteBuildOptions(void)
{
uint8_t State[8];
#ifdef ENABLE_FEAT_F4HWN
// 0x1FF0
PY25Q16_ReadBuffer(0x00A158, State, sizeof(State));
#endif
State[0] = 0
#ifdef ENABLE_FMRADIO
| (1 << 0)
#endif
#ifdef ENABLE_NOAA
| (1 << 1)
#endif
#ifdef ENABLE_VOICE
| (1 << 2)
#endif
#ifdef ENABLE_VOX
| (1 << 3)
#endif
#ifdef ENABLE_ALARM
| (1 << 4)
#endif
#ifdef ENABLE_TX1750
| (1 << 5)
#endif
#ifdef ENABLE_PWRON_PASSWORD
| (1 << 6)
#endif
#ifdef ENABLE_DTMF_CALLING
| (1 << 7)
#endif
;
State[1] = 0
#ifdef ENABLE_FLASHLIGHT
| (1 << 0)
#endif
#ifdef ENABLE_WIDE_RX
| (1 << 1)
#endif
#ifdef ENABLE_BYP_RAW_DEMODULATORS
| (1 << 2)
#endif
#ifdef ENABLE_FEAT_F4HWN_GAME
| (1 << 3)
#endif
#ifdef ENABLE_AM_FIX
| (1 << 4)
#endif
#ifdef ENABLE_SPECTRUM
| (1 << 5)
#endif
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
| (1 << 6)
#endif
;
PY25Q16_WriteBuffer(0x00A158, State, sizeof(State), false);
}
#ifdef ENABLE_FEAT_F4HWN_RESUME_STATE
void SETTINGS_WriteCurrentState(void)
{
+33 -50
View File
@@ -25,6 +25,10 @@
#include "radio.h"
#include <driver/backlight.h>
/* Shared PY25Q16 locations for the two configurable boot-message lines. */
#define SETTINGS_BOOT_MESSAGE_LINE1_ADDR 0x00A0C8u
#define SETTINGS_BOOT_MESSAGE_LINE2_ADDR 0x00A0D8u
enum POWER_OnDisplayMode_t {
#ifdef ENABLE_FEAT_F4HWN
POWER_ON_DISPLAY_MODE_ALL,
@@ -100,43 +104,35 @@ enum {
};
enum ACTION_OPT_t {
ACTION_OPT_NONE = 0,
ACTION_OPT_FLASHLIGHT,
ACTION_OPT_POWER,
ACTION_OPT_MONITOR,
ACTION_OPT_SCAN,
ACTION_OPT_VOX,
ACTION_OPT_ALARM,
ACTION_OPT_FM,
ACTION_OPT_1750,
ACTION_OPT_KEYLOCK,
ACTION_OPT_A_B,
ACTION_OPT_VFO_MR,
ACTION_OPT_SWITCH_DEMODUL,
ACTION_OPT_BLMIN_TMP_OFF, //BackLight Minimum Temporay OFF
#ifdef ENABLE_FEAT_F4HWN
ACTION_OPT_RXMODE,
ACTION_OPT_MAINONLY,
ACTION_OPT_PTT,
ACTION_OPT_WN,
ACTION_OPT_BACKLIGHT,
ACTION_OPT_MUTE,
ACTION_OPT_RXA,
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
ACTION_OPT_POWER_HIGH,
ACTION_OPT_REMOVE_OFFSET,
#endif
#endif
#ifdef ENABLE_FEAT_F4HWN_BEAM
ACTION_OPT_BEAM,
#endif
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
ACTION_OPT_RXTX_LOG,
#endif
#ifdef ENABLE_FEAT_F4HWN_FOXHUNT
ACTION_OPT_FOXHUNT,
#endif
ACTION_OPT_LEN
/* Persisted in EEPROM: never renumber or make these values conditional. */
ACTION_OPT_NONE = 0,
ACTION_OPT_FLASHLIGHT = 1,
ACTION_OPT_POWER = 2,
ACTION_OPT_MONITOR = 3,
ACTION_OPT_SCAN = 4,
ACTION_OPT_VOX = 5,
ACTION_OPT_FM = 6,
ACTION_OPT_1750 = 7,
ACTION_OPT_KEYLOCK = 8,
ACTION_OPT_A_B = 9,
ACTION_OPT_VFO_MR = 10,
ACTION_OPT_SWITCH_DEMODUL = 11,
ACTION_OPT_RXMODE = 12,
ACTION_OPT_MAINONLY = 13,
ACTION_OPT_PTT = 14,
ACTION_OPT_WN = 15,
ACTION_OPT_MUTE = 16,
ACTION_OPT_RXA = 17,
/* Preset-specific actions keep their IDs even when not compiled. */
ACTION_OPT_RXTX_LOG = 18,
ACTION_OPT_BEAM = 19,
ACTION_OPT_POWER_HIGH = 20,
ACTION_OPT_REMOVE_OFFSET = 21,
ACTION_OPT_FOXHUNT = 22,
ACTION_OPT_BEACON = 23,
ACTION_OPT_LEN = 24
};
#ifdef ENABLE_VOICE
@@ -149,12 +145,6 @@ enum ACTION_OPT_t {
typedef enum VOICE_Prompt_t VOICE_Prompt_t;
#endif
enum ALARM_Mode_t {
ALARM_MODE_SITE = 0,
ALARM_MODE_TONE
};
typedef enum ALARM_Mode_t ALARM_Mode_t;
enum ROGER_Mode_t {
ROGER_MODE_OFF = 0,
ROGER_MODE_ROGER,
@@ -237,9 +227,6 @@ typedef struct {
uint8_t field38_0x33;
uint8_t AUTO_KEYPAD_LOCK;
#if defined(ENABLE_ALARM) || defined(ENABLE_TX1750)
ALARM_Mode_t ALARM_MODE;
#endif
POWER_OnDisplayMode_t POWER_ON_DISPLAY_MODE;
ROGER_Mode_t ROGER;
uint8_t REPEATER_TAIL_TONE_ELIMINATION;
@@ -301,9 +288,6 @@ typedef struct {
uint8_t KEY_M_LONG_PRESS_ACTION;
uint8_t BACKLIGHT_MIN;
#ifdef ENABLE_BLMIN_TMP_OFF
BLMIN_STAT_t BACKLIGHT_MIN_STAT;
#endif
uint8_t BACKLIGHT_MAX;
BATTERY_Type_t BATTERY_TYPE;
#ifdef ENABLE_RSSI_BAR
@@ -350,7 +334,6 @@ void SETTINGS_SaveChannelName(uint16_t channel, const char * name);
void SETTINGS_SaveChannel(uint16_t Channel, uint8_t VFO, const VFO_Info_t *pVFO, uint8_t Mode);
void SETTINGS_SaveBatteryCalibration(const uint16_t * batteryCalibration);
void SETTINGS_UpdateChannel(uint16_t channel, const VFO_Info_t *pVFO, bool keep);
void SETTINGS_WriteBuildOptions(void);
#ifdef ENABLE_FEAT_F4HWN_RESUME_STATE
void SETTINGS_WriteCurrentState(void);
#endif
+49 -74
View File
@@ -27,14 +27,6 @@
#include "ui/helper.h"
#include "ui/inputbox.h"
static void set_bit(uint8_t* array, int bit_index) {
array[bit_index / 8] |= (1 << (bit_index % 8));
}
static int get_bit(uint8_t* array, int bit_index) {
return (array[bit_index / 8] >> (bit_index % 8)) & 1;
}
void UI_DisplayAircopy(void)
{
char String[16];
@@ -45,10 +37,21 @@ void UI_DisplayAircopy(void)
if (gAircopyState == AIRCOPY_READY) {
pPrintStr = "AIR COPY(RDY)";
} else if (gAircopyState == AIRCOPY_TRANSFER) {
pPrintStr = "AIR COPY";
if (gAircopyAll) {
// All mode: show the slice being replicated in place of the title.
const uint8_t m = AIRCOPY_CurrentSliceMap();
if (m < AIRCOPY_NUM_BANKS)
sprintf(String, "MEM %03u-%03u", (m * 128) + 1, (m + 1) * 128);
else
strcpy(String, "SETTINGS");
pPrintStr = String;
} else {
pPrintStr = "AIR COPY";
}
} else if (gAircopyState == AIRCOPY_COMPLETE) {
pPrintStr = "AIR COPY OK";
} else {
pPrintStr = "AIR COPY(CMP)";
gAircopyState = AIRCOPY_READY;
pPrintStr = "AIR COPY FAIL";
}
UI_PrintString(pPrintStr, 2, 127, 0, 8);
@@ -67,89 +70,61 @@ void UI_DisplayAircopy(void)
// show the main large frequency digits
UI_DisplayFrequency(String, 16, 2, false);
// Get the current map and calculate percentage based on its total blocks
const AIRCOPY_TransferMap_t *currentMap = AIRCOPY_GetCurrentMap();
const uint16_t totalBlocks = AIRCOPY_GetTotalBlocks();
uint16_t doneBlocks = gAirCopyBlockNumber;
uint16_t doneBlocks = gAirCopyBlockNumber + gErrorsDuringAirCopy;
if (doneBlocks > currentMap->total_blocks)
doneBlocks = currentMap->total_blocks;
if (doneBlocks > totalBlocks)
doneBlocks = totalBlocks;
// Draw memory selection
if (gAircopyState == AIRCOPY_READY)
{
doneBlocks = 0;
if(gAircopyCurrentMapIndex < AIRCOPY_NUM_BANKS) {
if(gAircopyCurrentMapIndex < AIRCOPY_NUM_BANKS) {
sprintf(String, "MEM %03u - %03u", (gAircopyCurrentMapIndex * 128) + 1, (gAircopyCurrentMapIndex + 1) * 128);
} else if(gAircopyCurrentMapIndex == AIRCOPY_NUM_BANKS) {
strcpy(String, "Settings");
} else {
strcpy(String, "Settings");
strcpy(String, "All (Mem+Set)");
}
UI_PrintString(String, 2, 127, 5, 8);
}
else
{
uint16_t percent = (doneBlocks * 10000) / currentMap->total_blocks;
uint16_t percent = (doneBlocks * 10000) / totalBlocks;
const unsigned displayedErrors = gErrorsDuringAirCopy > 99u
? 99u
: gErrorsDuringAirCopy;
if (gAirCopyIsSendMode == 0) {
sprintf(String, "RCV:%02u.%02u%% E:%d", percent / 100, percent % 100, gErrorsDuringAirCopy);
if (gAircopyState == AIRCOPY_COMPLETE || gAircopyState == AIRCOPY_FAILED) {
sprintf(String, "%s %u/%u %s:%u",
gAircopyState == AIRCOPY_COMPLETE ? "OK" : "KO",
doneBlocks, totalBlocks,
gAirCopyIsSendMode ? "RT" : "ER",
displayedErrors);
} else if (gAirCopyIsSendMode == 0) {
sprintf(String, "RX:%02u.%02u ER:%u", percent / 100, percent % 100,
displayedErrors);
} else {
sprintf(String, "SND:%02u.%02u%%", percent / 100, percent % 100);
sprintf(String, "TX:%02u.%02u RT:%u", percent / 100, percent % 100,
displayedErrors);
}
// Draw gauge
if(gAircopyStep != 0)
{
UI_PrintString(String, 2, 127, 5, 8);
gFrameBuffer[4][1] = 0x3c;
gFrameBuffer[4][2] = 0x42;
for(uint8_t i = 1; i <= AIRCOPY_BAR_WIDTH + 2; i++)
{
gFrameBuffer[4][2 + i] = 0x81;
}
gFrameBuffer[4][125] = 0x42;
gFrameBuffer[4][126] = 0x3c;
}
}
if (doneBlocks > 0)
{
// Track CRC errors per real block index
if (gErrorsDuringAirCopy != lErrorsDuringAirCopy)
{
// Mark the last processed block as faulty
set_bit(crc, doneBlocks - 1);
lErrorsDuringAirCopy = gErrorsDuringAirCopy;
}
uint16_t b = 0;
uint16_t fraction_accumulator = 0;
UI_PrintString(String, 2, 127, 5, 8);
gFrameBuffer[4][1] = 0x3c;
gFrameBuffer[4][2] = 0x42;
gFrameBuffer[4][3] = 0x81;
// Match the former DDA gauge exactly, including its partial first pixel.
const uint8_t filled = (doneBlocks * AIRCOPY_BAR_WIDTH + totalBlocks - 1u)
/ totalBlocks;
for (uint8_t col = 0; col < AIRCOPY_BAR_WIDTH; col++)
{
bool processed = (b < doneBlocks);
bool error = processed && get_bit(crc, b);
if (!processed)
gFrameBuffer[4][col + 4] = 0x81; // not yet processed
else if (error)
gFrameBuffer[4][col + 4] = 0x81; // error gap (intentional hole)
else
gFrameBuffer[4][col + 4] = 0xBD; // ok filled
// DDA/Bresenham algorythm
fraction_accumulator += currentMap->total_blocks;
while (fraction_accumulator >= AIRCOPY_BAR_WIDTH) {
fraction_accumulator -= AIRCOPY_BAR_WIDTH;
b++;
}
}
gFrameBuffer[4][col + 4] = col < filled ? 0xBD : 0x81;
gFrameBuffer[4][124] = 0x81;
gFrameBuffer[4][125] = 0x42;
gFrameBuffer[4][126] = 0x3c;
}
ST7565_BlitFullScreen();
}
#endif
#endif
+18 -8
View File
@@ -31,6 +31,13 @@
void UI_DisplayFM(void)
{
// Keep these labels in sync with the limits in driver/bk1080.c.
static const char BandNames[][10] = {
"87.5-108M",
"76-108M",
"76-90M",
"64-76M",
};
char String[16];
char *pPrintStr = String;
UI_DisplayClear();
@@ -41,13 +48,7 @@ void UI_DisplayFM(void)
UI_PrintString("FM", 2, 0, 0, 8);
sprintf(String, "%d%s-%dM",
BK1080_GetFreqLoLimit(gEeprom.FM_Band)/10,
gEeprom.FM_Band == 0 ? ".5" : "",
BK1080_GetFreqHiLimit(gEeprom.FM_Band)/10
);
UI_PrintStringSmallNormal(String, 1, 0, 6);
UI_PrintStringSmallNormal(BandNames[gEeprom.FM_Band], 1, 0, 6);
//uint8_t spacings[] = {20,10,5};
//sprintf(String, "%d0k", spacings[gEeprom.FM_Space % 3]);
@@ -81,7 +82,16 @@ void UI_DisplayFM(void)
UI_PrintString(pPrintStr, 0, 127, 3, 10); // memory, vfo, scan
if (gAskToSave || (gEeprom.FM_IsMrMode && gInputBoxIndex > 0)) {
UI_GenerateChannelString(String, gFM_ChannelPosition);
if (gInputBoxIndex == 0) {
sprintf(String, "CH-%02u", gFM_ChannelPosition + 1);
} else {
String[0] = 'C';
String[1] = 'H';
String[2] = '-';
for (unsigned int i = 0; i < 2; i++)
String[i + 3] = (gInputBox[i] == 10) ? '-' : gInputBox[i] + '0';
String[5] = '\0';
}
} else if (gAskToDelete) {
sprintf(String, "CH-%02u", gEeprom.FM_SelectedChannel + 1);
} else {
+2 -21
View File
@@ -25,25 +25,6 @@
#include "settings.h"
void UI_GenerateChannelString(char *pString, const uint16_t Channel)
{
unsigned int i;
if (gInputBoxIndex == 0)
{
sprintf(pString, "CH-%02u", Channel + 1);
return;
}
pString[0] = 'C';
pString[1] = 'H';
pString[2] = '-';
for (i = 0; i < 2; i++)
pString[i + 3] = (gInputBox[i] == 10) ? '-' : gInputBox[i] + '0';
pString[5] = 0;
}
void UI_GenerateChannelStringEx(char *pString, const bool bShowPrefix, const uint16_t ChannelNumber)
{
if (gInputBoxIndex > 0) {
@@ -421,12 +402,12 @@ void UI_DisplayPopup(const char *string)
UI_PrintStringSmallNormal("Press EXIT", 9, 118, 6);
}
void UI_DisplayClear()
void UI_DisplayClear(void)
{
memset(gFrameBuffer, 0, sizeof(gFrameBuffer));
}
void UI_StatusClear()
void UI_StatusClear(void)
{
memset(gStatusLine, 0, sizeof(gStatusLine));
}
+2 -3
View File
@@ -20,7 +20,6 @@
#include <stdbool.h>
#include <stdint.h>
void UI_GenerateChannelString(char *pString, const uint16_t Channel);
void UI_GenerateChannelStringEx(char *pString, const bool bShowPrefix, const uint16_t ChannelNumber);
void UI_PrintString(const char *pString, uint8_t Start, uint8_t End, uint8_t Line, uint8_t Width);
void UI_PrintStringSmallNormal(const char *pString, uint8_t Start, uint8_t End, uint8_t Line);
@@ -45,7 +44,7 @@ void UI_DrawPixelBuffer(uint8_t (*buffer)[128], uint8_t x, uint8_t y, bool black
void UI_DrawLineBuffer(uint8_t (*buffer)[128], int16_t x1, int16_t y1, int16_t x2, int16_t y2, bool black);
void UI_DrawRectangleBuffer(uint8_t (*buffer)[128], int16_t x1, int16_t y1, int16_t x2, int16_t y2, bool black);
void UI_DisplayClear();
void UI_StatusClear();
void UI_DisplayClear(void);
void UI_StatusClear(void);
#endif
+75 -100
View File
@@ -28,9 +28,6 @@
#include "app/beam.h"
#endif
#ifdef ENABLE_AM_FIX
#include "am_fix.h"
#endif
#include "bitmaps.h"
#include "board.h"
#include "driver/bk4819.h"
@@ -71,7 +68,10 @@ center_line_t center_line = CENTER_LINE_NONE;
#ifdef ENABLE_FEAT_F4HWN_SCAN_PROGRESS
#define SCAN_PROGRESS_MR_CHANNEL_BYTES ((MR_CHANNELS_MAX + 7u) / 8u)
#define SCAN_LIST_NAME_HOLD_500MS (2000u / 500u)
// Scan-list name hold, in 10 ms ticks. Counted down on the 10 ms timeslice (not the
// 500 ms one) so the hold is accurate to a single tick instead of +/- 500 ms. Stored
// in a uint8_t, so the practical ceiling is 255 ticks = 2.55 s.
#define SCAN_LIST_NAME_HOLD_10MS (1000u / 10u)
static bool gScanProgressSessionActive;
static bool gScanProgressSessionIsMemory;
@@ -86,7 +86,7 @@ static bool gScanProgressPrevResetVfosFlag;
static bool gScanProgressForceRebuild;
static uint16_t gScanProgressLastMemoryIndex;
static uint8_t gScanProgressPriorityState;
static uint8_t gScanListNameCountdown_500ms;
static uint8_t gScanListNameCountdown_10ms;
#define SCAN_PROGRESS_PRIORITY_LABEL_MASK 0x03u
#define SCAN_PROGRESS_PRIORITY_SEEN_SHIFT 2
#define SCAN_PROGRESS_PRIORITY_SEEN_MASK 0x1cu
@@ -149,7 +149,6 @@ const char *const VfoStateStr[] = {
[VFO_STATE_BAT_LOW]="BAT LOW",
[VFO_STATE_TX_DISABLE]="TX DISABLE",
[VFO_STATE_TIMEOUT]="TIMEOUT",
[VFO_STATE_ALARM]="ALARM",
[VFO_STATE_VOLTAGE_HIGH]="VOLT HIGH"
};
@@ -227,7 +226,7 @@ static void ScanProgress_ResetSession(void)
gScanProgressForceRebuild = false;
gScanProgressLastMemoryIndex = 0;
gScanProgressPriorityState = 0;
gScanListNameCountdown_500ms = 0;
gScanListNameCountdown_10ms = 0;
}
void UI_MAIN_NotifyScanProgressDataChanged(void)
@@ -239,7 +238,7 @@ void UI_MAIN_NotifyScanProgressDataChanged(void)
void UI_MAIN_NotifyScanListChanged(void)
{
UI_MAIN_NotifyScanProgressDataChanged();
gScanListNameCountdown_500ms = SCAN_LIST_NAME_HOLD_500MS;
gScanListNameCountdown_10ms = SCAN_LIST_NAME_HOLD_10MS;
gUpdateDisplay = true;
}
@@ -254,7 +253,7 @@ void UI_MAIN_NotifyScanListChanged(void)
// stall ~2 s with nothing on screen to explain the pause.
bool UI_MAIN_ShouldHoldScanResume(void)
{
return gScanListNameCountdown_500ms > 0 && IS_MR_CHANNEL(gNextMrChannel);
return gScanListNameCountdown_10ms > 0 && IS_MR_CHANNEL(gNextMrChannel);
}
static inline void ScanProgress_SetBit(uint8_t *map, uint16_t ch)
@@ -553,7 +552,7 @@ static bool UI_DrawScanProgress(void)
}
// Right after a scan-list change, briefly show its name instead of the progress bar
if (show_memory && gScanListNameCountdown_500ms > 0) {
if (show_memory && gScanListNameCountdown_10ms > 0) {
UI_MAIN_DrawScanListName();
return true;
}
@@ -818,8 +817,8 @@ void UI_DisplayAudioBar(void)
return; // screen is in use
}
#if defined(ENABLE_ALARM) || defined(ENABLE_TX1750)
if (gAlarmState != ALARM_STATE_OFF)
#ifdef ENABLE_TX1750
if (gTx1750Active)
return;
#endif
static uint8_t barsOld = 0;
@@ -847,15 +846,14 @@ void UI_DisplayAudioBar(void)
}
#endif
#ifdef ENABLE_FEAT_F4HWN_AUDIO_SCOPE
#if defined(ENABLE_FEAT_F4HWN_AUDIO_SCOPE) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
#define SCOPE_SAMPLES 43 // number of columns (43 × 3px = 128px wide)
#define SCOPE_NOISE_GATE 50u // minimum range below which the display shows baseline
#define SCOPE_FLOOR_RISE 2u // floor rise per frame (+100 units/s at 20ms/frame)
#define SCOPE_FLOOR_DROP_SHR 3u // floor drop IIR shift: drop by (floor-min) >> N per frame (~160ms to halve)
#define SCOPE_VOLUME_MIN 200u // let's assume that the sound level in silence is 200
void UI_DisplayAudioScope(void)
void UI_DisplayAudioScopeOverlay(const uint8_t line, const bool active)
{
static uint16_t g_scope_buf[SCOPE_SAMPLES];
static uint8_t g_scope_write = 0;
@@ -869,22 +867,11 @@ void UI_DisplayAudioScope(void)
static bool s_was_tx = false;
if (gCurrentFunction != FUNCTION_TRANSMIT) {
if (!active) {
s_was_tx = false;
return;
}
// This prevents a sudden spike on the bar caused by release the PTT button
if (!GPIO_IsPttPressed()
#ifdef ENABLE_VOX
&& !gEeprom.VOX_SWITCH
#endif
#ifdef ENABLE_FEAT_F4HWN
&& !gSetting_set_ptt_session
#endif
)
return;
if (!s_was_tx) {
// TX entry: full reset so every new transmission starts from a clean state
for (uint8_t i = 0; i < SCOPE_SAMPLES; i++) g_scope_buf[i] = SCOPE_VOLUME_MIN;
@@ -907,32 +894,6 @@ void UI_DisplayAudioScope(void)
g_scope_write = (g_scope_write + 1u) % SCOPE_SAMPLES;
// --------------------------------- Refresh display ---------------------------------
if (gLowBattery && !gLowBatteryConfirmed)
return;
if (gScreenToDisplay != DISPLAY_MAIN
#ifdef ENABLE_DTMF_CALLING
|| gDTMF_CallState != DTMF_CALL_STATE_NONE
#endif
)
return;
#if defined(ENABLE_ALARM) || defined(ENABLE_TX1750)
if (gAlarmState != ALARM_STATE_OFF)
return;
#endif
#ifdef ENABLE_FEAT_F4HWN
RxBlinkLed = 0;
RxBlinkLedCounter = 0;
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, false);
const unsigned int line = isMainOnly() ? 5 : 3;
#else
const unsigned int line = 3;
#endif
uint8_t *p_line = gFrameBuffer[line];
memset(p_line, 0, LCD_WIDTH);
@@ -972,9 +933,50 @@ void UI_DisplayAudioScope(void)
}
}
#ifdef ENABLE_FEAT_F4HWN_AUDIO_SCOPE
void UI_DisplayAudioScope(void)
{
const unsigned int line = isMainOnly() ? 5u : 3u;
/* Keep MAIN's original gating and side effects outside the shared renderer. */
if (gCurrentFunction != FUNCTION_TRANSMIT) {
UI_DisplayAudioScopeOverlay((uint8_t)line, false);
return;
}
if (!GPIO_IsPttPressed()
#ifdef ENABLE_VOX
&& !gEeprom.VOX_SWITCH
#endif
#ifdef ENABLE_FEAT_F4HWN
&& !gSetting_set_ptt_session
#endif
)
return;
if (gLowBattery && !gLowBatteryConfirmed)
return;
if (gScreenToDisplay != DISPLAY_MAIN
#ifdef ENABLE_DTMF_CALLING
|| gDTMF_CallState != DTMF_CALL_STATE_NONE
#endif
)
return;
#ifdef ENABLE_TX1750
if (gTx1750Active)
return;
#endif
#ifdef ENABLE_FEAT_F4HWN
RxBlinkLed = 0;
RxBlinkLedCounter = 0;
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, false);
#endif
UI_DisplayAudioScopeOverlay((uint8_t)line, true);
ST7565_BlitLine(line);
}
#endif // ENABLE_FEAT_F4HWN_AUDIO_SCOPE
#endif
#endif // ENABLE_FEAT_F4HWN_AUDIO_SCOPE || ENABLE_FEAT_F4HWN_OVERLAY_APPS
void DisplayRSSIBar(const bool now)
{
@@ -1075,9 +1077,6 @@ void DisplayRSSIBar(const bool now)
#ifdef ENABLE_FEAT_F4HWN
int16_t rssi_dBm =
BK4819_GetRSSI_dBm()
#ifdef ENABLE_AM_FIX
+ ((gSetting_AM_fix && gRxVfo->Modulation == MODULATION_AM) ? AM_fix_get_gain_diff() : 0)
#endif
+ dBmCorrTable[gRxVfo->Band];
// IARU VHF/UHF S-meter: S9 = -93 dBm, 1 S-unit = 6 dB
@@ -1114,9 +1113,6 @@ void DisplayRSSIBar(const bool now)
const int16_t s0_dBm = -gEeprom.S0_LEVEL; // S0 .. base level
const int16_t rssi_dBm =
BK4819_GetRSSI_dBm()
#ifdef ENABLE_AM_FIX
+ ((gSetting_AM_fix && gRxVfo->Modulation == MODULATION_AM) ? AM_fix_get_gain_diff() : 0)
#endif
+ dBmCorrTable[gRxVfo->Band];
int s0_9 = gEeprom.S0_LEVEL - gEeprom.S9_LEVEL;
@@ -1232,13 +1228,25 @@ void UI_MAIN_PrintAGC(bool now)
}
#endif
#ifdef ENABLE_FEAT_F4HWN_SCAN_PROGRESS
// Count the scan-list name hold down on the 10 ms tick. It used to ride the 500 ms
// tick, but the countdown is armed at an arbitrary instant, so the first interval was
// anywhere from ~0 to 500 ms - the name could linger up to half a second short of, or
// over, its nominal hold. At 10 ms resolution that error is one tick at most. Gated on
// DISPLAY_MAIN exactly as the old 500 ms path was, so it only ticks while the name can
// actually be on screen.
void UI_MAIN_TimeSlice10ms(void)
{
if (gScreenToDisplay == DISPLAY_MAIN
&& gScanListNameCountdown_10ms > 0
&& --gScanListNameCountdown_10ms == 0)
gUpdateDisplay = true;
}
#endif
void UI_MAIN_TimeSlice500ms(void)
{
if(gScreenToDisplay==DISPLAY_MAIN) {
#ifdef ENABLE_FEAT_F4HWN_SCAN_PROGRESS
if (gScanListNameCountdown_500ms > 0 && --gScanListNameCountdown_500ms == 0)
gUpdateDisplay = true;
#endif
#ifdef ENABLE_AGC_SHOW_DATA
UI_MAIN_PrintAGC(true);
return;
@@ -1376,6 +1384,8 @@ void UI_DisplayMain(void)
UI_PrintActionPickerLabel(previous, 1, false);
UI_PrintActionPickerLabel(selection, 2, true);
UI_PrintActionPickerLabel(next, 4, false);
if (!ACTION_IsAvailable(gSubMenu_SIDEFUNCTIONS[selection].id))
UI_PrintStringSmallNormalInverse("N/A", 53, 0, 6);
ST7565_BlitFullScreen();
return;
}
@@ -1566,11 +1576,6 @@ void UI_DisplayMain(void)
if (gCurrentFunction == FUNCTION_TRANSMIT)
{ // transmitting
#ifdef ENABLE_ALARM
if (gAlarmState == ALARM_STATE_SITE_ALARM)
mode = VFO_MODE_RX;
else
#endif
{
if (activeTxVFO == vfo_num)
{ // show the TX symbol
@@ -1732,12 +1737,6 @@ void UI_DisplayMain(void)
enum VfoState_t state = VfoState[vfo_num];
#ifdef ENABLE_ALARM
if (gCurrentFunction == FUNCTION_TRANSMIT && gAlarmState == ALARM_STATE_SITE_ALARM) {
if (activeTxVFO == vfo_num)
state = VFO_STATE_ALARM;
}
#endif
if (state != VFO_STATE_NORMAL)
{
if (state < ARRAY_SIZE(VfoStateStr))
@@ -2105,15 +2104,12 @@ void UI_DisplayMain(void)
}
GUI_DisplaySmallest(String, 68 + shift, line == 0 ? 17 : 49, false, true);
//sprintf(String, "%d.%02u", vfoInfo->StepFrequency / 100, vfoInfo->StepFrequency % 100);
//GUI_DisplaySmallest(String, 91, line == 0 ? 2 : 34, false, true);
}
#else
UI_PrintStringSmallNormal(s, LCD_WIDTH + 24, 0, line + 1);
#endif
if (state == VFO_STATE_NORMAL || state == VFO_STATE_ALARM)
if (state == VFO_STATE_NORMAL)
{ // show the TX power
uint8_t currentPower = vfoInfo->OUTPUT_POWER % 8;
uint8_t arrowPos = 19;
@@ -2142,8 +2138,6 @@ void UI_DisplayMain(void)
else
{
const char pwr_long[][5] = {"LOW1", "LOW2", "LOW3", "LOW4", "LOW5", "MID", "HIGH"};
//sprintf(String, "%s", pwr_long[currentPower]);
//GUI_DisplaySmallest(String, 24, line == 0 ? 17 : 49, false, true);
GUI_DisplaySmallest(pwr_long[currentPower], 24, line == 0 ? 17 : 49, false, true);
}
@@ -2181,9 +2175,7 @@ void UI_DisplayMain(void)
{
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
if(i == 3)
{
GUI_DisplaySmallest(dir_list[i], 43, line == 0 ? 17 : 49, false, true);
}
else
{
#endif
@@ -2358,23 +2350,6 @@ void UI_DisplayMain(void)
else
#endif
#if defined(ENABLE_AM_FIX) && defined(ENABLE_AM_FIX_SHOW_DATA)
if (rx && gEeprom.VfoInfo[gEeprom.RX_VFO].Modulation == MODULATION_AM && gSetting_AM_fix)
{
if (gScreenToDisplay != DISPLAY_MAIN
#ifdef ENABLE_DTMF_CALLING
|| gDTMF_CallState != DTMF_CALL_STATE_NONE
#endif
)
return;
center_line = CENTER_LINE_AM_FIX_DATA;
AM_fix_print_data(gEeprom.RX_VFO, String);
UI_PrintStringSmallNormal(String, 2, 0, 3);
}
else
#endif
#ifdef ENABLE_RSSI_BAR
if (rx) {
center_line = CENTER_LINE_RSSI;
+5 -1
View File
@@ -27,7 +27,6 @@ enum center_line_t {
CENTER_LINE_AUDIO_BAR,
CENTER_LINE_AUDIO_SCOPE,
CENTER_LINE_RSSI,
CENTER_LINE_AM_FIX_DATA,
CENTER_LINE_DTMF_DEC,
CENTER_LINE_CHARGE_DATA,
#ifdef ENABLE_FEAT_F4HWN_BEAM
@@ -48,6 +47,9 @@ extern center_line_t center_line;
#ifdef ENABLE_AUDIO_BAR
void UI_DisplayAudioBar(void);
#endif
#if defined(ENABLE_FEAT_F4HWN_AUDIO_SCOPE) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
void UI_DisplayAudioScopeOverlay(uint8_t line, bool active);
#endif
#ifdef ENABLE_FEAT_F4HWN_AUDIO_SCOPE
void UI_DisplayAudioScope(void);
#endif
@@ -58,10 +60,12 @@ void UI_DisplayMain(void);
void UI_MAIN_NotifyScanProgressDataChanged(void);
void UI_MAIN_NotifyScanListChanged(void);
bool UI_MAIN_ShouldHoldScanResume(void);
void UI_MAIN_TimeSlice10ms(void);
#else
static inline void UI_MAIN_NotifyScanProgressDataChanged(void) {}
static inline void UI_MAIN_NotifyScanListChanged(void) {}
static inline bool UI_MAIN_ShouldHoldScanResume(void) { return false; }
static inline void UI_MAIN_TimeSlice10ms(void) {}
#endif
#ifdef ENABLE_AGC_SHOW_DATA
+97 -60
View File
@@ -14,9 +14,11 @@
* limitations under the License.
*/
#include <assert.h>
#include <string.h>
#include <stdlib.h>
#include "../app/action.h"
#include "../app/dtmf.h"
#include "../app/menu.h"
#include "../bitmaps.h"
@@ -42,6 +44,10 @@
#include "menu.h"
#include "ui.h"
#include "welcome.h"
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
#include "driver/mb_flash.h"
#include "multiboot.h"
#endif
const t_menu_item MenuList[] =
@@ -106,9 +112,6 @@ const t_menu_item MenuList[] =
{"STE", MENU_STE },
{"RP STE", MENU_RP_STE },
{"1 Call", MENU_1_CALL },
#ifdef ENABLE_ALARM
{"AlarmT", MENU_AL_MOD },
#endif
#ifdef ENABLE_DTMF_CALLING
{"ANI ID", MENU_ANI_ID },
#endif
@@ -126,11 +129,6 @@ const t_menu_item MenuList[] =
{"D List", MENU_D_LIST },
#endif
{"D Live", MENU_D_LIVE_DEC }, // live DTMF decoder
#ifndef ENABLE_FEAT_F4HWN
#ifdef ENABLE_AM_FIX
{"AM Fix", MENU_AM_FIX },
#endif
#endif
{"VOX", MENU_VOX },
#ifdef ENABLE_FEAT_F4HWN
{"SysInf", MENU_VOL }, // was "VOL"
@@ -174,6 +172,9 @@ const t_menu_item MenuList[] =
#ifdef ENABLE_FEAT_F4HWN_LOGO_SAV
{"SetSav", MENU_SET_SAV },
#endif
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
{"SetCfg", MENU_SET_CFG }, // load another settings bank (reboots)
#endif
#endif
// hidden menu items from here on
// enabled if pressing both the PTT and upper side button at power-on
@@ -258,14 +259,6 @@ const char* const gSubMenu_MDF[] =
"NAME\n+\nFREQ"
};
#ifdef ENABLE_ALARM
const char* const gSubMenu_AL_MOD[] =
{
"SITE",
"TONE"
};
#endif
#ifdef ENABLE_DTMF_CALLING
const char* const gSubMenu_D_RSP[] =
{
@@ -481,57 +474,33 @@ const char* const gSubMenu_SCRAMBLER[] =
const t_sidefunction gSubMenu_SIDEFUNCTIONS[] =
{
{"NONE", ACTION_OPT_NONE},
#ifdef ENABLE_FLASHLIGHT
{"FLASH\nLIGHT", ACTION_OPT_FLASHLIGHT},
#endif
{"POWER", ACTION_OPT_POWER},
{"MONITOR", ACTION_OPT_MONITOR},
{"SCAN", ACTION_OPT_SCAN},
#ifdef ENABLE_VOX
{"VOX", ACTION_OPT_VOX},
#endif
#ifdef ENABLE_ALARM
{"ALARM", ACTION_OPT_ALARM},
#endif
#ifdef ENABLE_FMRADIO
{"FM RADIO", ACTION_OPT_FM},
#endif
#ifdef ENABLE_TX1750
{"1750Hz", ACTION_OPT_1750},
#endif
{"LOCK\nKEYPAD", ACTION_OPT_KEYLOCK},
{"VFO A\nVFO B", ACTION_OPT_A_B},
{"VFO\nMEM", ACTION_OPT_VFO_MR},
{"MODE", ACTION_OPT_SWITCH_DEMODUL},
#ifdef ENABLE_BLMIN_TMP_OFF
{"BLMIN\nTMP OFF", ACTION_OPT_BLMIN_TMP_OFF}, //BackLight Minimum Temporary OFF
#endif
#ifdef ENABLE_FEAT_F4HWN
{"RX MODE", ACTION_OPT_RXMODE},
{"MAIN ONLY", ACTION_OPT_MAINONLY},
{"PTT", ACTION_OPT_PTT},
{"WIDE\nNARROW", ACTION_OPT_WN},
{"MUTE", ACTION_OPT_MUTE},
#ifdef ENABLE_FEAT_F4HWN_AUDIO
{"RxA", ACTION_OPT_RXA},
#endif
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
{"POWER\nHIGH", ACTION_OPT_POWER_HIGH},
{"REMOVE\nOFFSET", ACTION_OPT_REMOVE_OFFSET},
#endif
#ifdef ENABLE_FEAT_F4HWN_BEAM
{"BEAM", ACTION_OPT_BEAM},
#endif
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
{"RF LOG", ACTION_OPT_RXTX_LOG},
#endif
#ifdef ENABLE_FEAT_F4HWN_FOXHUNT
{"FOX HUNT\nBEACON", ACTION_OPT_FOXHUNT},
#endif
#endif
{"RxA", ACTION_OPT_RXA},
{"RF LOG", ACTION_OPT_RXTX_LOG},
{"BEAM", ACTION_OPT_BEAM},
{"POWER\nHIGH", ACTION_OPT_POWER_HIGH},
{"REMOVE\nOFFSET", ACTION_OPT_REMOVE_OFFSET},
{"FOX HUNT", ACTION_OPT_FOXHUNT},
{"BEACON", ACTION_OPT_BEACON},
};
const uint8_t gSubMenu_SIDEFUNCTIONS_size = ARRAY_SIZE(gSubMenu_SIDEFUNCTIONS);
static_assert(ARRAY_SIZE(gSubMenu_SIDEFUNCTIONS) == ACTION_OPT_LEN);
bool gIsInSubMenu;
uint8_t gMenuCursor;
@@ -598,6 +567,9 @@ static const uint8_t CatChannels[] = {
#endif
MENU_BCL, MENU_COMPAND, MENU_AM, MENU_TX_LOCK, MENU_PTT_ID, MENU_LIST_CH,
MENU_MEM_CH, MENU_DEL_CH, MENU_MEM_NAME,
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
MENU_SET_CFG,
#endif
};
static const uint8_t CatScan[] = {
MENU_S_LIST, MENU_S_PRI, MENU_S_PRI_CH_1, MENU_S_PRI_CH_2, MENU_SC_REV,
@@ -836,6 +808,28 @@ static void UI_MENU_DrawTopRightRoundedBadge(const char *text, const uint8_t lin
UI_PrintStringSmallNormalInverse(text, text_x, 0, line);
}
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
/* Draw `text` (3x5 font) centred inside a fixed-width rounded inverse capsule:
* left edge `cap_left`, inclusive width `cap_w`, on framebuffer page `line`. Same
* capsule pattern as GUI_DisplaySmallestInverse (0x3E rounded ends, 0x7F body) but
* with the width decoupled from the text length, so two labels of different
* lengths (e.g. "SLOT 2" / "CFG 4") share one width and each stays centred. */
static void UI_MENU_DrawFixedCapsule(const char *text, uint8_t cap_left,
uint8_t cap_w, uint8_t line)
{
const uint8_t cap_right = (uint8_t)(cap_left + cap_w - 1u);
const uint8_t text_w = (uint8_t)(strlen(text) * 4u - 1u); /* 3x5 glyphs: 4 px/char, last one 3 px wide */
const uint8_t tx = (uint8_t)(cap_left + (cap_w - text_w) / 2u);
GUI_DisplaySmallest(text, tx, (uint8_t)(line * 8u + 1u), false, true);
gFrameBuffer[line][cap_left] ^= 0x3Eu;
for (uint8_t x = (uint8_t)(cap_left + 1u); x < cap_right; x++)
gFrameBuffer[line][x] ^= 0x7Fu;
gFrameBuffer[line][cap_right] ^= 0x3Eu;
}
#endif
void UI_DisplayMenu(void)
{
const unsigned int menu_list_width = 6; // max no. of characters on the menu list (left side)
@@ -844,6 +838,7 @@ void UI_DisplayMenu(void)
unsigned int i;
char String[64]; // bigger cuz we can now do multi-line in one string (use '\n' char)
char top_right_badge[16];
uint8_t top_right_badge_line = 1;
#ifdef ENABLE_FEAT_F4HWN_MENU_CAT
if (gMenuLevel == MENU_LEVEL_CAT)
@@ -1132,11 +1127,6 @@ void UI_DisplayMenu(void)
strcpy(String, gSubMenu_RX_TX[gSubMenuSelection]);
break;
#ifndef ENABLE_FEAT_F4HWN
#ifdef ENABLE_AM_FIX
case MENU_AM_FIX:
#endif
#endif
case MENU_BCL:
case MENU_BEEP:
case MENU_STE:
@@ -1338,12 +1328,6 @@ void UI_DisplayMenu(void)
}
break;
#ifdef ENABLE_ALARM
case MENU_AL_MOD:
sprintf(String, gSubMenu_AL_MOD[gSubMenuSelection]);
break;
#endif
#ifdef ENABLE_DTMF_CALLING
case MENU_ANI_ID:
strcpy(String, gEeprom.ANI_DTMF_ID);
@@ -1421,6 +1405,41 @@ void UI_DisplayMenu(void)
#ifdef ENABLE_FEAT_F4HWN
sprintf(String, "%s\n%s", AUTHOR_STRING_2, DISPLAY_VERSION_STRING_2);
UI_PrintStringSmallNormal(Edition, menu_item_x1 - 1, menu_item_x2, 6);
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
/* Two 3x5 inverse-capsule labels on one line (scan-list "label"
* style): the running firmware slot (M = Main) and the active
* config bank. They match unless SetCfg has pointed the bank at a
* different bank (e.g. SLOT 2 / CFG 4). */
const uint8_t fw_slot = MB_GetRunningSlot();
const uint8_t bank = MB_GetActiveBank();
char slot_lbl[8];
char cfg_lbl[8];
/* Only the last glyph varies (M / digit / ?), so poke it in place
* instead of pulling sprintf for a single character. */
strcpy(slot_lbl, "SLOT ?");
if (fw_slot == 0u)
slot_lbl[5] = 'M';
else if (fw_slot < MB_SLOT_COUNT)
slot_lbl[5] = (char)('0' + fw_slot);
strcpy(cfg_lbl, "CFG M"); /* bank 0 = base config, like SLOT M */
if (bank != 0u)
cfg_lbl[4] = (char)('0' + bank);
/* Both capsules share the wider label's width (6-char "SLOT x" ->
* 4*6+3 = 27 px); the shorter CFG text is centred inside its own.
* The two are drawn as one centred pair with a small gap, centred in
* the space between the separator bar (x=48) and the right screen
* edge, so they line up with the centred identity lines above. */
const uint8_t cap_w = (uint8_t)(4u * 6u + 3u); /* 27 */
const uint8_t cap_gap = 4u;
const uint8_t pair_w = (uint8_t)(2u * cap_w + cap_gap); /* 58 */
const uint8_t slot_left = (uint8_t)((48u + LCD_WIDTH - pair_w) / 2u); /* 59 */
const uint8_t cfg_left = (uint8_t)(slot_left + cap_w + cap_gap); /* 90 */
UI_MENU_DrawFixedCapsule(slot_lbl, slot_left, cap_w, 5);
UI_MENU_DrawFixedCapsule(cfg_lbl, cfg_left, cap_w, 5);
#endif
#else
sprintf(String, "%u.%02uV\n%u%%",
gBatteryVoltageAverage / 100, gBatteryVoltageAverage % 100,
@@ -1542,13 +1561,28 @@ void UI_DisplayMenu(void)
strcpy(String, gSubMenu_SET_NAV[gSubMenuSelection]);
break;
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
case MENU_SET_CFG:
strcpy(String, "CFG M"); /* bank 0 = base config, like SysInfo */
if (gSubMenuSelection != 0)
String[4] = (char)('0' + gSubMenuSelection);
break;
#endif
case MENU_F1SHRT:
case MENU_F1LONG:
case MENU_F2SHRT:
case MENU_F2LONG:
case MENU_MLONG:
{
const uint8_t action = gSubMenu_SIDEFUNCTIONS[gSubMenuSelection].id;
strcpy(String, gSubMenu_SIDEFUNCTIONS[gSubMenuSelection].name);
if (!ACTION_IsAvailable(action)) {
strcpy(top_right_badge, "N/A");
top_right_badge_line = 5;
}
break;
}
#ifdef ENABLE_FEAT_F4HWN_SLEEP
case MENU_SET_OFF:
@@ -1791,12 +1825,15 @@ void UI_DisplayMenu(void)
#endif
if (top_right_badge[0] != '\0') {
UI_MENU_DrawTopRightRoundedBadge(top_right_badge, 1, true, menu_item_x1, menu_item_x2);
UI_MENU_DrawTopRightRoundedBadge(top_right_badge, top_right_badge_line, true, menu_item_x1, menu_item_x2);
}
if ((m == MENU_RESET ||
m == MENU_MEM_CH ||
m == MENU_MEM_NAME ||
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
m == MENU_SET_CFG ||
#endif
m == MENU_DEL_CH) && gAskForConfirmation)
{ // display confirmation
char *pPrintStr = (gAskForConfirmation == 1) ? "SURE?" : "WAIT!";
+3 -9
View File
@@ -76,9 +76,6 @@ enum
MENU_S_PRI,
MENU_S_PRI_CH_1,
MENU_S_PRI_CH_2,
#ifdef ENABLE_ALARM
MENU_AL_MOD,
#endif
#ifdef ENABLE_DTMF_CALLING
MENU_ANI_ID,
#endif
@@ -101,9 +98,6 @@ enum
MENU_VOL,
MENU_BAT_TXT,
MENU_AM,
#ifdef ENABLE_AM_FIX
MENU_AM_FIX,
#endif
#ifndef ENABLE_FEAT_F4HWN
#ifdef ENABLE_NOAA
MENU_NOAA_S,
@@ -153,6 +147,9 @@ enum
MENU_NOAA_S,
#endif
MENU_SET_NAV,
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
MENU_SET_CFG,
#endif
#ifdef ENABLE_FEAT_F4HWN_AUDIO
MENU_SET_AUD,
#endif
@@ -215,9 +212,6 @@ extern const char* const gSubMenu_RXMode[4];
extern const char* const gSubMenu_VOICE[3];
#endif
extern const char* const gSubMenu_MDF[4];
#ifdef ENABLE_ALARM
extern const char* const gSubMenu_AL_MOD[2];
#endif
#ifdef ENABLE_DTMF_CALLING
extern const char* const gSubMenu_D_RSP[4];
#endif
+576
View File
@@ -0,0 +1,576 @@
/* Copyright 2026 Armel F4HWN
* https://github.com/armel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <string.h>
#include "driver/backlight.h"
#include "driver/gpio.h"
#include "driver/keyboard.h"
#include "driver/mb_flash.h"
#include "driver/st7565.h"
#include "driver/system.h"
#include "ui/helper.h"
#include "ui/multiboot.h"
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
#include "k5viewer.h"
#endif
static uint8_t gRunningSlot = 0xFFu;
static uint8_t gActiveBank = 0u;
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
static void mb_k5viewer_service(void)
{
/* The Multiboot selector is modal and does not return to APP_Update(). */
K5VIEWER_ParseInput();
K5VIEWER_Update(false);
}
#endif
static uint8_t mb_remember_boot_state(uint8_t slot, uint8_t bank)
{
gRunningSlot = slot;
gActiveBank = bank;
return bank;
}
uint8_t MB_GetRunningSlot(void)
{
return gRunningSlot;
}
uint8_t MB_GetActiveBank(void)
{
return gActiveBank;
}
static const char *mb_error_text(uint8_t err)
{
switch (err)
{
case MB_OK: return "OK";
case MB_ERR_MAGIC: return "empty";
case MB_ERR_VERSION: return "new header";
case MB_ERR_NOT_COMMITTED: return "incomplete";
case MB_ERR_SIZE: return "bad size";
case MB_ERR_CRC: return "CRC ERROR";
case MB_ERR_SPI: return "SPI ERROR";
case MB_ERR_SLOT: return "bad index";
case MB_ERR_AUTH: return "auth";
case MB_ERR_RAM_LOAD: return "RAM LOAD ERROR";
default: return "error";
}
}
static void mb_copy_label(char *dst, uint8_t cap, const char *src, uint8_t src_cap)
{
uint8_t n = 0;
while (n + 1u < cap && n < src_cap && src[n])
{
dst[n] = src[n];
n++;
}
dst[n] = 0;
}
static uint8_t mb_copy_slot_version(char *dst, uint8_t cap, const mb_slot_header_t *header)
{
uint8_t n = 0;
dst[0] = 0;
for (uint8_t i = 0; i + 1u < MB_VERSION_LEN && header->fw_version[i]; i++)
{
if (header->fw_version[i] == 'v' &&
header->fw_version[i + 1u] >= '0' &&
header->fw_version[i + 1u] <= '9')
{
if (n + 1u < cap)
dst[n++] = 'v';
i++;
while (n + 1u < cap && i < MB_VERSION_LEN)
{
const char c = header->fw_version[i];
if ((c < '0' || c > '9') && c != '.')
break;
dst[n++] = c;
i++;
}
break;
}
}
dst[n] = 0;
return n;
}
/* "F4HWN MULTIBOOT" banner in the top status bar, shown on every screen - the
* same way the firmware puts mode labels there (inverse 3x5 capsule). */
static void mb_status_bar(void)
{
UI_StatusClear();
GUI_DisplaySmallestInverse("F4HWN MULTIBOOT", 34, 0, true, true, 94);
/* Thin line dressing up the otherwise blank row between the status bar and
* the first content row. Drawn on gFrameBuffer[0] (line 0), which every
* multiboot screen leaves blank, so it shows on all of them. Bit 3 (~mid of
* the row) stays clear of the selected-slot capsule's top edge (bit 7). */
for (uint8_t x = 2u; x < LCD_WIDTH - 2u; x++)
gFrameBuffer[0][x] |= 0x08u;
}
/* Bottom key-hint line: each key name as an inverse 3x5 capsule label, its
* action in plain 3x5 text beside it. Drawn on the bottom line
* (gFrameBuffer[6] -> y = 6*8+1 = 49). MENU is pinned to the left and EXIT to
* the right, leaving an airy gap in the middle. "MENU"/"EXIT" are 4 chars
* (16 px); their capsule spans [x-2 .. x+16]. */
static void mb_key_hints(const char *act_menu, const char *act_exit)
{
const uint8_t sp = 6u; /* label <-> action gap */
const uint8_t ae = (uint8_t)strlen(act_exit);
const uint8_t xm = 4u; /* MENU text; capsule at x=2 */
const uint8_t xe = (uint8_t)(124u - ae * 4u - sp - 16u); /* EXIT action ends at x=124 */
GUI_DisplaySmallestInverse("MENU", xm, 6, false, true, (uint8_t)(xm + 16u));
GUI_DisplaySmallest(act_menu, (uint8_t)(xm + 16u + sp), 49, false, true);
GUI_DisplaySmallestInverse("EXIT", xe, 6, false, true, (uint8_t)(xe + 16u));
GUI_DisplaySmallest(act_exit, (uint8_t)(xe + 16u + sp), 49, false, true);
}
/* Fixed selection capsule around the firmware name. The slot index stays in
* the normal font while the version is plain 3x5 metadata. */
#define MB_NAME_BOX_START 12u
#define MB_NAME_BOX_END 96u
#define MB_NAME_TEXT_X 14u
static void mb_invert_name(uint8_t line)
{
gFrameBuffer[line][MB_NAME_BOX_START] ^= 0x7Fu;
for (uint8_t x = MB_NAME_BOX_START + 1u; x < MB_NAME_BOX_END; x++)
{
gFrameBuffer[line][x] ^= 0xFFu;
gFrameBuffer[line - 1u][x] ^= 0x80u;
}
gFrameBuffer[line][MB_NAME_BOX_END] ^= 0x7Fu;
}
static void mb_show_message(const char *line1, const char *line2, const char *line3)
{
UI_DisplayClear();
mb_status_bar();
if (line1) UI_PrintStringSmallNormal(line1, 2, 126, 2);
if (line2) UI_PrintStringSmallNormal(line2, 2, 126, 4);
if (line3) UI_PrintStringSmallNormal(line3, 2, 126, 6);
ST7565_BlitStatusLine();
ST7565_BlitFullScreen();
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
mb_k5viewer_service();
#endif
}
static void mb_wait_release(void)
{
uint8_t stable = 0;
while (stable < 10u)
{
if (!GPIO_IsPttPressed() && KEYBOARD_Poll() == KEY_INVALID)
stable++;
else
stable = 0;
SYSTEM_DelayMs(10);
}
}
static KEY_Code_t mb_get_key(void)
{
for (;;)
{
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
mb_k5viewer_service();
#endif
KEY_Code_t key = KEYBOARD_Poll();
if (key != KEY_INVALID)
{
SYSTEM_DelayMs(30);
if (KEYBOARD_Poll() == key)
{
while (KEYBOARD_Poll() != KEY_INVALID)
SYSTEM_DelayMs(10);
return key;
}
}
SYSTEM_DelayMs(10);
}
}
/* Shown from the normal settings menu (SetCfg), not the boot selector, so it does
* NOT paint the "F4HWN MULTIBOOT" status banner - just a plain acknowledged message. */
void UI_MultibootShowConfigError(uint8_t err)
{
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
gKeyReading0 = KEY_INVALID;
gKeyReading1 = KEY_INVALID;
#endif
UI_DisplayClear();
UI_StatusClear();
UI_PrintStringSmallNormal("CFG ERROR", 2, 126, 2);
UI_PrintStringSmallNormal(mb_error_text(err), 2, 126, 4);
UI_PrintStringSmallNormal("Press any key", 2, 126, 6);
ST7565_BlitStatusLine();
ST7565_BlitFullScreen();
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
mb_k5viewer_service();
#endif
/* The MENU press that confirmed SetCfg may still be down; wait for a clean
* release first so it isn't consumed as the acknowledgement immediately. */
mb_wait_release();
(void)mb_get_key();
}
static void mb_scan_slots(mb_slot_header_t headers[MB_SLOT_COUNT], uint8_t status[MB_SLOT_COUNT])
{
mb_show_message("Scanning slots...", NULL, "Please wait");
for (uint8_t slot = 0; slot < MB_SLOT_COUNT; slot++)
status[slot] = MB_ValidateSlot(slot, &headers[slot], NULL);
}
static void mb_render_slots(uint8_t selected,
const mb_slot_header_t headers[MB_SLOT_COUNT],
const uint8_t status[MB_SLOT_COUNT])
{
char name[13];
char version[8]; /* v + up to six version digits/dots in the 3x5 column. */
UI_DisplayClear();
mb_status_bar();
for (uint8_t slot = 0; slot < MB_SLOT_COUNT; slot++)
{
const uint8_t fbLine = (uint8_t)(slot + 1u); /* page 1 stays blank */
char index[2];
uint8_t version_len = 0;
uint8_t version_x = 0;
memset(name, 0, sizeof(name));
memset(version, 0, sizeof(version));
/* Slot 0 is the auto-backed-up main firmware: label it 'M' (Main) so it
* reads apart from the numbered user slots 1..4. */
index[0] = (slot == 0u) ? 'M' : (char)('0' + slot);
index[1] = '\0';
if (status[slot] == MB_OK)
{
version_len = mb_copy_slot_version(version, sizeof(version), &headers[slot]);
if (version_len)
version_x = (uint8_t)(LCD_WIDTH - 2u - version_len * 4u);
if (headers[slot].name[0])
mb_copy_label(name, sizeof(name), headers[slot].name, MB_NAME_LEN);
else if (!version_len)
mb_copy_label(name, sizeof(name), headers[slot].fw_version, MB_VERSION_LEN);
}
else
mb_copy_label(name, sizeof(name), mb_error_text(status[slot]), 20u);
UI_PrintStringSmallNormal(index, 2u, 0, fbLine);
UI_PrintStringSmallNormal(name, MB_NAME_TEXT_X, 0, fbLine);
if (version_len)
GUI_DisplaySmallest(version, version_x,
(uint8_t)(fbLine * 8u + 1u), false, true);
/* Selected row: fixed rounded inverse capsule around the name only. */
if (slot == selected)
mb_invert_name(fbLine);
}
mb_key_hints("SELECT", "QUIT");
ST7565_BlitStatusLine();
ST7565_BlitFullScreen();
}
static void mb_draw_progress_outline(void)
{
/* Same rounded outline and hatch pattern as the scan progress gauge. */
gFrameBuffer[6][3] = 0x0Cu;
gFrameBuffer[6][4] = 0x12u;
gFrameBuffer[6][123] = 0x12u;
gFrameBuffer[6][124] = 0x0Cu;
for (uint8_t x = 5; x < 123u; x++)
gFrameBuffer[6][x] = 0x21u;
}
/* Full progress frame used while restoring a firmware slot. */
__attribute__((noinline)) static void mb_prepare_progress_screen(const char *title,
const char *detail)
{
UI_DisplayClear();
mb_status_bar();
UI_PrintStringSmallNormal(title, 2, 126, 1);
UI_PrintStringSmallNormal("DO NOT POWER OFF", 2, 126, 3);
UI_PrintStringSmallNormal(detail, 2, 126, 5);
/* Empty gauge that the RAM copier fills as it reflashes. */
mb_draw_progress_outline();
ST7565_BlitStatusLine();
ST7565_BlitFullScreen();
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
mb_k5viewer_service();
#endif
}
static void mb_prepare_progress(uint8_t slot)
{
char slot_title[] = "Restore slot 0";
slot_title[13] = (char)('0' + slot);
const char *title = (slot == 0u) ? "Restore Main" : slot_title;
mb_prepare_progress_screen(title, "Writing / Verify");
}
/* Discreet "Main backup" screen shown once, at the first boot after a normal
* Flash-Firmware install, while the running firmware is copied into slot 0. */
static void mb_backup_prepare(void)
{
UI_DisplayClear();
UI_StatusClear();
UI_PrintStringSmallNormal("Init Main", 2, 126, 1);
UI_PrintStringSmallNormal("DO NOT POWER OFF", 2, 126, 3);
mb_draw_progress_outline();
ST7565_BlitStatusLine();
ST7565_BlitFullScreen();
}
static void mb_backup_progress(uint32_t done, uint32_t total)
{
uint32_t cols = total ? (done * 118u / total) : 118u;
if (cols > 118u)
cols = 118u;
for (uint32_t i = 0; i < cols; i++)
gFrameBuffer[6][5u + i] = 0x2Du;
ST7565_BlitFullScreen();
}
/* With no trustworthy bank, continuing would let normal boot-time settings
* writes modify an arbitrary bank. Keep the radio in a read-only error state;
* a power cycle can recover from a transient SPI fault. */
__attribute__((noreturn)) static void mb_state_error_halt(void)
{
BACKLIGHT_TurnOn();
mb_show_message("STATE ERROR", "Flash state unknown", "Restart radio");
for (;;)
SYSTEM_DelayMs(100);
}
static void mb_confirm_screen(uint8_t slot)
{
char slot_title[] = "Restore slot 0?";
slot_title[13] = (char)('0' + slot);
const char *title = (slot == 0u) ? "Restore Main?" : slot_title;
UI_DisplayClear();
mb_status_bar();
UI_PrintStringSmallNormal(title, 2, 126, 3);
mb_key_hints("CONFIRM", "BACK");
ST7565_BlitStatusLine();
ST7565_BlitFullScreen();
}
void UI_MultibootSelector(void)
{
mb_slot_header_t headers[MB_SLOT_COUNT];
uint8_t status[MB_SLOT_COUNT];
uint8_t selected = 0;
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
/* The selector is entered from a boot key event. Clear that stale key so
* K5Viewer is allowed to publish the first selector frame immediately. */
gKeyReading0 = KEY_INVALID;
gKeyReading1 = KEY_INVALID;
#endif
/* Clear + blit the LCD BEFORE the backlight comes on, otherwise it reveals
* the random power-on contents of the display RAM for a moment. */
mb_show_message("Release keys", NULL, NULL);
BACKLIGHT_TurnOn();
mb_wait_release();
mb_scan_slots(headers, status);
/* Pre-select the exact firmware slot resolved at boot, independently of the
* active config bank (SetCfg can point the bank elsewhere), so the cursor
* lands on "where you are". An unknown or now-invalid slot falls back to the
* first valid slot below. */
selected = MB_GetRunningSlot();
if (selected >= MB_SLOT_COUNT || status[selected] != MB_OK)
{
for (uint8_t slot = 0; slot < MB_SLOT_COUNT; slot++)
{
if (status[slot] == MB_OK)
{
selected = slot;
break;
}
}
}
for (;;)
{
mb_render_slots(selected, headers, status);
KEY_Code_t key = mb_get_key();
if (key == KEY_EXIT)
{
/* MENU was latched by BOOT_GetMode(). Do not let that stale boot
* key reach the normal application after leaving the selector. */
gKeyReading0 = KEY_INVALID;
gKeyReading1 = KEY_INVALID;
gDebounceCounter = 0;
return;
}
if (key == KEY_UP)
{
selected = (uint8_t)((selected + MB_SLOT_COUNT - 1u) % MB_SLOT_COUNT);
continue;
}
if (key == KEY_DOWN)
{
selected = (uint8_t)((selected + 1u) % MB_SLOT_COUNT);
continue;
}
if (key != KEY_MENU)
continue;
if (status[selected] != MB_OK)
{
mb_show_message("SLOT NOT VALID", mb_error_text(status[selected]), "Press any key");
(void)mb_get_key();
continue;
}
mb_confirm_screen(selected);
key = mb_get_key();
if (key != KEY_MENU)
continue;
/* Bind this slot to its own settings bank BEFORE reflashing. The
* write is verified (read-back); if it can't be confirmed we must NOT
* reflash - otherwise the next boot could resolve to the wrong bank
* (e.g. when two slots hold the same firmware image). */
if (MB_SetActiveSlot(selected) != MB_OK)
{
mb_show_message("STATE ERROR", "Marker not saved", "Press any key");
(void)mb_get_key();
continue;
}
mb_prepare_progress(selected);
uint8_t err = MB_RestoreSlot(selected, gFrameBuffer[6]);
/* Only reached when the final pre-erase validation refused the slot. */
status[selected] = err;
mb_show_message("RESTORE REFUSED", mb_error_text(err), "Press any key");
(void)mb_get_key();
mb_scan_slots(headers, status);
}
}
/* Adopt the running internal firmware as Main: back it up into slot 0 and point
* the marker at bank 0. Reached when the firmware was installed outside
* multiboot (fresh radio, or a plain Flash-Firmware). */
static uint8_t mb_adopt_internal_as_main(void)
{
mb_backup_prepare();
BACKLIGHT_TurnOn();
if (MB_BackupInternalToSlot0(mb_backup_progress) == MB_OK)
(void)MB_SetActiveSlot(MB_SLOT_BACKUP);
return MB_SLOT_BACKUP;
}
uint8_t MB_BootResolveState(void)
{
mb_state_t mark;
mb_mark_status_t ms = MB_MARK_IO;
for (uint8_t retry = 0; retry < 3u && ms == MB_MARK_IO; retry++)
{
ms = MB_ReadActiveState(&mark);
if (ms == MB_MARK_IO)
SYSTEM_DelayMs(10);
}
/* A reliably-read marker is authoritative: it carries the expected internal
* identity, so we don't even need the slot header. */
if (ms == MB_MARK_VALID)
{
if (MB_InternalMatchesState(&mark))
return mb_remember_boot_state(mark.firmware_slot, mark.config_bank);
/* Marker read fine but internal no longer carries its identity -> the
* firmware was replaced outside multiboot (a plain Flash-Firmware). Adopt
* it as Main. Deliberately NOT a content scan here: a build that merely
* duplicates a user slot (or a marker that already points at such a slot)
* must still refresh Main. */
return mb_remember_boot_state(mb_adopt_internal_as_main(), MB_SLOT_BACKUP);
}
/* Marker unreliable (MISSING / LEGACY / CORRUPT / IO): identify the running
* firmware by content, and never destroy Main on uncertainty - internal is
* adopted only when it matches no slot AND every read was clean, so a
* transient SPI error or a half-written marker can never destroy Main. */
/* An FMP1 record still names a coupled slot/bank; honour it before the
* content scan so a duplicate image in a lower slot cannot hijack the
* migration. Falls through to the scan below on mismatch or IO. */
if (ms == MB_MARK_LEGACY && mark.firmware_slot < MB_SLOT_COUNT)
{
mb_fw_match_t m = MB_FW_IO;
for (uint8_t retry = 0; retry < 3u && m == MB_FW_IO; retry++)
m = MB_InternalMatchesSlot(mark.firmware_slot);
if (m == MB_FW_MATCH)
{
(void)MB_SetActiveSlot(mark.firmware_slot);
return mb_remember_boot_state(mark.firmware_slot, mark.config_bank);
}
}
bool had_io = false;
for (uint8_t slot = 0; slot < MB_SLOT_COUNT; slot++)
{
mb_fw_match_t m = MB_FW_IO;
for (uint8_t retry = 0; retry < 3u && m == MB_FW_IO; retry++)
m = MB_InternalMatchesSlot(slot);
if (m == MB_FW_MATCH)
{
(void)MB_SetActiveSlot(slot); /* record/repair the marker */
return mb_remember_boot_state(slot, slot);
}
if (m == MB_FW_IO)
had_io = true;
}
if (had_io || ms == MB_MARK_IO || ms == MB_MARK_CORRUPT)
{
/* Halt to protect an existing Main while the flash state is uncertain;
* but if slot 0 holds no valid backup there is nothing to protect, so
* fall through and adopt instead of bricking a first boot. */
uint8_t main_status = MB_SlotInfo(MB_SLOT_BACKUP, NULL);
bool main_exists = (main_status != MB_ERR_MAGIC &&
main_status != MB_ERR_NOT_COMMITTED);
if (main_exists)
mb_state_error_halt();
}
return mb_remember_boot_state(mb_adopt_internal_as_main(), MB_SLOT_BACKUP);
}
+42
View File
@@ -0,0 +1,42 @@
/* Copyright 2026 Armel F4HWN
* https://github.com/armel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef UI_MULTIBOOT_H
#define UI_MULTIBOOT_H
#include <stdint.h>
/* Blocking boot-time slot selector. Returns only when the user chooses EXIT;
* a successful restore resets the radio from the RAM-resident copier. */
void UI_MultibootSelector(void);
/* Show a blocking, acknowledged error screen for a failed SetCfg operation. */
void UI_MultibootShowConfigError(uint8_t err);
/* Resolve the active config bank at boot, BEFORE any settings read. Detects a
* firmware installed outside multiboot (internal identity != marker) and, if so,
* discreetly self-backs it up into slot 0 and adopts slot 0 / bank 0. Returns the
* config bank (0..MB_BANK_COUNT-1) to feed PY25Q16_SetBankBase(); the exact
* firmware slot and bank are cached too (see MB_GetRunningSlot/MB_GetActiveBank). */
uint8_t MB_BootResolveState(void);
/* Exact firmware slot and active config bank resolved for this session. Both
* are cached in RAM so UI callers never have to reread the marker or scan slot
* headers merely to render their state. */
uint8_t MB_GetRunningSlot(void);
uint8_t MB_GetActiveBank(void);
#endif
Loaded 100 of 115 files, more files were not shown because too many files have changed in this diff. Show more