diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 924a63af..026be6e9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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 diff --git a/.gitignore b/.gitignore index d6d64b8f..865848f0 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/App/CMakeLists.txt b/App/CMakeLists.txt index 3cddbf7f..44f71d8f 100644 --- a/App/CMakeLists.txt +++ b/App/CMakeLists.txt @@ -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) diff --git a/App/am_fix.c b/App/am_fix.c deleted file mode 100644 index 3c4c9e45..00000000 --- a/App/am_fix.c +++ /dev/null @@ -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 - -#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 diff --git a/App/app/action.c b/App/app/action.c index 2428b965..48c142e9 100644 --- a/App/app/action.c +++ b/App/app/action.c @@ -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; diff --git a/App/app/action.h b/App/app/action.h index 2684d0e9..6c01465f 100644 --- a/App/app/action.h +++ b/App/app/action.h @@ -17,6 +17,9 @@ #ifndef APP_ACTION_H #define APP_ACTION_H +#include +#include + #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 diff --git a/App/app/aircopy.c b/App/app/aircopy.c index b9790346..7c2fadc6 100644 --- a/App/app/aircopy.c +++ b/App/app/aircopy.c @@ -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 +#include #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 \ No newline at end of file +#endif diff --git a/App/app/aircopy.h b/App/app/aircopy.h index f84f22d0..1092bbc0 100644 --- a/App/app/aircopy.h +++ b/App/app/aircopy.h @@ -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. diff --git a/App/app/app.c b/App/app/app.c index 5165646f..7946677f 100644 --- a/App/app/app.c +++ b/App/app/app.c @@ -18,7 +18,6 @@ #include #include -#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) diff --git a/App/app/app.h b/App/app/app.h index 602dcc1c..63b520aa 100644 --- a/App/app/app.h +++ b/App/app/app.h @@ -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 diff --git a/App/app/beam.c b/App/app/beam.c index 0f024757..ae4b3190 100644 --- a/App/app/beam.c +++ b/App/app/beam.c @@ -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(); } diff --git a/App/app/beam.h b/App/app/beam.h index 6571119e..ca9aafa4 100644 --- a/App/app/beam.h +++ b/App/app/beam.h @@ -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); diff --git a/App/app/breakout.c b/App/app/breakout.c index ea0be553..667f6247 100644 --- a/App/app/breakout.c +++ b/App/app/breakout.c @@ -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); diff --git a/App/app/breakout.h b/App/app/breakout.h index 3f71b194..a11dbbdd 100644 --- a/App/app/breakout.h +++ b/App/app/breakout.h @@ -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; diff --git a/App/app/chFrScanner.c b/App/app/chFrScanner.c index c1a25996..d663049d 100644 --- a/App/app/chFrScanner.c +++ b/App/app/chFrScanner.c @@ -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) diff --git a/App/app/dtmf.c b/App/app/dtmf.c index 64dd0477..138103af 100644 --- a/App/app/dtmf.c +++ b/App/app/dtmf.c @@ -18,7 +18,7 @@ #include // 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(); diff --git a/App/app/fm.c b/App/app/fm.c index 78f5c9a1..442f05ea 100644 --- a/App/app/fm.c +++ b/App/app/fm.c @@ -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 diff --git a/App/app/fm.h b/App/app/fm.h index 6a315a30..1578a2e7 100644 --- a/App/app/fm.h +++ b/App/app/fm.h @@ -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 diff --git a/App/app/foxhunt.c b/App/app/foxhunt.c index 1563e8da..9d08c0c6 100644 --- a/App/app/foxhunt.c +++ b/App/app/foxhunt.c @@ -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 " 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 diff --git a/App/app/foxhunt.h b/App/app/foxhunt.h index 3743e6b1..a973d319 100644 --- a/App/app/foxhunt.h +++ b/App/app/foxhunt.h @@ -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 #include #include +#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 diff --git a/App/app/generic.c b/App/app/generic.c index f6c907a2..f9e3ff0f 100644 --- a/App/app/generic.c +++ b/App/app/generic.c @@ -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 ) { diff --git a/App/app/main.c b/App/app/main.c index b44bd4d7..29fc3c5d 100644 --- a/App/app/main.c +++ b/App/app/main.c @@ -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; diff --git a/App/app/menu.c b/App/app/menu.c index 37c084da..0b80664b 100644 --- a/App/app/menu.c +++ b/App/app/menu.c @@ -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; diff --git a/App/app/spectrum.c b/App/app/spectrum.c index f86cb8a1..0661dc13 100644 --- a/App/app/spectrum.c +++ b/App/app/spectrum.c @@ -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(); } diff --git a/App/app/uart.c b/App/app/uart.c index 5c052430..d44d58d9 100644 --- a/App/app/uart.c +++ b/App/app/uart.c @@ -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); diff --git a/App/apps/app_api.h b/App/apps/app_api.h new file mode 100644 index 00000000..58a0de21 --- /dev/null +++ b/App/apps/app_api.h @@ -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 +#include + +/* 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 */ diff --git a/App/apps/app_menu.c b/App/apps/app_menu.c new file mode 100644 index 00000000..24f86eed --- /dev/null +++ b/App/apps/app_menu.c @@ -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 +#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 */ diff --git a/App/am_fix.h b/App/apps/app_menu.h similarity index 56% rename from App/am_fix.h rename to App/apps/app_menu.h index 3667301f..da49c21c 100644 --- a/App/am_fix.h +++ b/App/apps/app_menu.h @@ -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 -#include +/* 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 */ diff --git a/App/apps/app_overlay.c b/App/apps/app_overlay.c new file mode 100644 index 00000000..444aa1ad --- /dev/null +++ b/App/apps/app_overlay.c @@ -0,0 +1,1241 @@ +/* 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 +#include /* offsetof */ +#include "py32f0xx.h" + +#include "driver/bk4819.h" +#include "driver/bk4819-regs.h" +#ifdef ENABLE_FMRADIO +#include "driver/bk1080.h" +#include "app/fm.h" +#endif +#include "driver/keyboard.h" +#include "driver/mb_flash.h" +#include "driver/py25q16.h" +#include "driver/st7565.h" +#include "driver/system.h" +#include "driver/backlight.h" +#ifdef ENABLE_FEAT_F4HWN_K5VIEWER +#include "k5viewer.h" +#endif +#include "app/app.h" +#include "ui/helper.h" +#include "ui/main.h" +#include "ui/status.h" +#include "board.h" +#include "audio.h" +#include "dcs.h" +#include "functions.h" +#include "frequencies.h" +#include "radio.h" +#include "helper/battery.h" +#include "settings.h" +#include "misc.h" /* dBmCorrTable */ + +_Static_assert(sizeof(app_header_t) == 64, "app_header_t must be 64 bytes"); +_Static_assert(sizeof(app_api_t) <= UINT16_MAX, "app_api_t size field overflow"); + +enum { + APP_AVAILABLE_CAPS = 0u +#ifdef ENABLE_FMRADIO + | APP_CAP_FM +#endif +#ifdef ENABLE_FEAT_F4HWN_OVERLAY_TRIVFO + | APP_CAP_TRIVFO +#endif +#ifdef ENABLE_FEAT_F4HWN_OVERLAY_BEAM + | APP_CAP_BEAM +#endif +}; + +/* ---- ABI wrappers: the few resident calls that are not a direct signature match ---- */ +static bool app_allow_screen_saver; +static bool app_screen_saver_wake; + +static void app_backlight_on(void) +{ + APP_ModalScreenSaverExit(); + BACKLIGHT_TurnOn(); +} + +static void app_backlight_update(void) +{ + APP_ModalBacklightTick(app_allow_screen_saver); +} + +static uint8_t app_get_key(void) +{ +#ifdef ENABLE_FEAT_F4HWN_K5VIEWER + /* Overlay apps run synchronously outside APP_Update(). Keep serial key + * injection alive while an app owns the foreground loop. */ + K5VIEWER_ParseInput(); +#endif + const KEY_Code_t key = KEYBOARD_GetKey(); + + if (app_screen_saver_wake) { + if (key == KEY_INVALID) + app_screen_saver_wake = false; + return APP_KEY_INVALID; + } + + if (!APP_IsScreenSaverDisplayed()) { + if (key != KEY_INVALID) + BACKLIGHT_TurnOn(); /* re-arm BLTime on activity, mirrors ProcessKey(): + overlay apps bypass the resident key handler, so + the saver would otherwise fire mid-use. */ + return (uint8_t)key; + } + + if (key == KEY_INVALID) + return APP_KEY_SAVER; + + app_backlight_on(); + if (key == KEY_PTT) + return APP_KEY_PTT; + + app_screen_saver_wake = true; + return APP_KEY_WAKE; +} + +#ifdef ENABLE_FEAT_F4HWN_K5VIEWER +static void app_blit_full(void) +{ + ST7565_BlitFullScreen(); + /* The normal loop mirrors completed frames after drawing. Overlay apps + * bypass that loop, so publish the frame from this ABI wrapper. */ + K5VIEWER_Update(false); +} +#endif + +static int8_t app_nav_dir(uint8_t key) +{ + int8_t direction; + + if (key == KEY_UP) + direction = 1; + else if (key == KEY_DOWN) + direction = -1; + else + return 0; + + return gEeprom.SET_NAV ? direction : -direction; +} +static void app_led(bool on) { BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, on); } + +static void app_play_tone(uint16_t tone, uint16_t ms) +{ + BK4819_PrepareToPlayTone(true); + AUDIO_AudioPathOn(); + BK4819_PlayToneRaw(tone, ms); + AUDIO_AudioPathOff(); +} + +#ifdef ENABLE_FEAT_F4HWN_OVERLAY_TRIVFO +/* ---- optional resident triple-VFO engine -------------------------------- + * The overlay owns the UI and key timing, while this resident engine owns all + * radio details. Keeping VFO_Info_t and BK4819 sequencing on this side makes + * the app independent of feature-dependent firmware layouts. */ +#define APP_TRIVFO_COUNT 3u +#define APP_TRIVFO_TUNE_TICKS 5u /* 100 ms at one tick / 20 ms */ +#define APP_TRIVFO_TX_HOLD_TICKS 125u /* keep the existing 2.5 s TX return */ + +static VFO_Info_t app_trivfo_c; +static VFO_Info_t *app_trivfo_saved_rx; +static VFO_Info_t *app_trivfo_saved_tx; +static VFO_Info_t *app_trivfo_saved_current; +static uint8_t app_trivfo_selected; +static uint8_t app_trivfo_pre_rx_selected; +static uint8_t app_trivfo_current; +static uint8_t app_trivfo_settle; +static uint16_t app_trivfo_hold; +static uint8_t app_trivfo_candidate_wait; +static uint32_t app_trivfo_tx_ticks; +static bool app_trivfo_running; +static bool app_trivfo_receiving; +static bool app_trivfo_transmitting; +static bool app_trivfo_sql_open; +static bool app_trivfo_ctcss_ok; +static bool app_trivfo_cdcss_ok; +static bool app_trivfo_ab_dirty; +static bool app_trivfo_restore_selection; +static bool app_trivfo_onepush_stop_armed; +static uint8_t app_trivfo_freq_dirty; + +static void app_trivfo_end_tx(void); + +static VFO_Info_t *app_trivfo_vfo(uint8_t vfo) +{ + return vfo < 2u ? &gEeprom.VfoInfo[vfo] : &app_trivfo_c; +} + +static bool app_trivfo_load_memory(VFO_Info_t *vfo, uint16_t channel) +{ + ChannelScanDisplayInfo_t info; + if (!IS_MR_CHANNEL(channel) || + !SETTINGS_FetchChannelScanDisplayInfo(channel, &info)) + return false; + + RADIO_InitInfo(vfo, channel, info.rx.Frequency); + vfo->freq_config_RX = info.rx; + vfo->freq_config_TX = info.tx; + vfo->TX_OFFSET_FREQUENCY = info.offset; + vfo->StepFrequency = info.stepFrequency; + vfo->STEP_SETTING = info.stepSetting; + vfo->Modulation = info.modulation; + vfo->TX_OFFSET_FREQUENCY_DIRECTION = info.txOffsetFrequencyDirection; + vfo->OUTPUT_POWER = info.outputPower; + vfo->FrequencyReverse = info.frequencyReverse; + vfo->CHANNEL_BANDWIDTH = info.channelBandwidth; + vfo->BUSY_CHANNEL_LOCK = info.busyChannelLock; + vfo->TX_LOCK = info.txLock; +#ifdef ENABLE_DTMF_CALLING + vfo->DTMF_DECODING_ENABLE = info.dtmfDecodingEnable; +#endif + vfo->DTMF_PTT_ID_TX_MODE = info.dtmfPttIdTxMode; + vfo->Band = FREQUENCY_GetBand(vfo->freq_config_RX.Frequency); + vfo->Compander = MR_GetChannelAttributes(channel)->compander; + SETTINGS_FetchChannelName(vfo->Name, channel); + vfo->pRX = vfo->FrequencyReverse ? &vfo->freq_config_TX : &vfo->freq_config_RX; + vfo->pTX = vfo->FrequencyReverse ? &vfo->freq_config_RX : &vfo->freq_config_TX; + RADIO_ConfigureSquelchAndOutputPower(vfo); + return true; +} + +static uint16_t app_trivfo_next_channel(uint16_t channel, int8_t direction, uint8_t vfo) +{ + if (direction == 0) + direction = 1; + channel = RADIO_FindNextChannel((uint16_t)(channel + direction), direction, + false, vfo < 2u ? vfo : 0u); + return channel; +} + +static void app_trivfo_tune(uint8_t vfo) +{ + app_trivfo_current = vfo % APP_TRIVFO_COUNT; + gRxVfo = app_trivfo_vfo(app_trivfo_current); + gCurrentVfo = gRxVfo; + app_trivfo_receiving = false; + app_trivfo_sql_open = false; + app_trivfo_ctcss_ok = false; + app_trivfo_cdcss_ok = false; + app_trivfo_candidate_wait = 0; + app_trivfo_settle = APP_TRIVFO_TUNE_TICKS; + AUDIO_AudioPathOff(); + gEnableSpeaker = false; + RADIO_SetupRegisters(false); + FUNCTION_Init(); +} + +static void app_trivfo_poll_irq(void) +{ + while (BK4819_ReadRegister(BK4819_REG_0C) & 1u) { + BK4819_WriteRegister(BK4819_REG_02, 0); + const uint16_t irq = BK4819_ReadRegister(BK4819_REG_02); + if (irq & BK4819_REG_02_SQUELCH_LOST) app_trivfo_sql_open = true; + if (irq & BK4819_REG_02_SQUELCH_FOUND) { + app_trivfo_sql_open = false; + app_trivfo_ctcss_ok = false; + app_trivfo_cdcss_ok = false; + } + + /* A BK4819 CSS interrupt is a transition, not a persistent level. + * Depending on the silicon revision and the configured polarity, the + * first transition can be reported as FOUND or LOST. MAIN keeps the + * decoder state across both transitions; do the same here and use any + * CSS transition as proof that the configured decoder has acquired the + * signal. A wrong CTCSS/DCS does not generate either transition. */ + if (irq & (BK4819_REG_02_CTCSS_LOST | BK4819_REG_02_CTCSS_FOUND)) + app_trivfo_ctcss_ok = true; + if (irq & (BK4819_REG_02_CDCSS_LOST | BK4819_REG_02_CDCSS_FOUND)) + app_trivfo_cdcss_ok = true; + } +} + +static bool app_trivfo_qualified(void) +{ + const VFO_Info_t *vfo = app_trivfo_vfo(app_trivfo_current); + if (!app_trivfo_sql_open) + return false; + if (vfo->Modulation != MODULATION_FM || vfo->pRX->CodeType == CODE_TYPE_OFF) + return true; + if (vfo->pRX->CodeType == CODE_TYPE_CONTINUOUS_TONE) + return app_trivfo_ctcss_ok; + return app_trivfo_cdcss_ok; +} + +static uint16_t app_trivfo_enter(uint16_t c_channel) +{ + app_trivfo_saved_rx = gRxVfo; + app_trivfo_saved_tx = gTxVfo; + app_trivfo_saved_current = gCurrentVfo; + + if (!IS_MR_CHANNEL(c_channel) || + !SETTINGS_FetchChannelScanInfo(c_channel, NULL, NULL)) { + uint16_t start = IS_MR_CHANNEL(gEeprom.ScreenChannel[1]) + ? gEeprom.ScreenChannel[1] : gEeprom.MrChannel[1]; + c_channel = app_trivfo_next_channel(start, 1, 2); + } + if (c_channel == 0xFFFFu || !app_trivfo_load_memory(&app_trivfo_c, c_channel)) { + c_channel = RADIO_FindNextChannel(MR_CHANNEL_FIRST, RADIO_CHANNEL_UP, false, 0); + if (c_channel != 0xFFFFu) + app_trivfo_load_memory(&app_trivfo_c, c_channel); + } + + const uint8_t initial_vfo = gEeprom.TX_VFO < 2u ? gEeprom.TX_VFO : 0u; + app_trivfo_selected = initial_vfo; + app_trivfo_pre_rx_selected = initial_vfo; + app_trivfo_restore_selection = false; + app_trivfo_hold = 0; + app_trivfo_running = true; + app_trivfo_transmitting = false; + app_trivfo_onepush_stop_armed = false; + app_trivfo_ab_dirty = false; + app_trivfo_freq_dirty = 0; + app_trivfo_tune(initial_vfo); + return c_channel; +} + +static void app_trivfo_leave(void) +{ + if (!app_trivfo_running) + return; + if (app_trivfo_transmitting) + app_trivfo_end_tx(); + AUDIO_AudioPathOff(); + gEnableSpeaker = false; + app_trivfo_running = false; + app_trivfo_transmitting = false; + gRxVfo = app_trivfo_saved_rx; + gTxVfo = app_trivfo_saved_tx; + gCurrentVfo = app_trivfo_saved_current; +} + +static void app_trivfo_get(uint8_t index, app_trivfo_info_t *info) +{ + if (info == NULL || index >= APP_TRIVFO_COUNT) + return; + const VFO_Info_t *vfo = app_trivfo_vfo(index); + memset(info, 0, sizeof(*info)); + info->frequency = vfo->pRX->Frequency; + info->channel = vfo->CHANNEL_SAVE; + info->step = vfo->StepFrequency; + if (vfo->pRX->CodeType == CODE_TYPE_CONTINUOUS_TONE) + info->code_value = CTCSS_Options[vfo->pRX->Code]; + else if (vfo->pRX->CodeType == CODE_TYPE_DIGITAL || + vfo->pRX->CodeType == CODE_TYPE_REVERSE_DIGITAL) + info->code_value = DCS_Options[vfo->pRX->Code]; + info->rssi_dbm = (index == app_trivfo_current) + ? BK4819_GetRSSI_dBm() + dBmCorrTable[vfo->Band] : -160; + info->modulation = vfo->Modulation; + info->power = vfo->OUTPUT_POWER == OUTPUT_POWER_USER + ? (uint8_t)(gSetting_set_pwr + 1u) : vfo->OUTPUT_POWER; + info->bandwidth = vfo->CHANNEL_BANDWIDTH; +#ifdef ENABLE_FEAT_F4HWN_NARROWER + if (info->bandwidth == BANDWIDTH_NARROW && gSetting_set_nfm == 1) + info->bandwidth++; +#endif + info->code_type = vfo->pRX->CodeType; + info->code = vfo->pRX->Code; + info->offset_direction = (vfo->freq_config_RX.Frequency != vfo->freq_config_TX.Frequency) + ? vfo->TX_OFFSET_FREQUENCY_DIRECTION : 0u; + info->reverse = vfo->FrequencyReverse; + info->squelch = gEeprom.SQUELCH_LEVEL; + if (index == app_trivfo_selected) info->flags |= APP_TRIVFO_SELECTED; + if (index == app_trivfo_current) info->flags |= APP_TRIVFO_TUNED; + if (index == app_trivfo_current && app_trivfo_receiving) info->flags |= APP_TRIVFO_RECEIVING; + if (index == app_trivfo_selected && app_trivfo_transmitting) info->flags |= APP_TRIVFO_TX; + if (vfo->OUTPUT_POWER == OUTPUT_POWER_USER) info->flags |= APP_TRIVFO_USER_POWER; +#ifdef ENABLE_AUDIO_BAR + if (gSetting_mic_bar) info->flags |= APP_TRIVFO_AUDIO_BAR; +#endif + if (gSetting_set_gui) info->flags |= APP_TRIVFO_GUI_CLASSIC; + if (gSetting_set_ptt_session) info->flags |= APP_TRIVFO_PTT_ONEPUSH; + if (IS_MR_CHANNEL(vfo->CHANNEL_SAVE)) + memcpy(info->name, vfo->Name, sizeof(info->name) - 1u); +} + +static void app_trivfo_select(uint8_t vfo) +{ + if (vfo < APP_TRIVFO_COUNT) + app_trivfo_selected = vfo; +} + +static uint16_t app_trivfo_step(uint8_t index, int8_t direction) +{ + if (index >= APP_TRIVFO_COUNT || app_trivfo_transmitting) + return 0xFFFFu; + VFO_Info_t *vfo = app_trivfo_vfo(index); + + if (IS_FREQ_CHANNEL(vfo->CHANNEL_SAVE)) { + if (direction == 0) + direction = 1; + const uint32_t frequency = APP_SetFrequencyByStep(vfo, direction); + if (RX_freq_check(frequency) < 0) + return 0xFFFFu; + + vfo->freq_config_RX.Frequency = frequency; + RADIO_ApplyOffset(vfo); + RADIO_ConfigureSquelchAndOutputPower(vfo); + if (index < 2u) + app_trivfo_freq_dirty |= (uint8_t)(1u << index); + app_trivfo_hold = 0; + app_trivfo_tune(index); + return vfo->CHANNEL_SAVE; + } + + uint16_t base = IS_MR_CHANNEL(vfo->CHANNEL_SAVE) ? vfo->CHANNEL_SAVE + : (index < 2u ? gEeprom.MrChannel[index] : gEeprom.MrChannel[1]); + const uint16_t channel = app_trivfo_next_channel(base, direction, index); + if (channel == 0xFFFFu) + return channel; + if (index < 2u) { + gEeprom.ScreenChannel[index] = channel; + gEeprom.MrChannel[index] = channel; + RADIO_ConfigureChannel(index, VFO_CONFIGURE_RELOAD); + app_trivfo_ab_dirty = true; + } else { + app_trivfo_load_memory(&app_trivfo_c, channel); + } + app_trivfo_hold = 0; + app_trivfo_tune(index); + return channel; +} + +static void app_trivfo_end_tx(void) +{ + if (!app_trivfo_transmitting) + return; + RADIO_SendEndOfTransmission(); + app_trivfo_transmitting = false; + app_trivfo_onepush_stop_armed = false; + BK4819_ToggleGpioOut(BK4819_GPIO5_PIN1_RED, false); + app_trivfo_hold = APP_TRIVFO_TX_HOLD_TICKS; + app_trivfo_tune(app_trivfo_selected); +} + +static uint8_t app_trivfo_tick(void) +{ + if (!app_trivfo_running) + return APP_TRIVFO_SCAN; + if (app_trivfo_transmitting) { + const uint32_t timeout = ((uint32_t)gEeprom.TX_TIMEOUT_TIMER + 1u) * 250u; + if (++app_trivfo_tx_ticks >= timeout) { + app_trivfo_end_tx(); + return APP_TRIVFO_HOLD; + } + return APP_TRIVFO_TX_STATE; + } + + app_trivfo_poll_irq(); + if (app_trivfo_settle > 0) { + app_trivfo_settle--; + return APP_TRIVFO_SCAN; + } + + const bool qualified = app_trivfo_qualified(); + if (qualified) { + app_trivfo_hold = 0; + if (!app_trivfo_receiving) { + if (app_trivfo_selected != app_trivfo_current) { + app_trivfo_pre_rx_selected = app_trivfo_selected; + app_trivfo_restore_selection = true; + app_trivfo_selected = app_trivfo_current; + } + app_trivfo_receiving = true; + AUDIO_AudioPathOn(); + gEnableSpeaker = true; + BK4819_SetRxAudioGain(); + /* BK4819_SetupSquelch() ends by selecting AF_MUTE. Mirror + * APP_StartListening(): restore the channel demodulator only once + * the carrier/CSS has qualified. */ + RADIO_SetModulation(gRxVfo->Modulation); + BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, true); + } + return APP_TRIVFO_RX; + } + + if (app_trivfo_receiving) { + app_trivfo_receiving = false; + AUDIO_AudioPathOff(); + gEnableSpeaker = false; + BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, false); + /* Use the same receive-response dwell as resident DWR. The resident + * value is expressed in 10 ms ticks; Triple VFO ticks every 20 ms. */ + app_trivfo_hold = (dual_watch_count_after_2_10ms + 1u) / 2u; + app_trivfo_candidate_wait = 50u; + } + if (app_trivfo_hold > 0) { + app_trivfo_hold--; + if (app_trivfo_hold == 0 && app_trivfo_restore_selection) { + app_trivfo_selected = app_trivfo_pre_rx_selected; + app_trivfo_restore_selection = false; + } + return APP_TRIVFO_HOLD; + } + + /* Like resident dual watch, give a coded carrier time to acquire its CSS. + * A wrong tone/code must not monopolise the receiver indefinitely. */ + if (app_trivfo_sql_open && app_trivfo_candidate_wait < 50u) { + app_trivfo_candidate_wait++; + return APP_TRIVFO_HOLD; + } + + app_trivfo_tune((uint8_t)((app_trivfo_current + 1u) % APP_TRIVFO_COUNT)); + return APP_TRIVFO_SCAN; +} + +static uint8_t app_trivfo_ptt(bool pressed) +{ + if (!app_trivfo_running) + return 1; + if (!pressed) { + if (!app_trivfo_transmitting) + return 0; + /* ONEPUSH mirrors the resident PTT sequence: the first release keeps + * TX keyed; the release following the second press ends TX. */ + if (gSetting_set_ptt_session && !app_trivfo_onepush_stop_armed) + return 0; + app_trivfo_end_tx(); + return 0; + } + if (app_trivfo_transmitting) { + if (gSetting_set_ptt_session) + app_trivfo_onepush_stop_armed = true; + return 0; + } + + VFO_Info_t *vfo = app_trivfo_vfo(app_trivfo_selected); + if ((TX_freq_check(vfo->pTX->Frequency) != 0 && vfo->TX_LOCK) || + vfo->Modulation != MODULATION_FM || + (vfo->BUSY_CHANNEL_LOCK && app_trivfo_receiving) || + gBatteryDisplayLevel == 0 || gBatteryDisplayLevel > 6) + return 1; + + AUDIO_AudioPathOff(); + gEnableSpeaker = false; + BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, false); + gRxVfo = gTxVfo = gCurrentVfo = vfo; + RADIO_SetTxParameters(); + BK4819_ToggleGpioOut(BK4819_GPIO5_PIN1_RED, true); + BK4819_DisableScramble(); + app_trivfo_current = app_trivfo_selected; + app_trivfo_receiving = false; + app_trivfo_transmitting = true; + app_trivfo_onepush_stop_armed = false; + app_trivfo_tx_ticks = 0; + return 0; +} +#endif + +#ifdef ENABLE_FEAT_F4HWN_OVERLAY_BEAM +/* ---- optional BEAM radio/channel bridge ----------------------------------- + * The modal app owns the packet format, CRC, UI and state machine. Resident + * code only translates the stable ABI channel structure and performs the FSK + * operations which depend on VFO_Info_t and the BK4819 driver. */ +static VFO_Info_t app_beam_vfo; +static app_beam_channel_t app_beam_pending; +static uint8_t app_beam_fsk_index; +static uint16_t app_beam_pending_channel; +static bool app_beam_dirty; + +static void app_beam_prepare(void) +{ + const uint16_t channel = FREQ_CHANNEL_FIRST + BAND6_400MHz; + RADIO_InitInfo(&app_beam_vfo, channel, DEFAULT_FREQ); + app_beam_vfo.CHANNEL_BANDWIDTH = BANDWIDTH_NARROW; + app_beam_vfo.OUTPUT_POWER = OUTPUT_POWER_LOW1; + RADIO_ConfigureSquelchAndOutputPower(&app_beam_vfo); + + gRxVfo = &app_beam_vfo; + gTxVfo = &app_beam_vfo; + gCurrentVfo = &app_beam_vfo; + RADIO_SetupRegisters(true); + BK4819_SetupAircopy(); + BK4819_ResetFSK(); + app_beam_fsk_index = 0; +} + +static void app_beam_leave(void) +{ + BK4819_ResetFSK(); +} + +/* Wire<->VFO fields that are a plain one-byte copy in BOTH directions. Fields + * that differ in width (frequency, offset, band) or are enum-typed on the VFO + * side (modulation, code types, PTT-id) are excluded: enums are int-sized here + * (no -fshort-enums), so a byte copy would truncate them. Those stay as the + * explicit width-converting assignments below. Driving the byte fields from one + * table collapses two near-identical copy blocks into a single shared loop. */ +#ifdef ENABLE_DTMF_CALLING +#define APP_BEAM_BYTE_FIELDS_DTMF(F) F(dtmf_decoding_enable, DTMF_DECODING_ENABLE) +#else +#define APP_BEAM_BYTE_FIELDS_DTMF(F) +#endif +#define APP_BEAM_BYTE_FIELDS(F) \ + F(rx_code, freq_config_RX.Code) \ + F(tx_code, freq_config_TX.Code) \ + F(tx_offset_direction, TX_OFFSET_FREQUENCY_DIRECTION) \ + F(tx_lock, TX_LOCK) \ + F(busy_channel_lock, BUSY_CHANNEL_LOCK) \ + F(output_power, OUTPUT_POWER) \ + F(channel_bandwidth, CHANNEL_BANDWIDTH) \ + F(scanlist, SCANLIST_PARTICIPATION) \ + F(compander, Compander) \ + APP_BEAM_BYTE_FIELDS_DTMF(F) + +typedef struct { uint8_t wire_off, vfo_off; } app_beam_byte_map_t; + +#define APP_BEAM_MAP_ROW(w, v) { offsetof(app_beam_channel_t, w), offsetof(VFO_Info_t, v) }, +static const app_beam_byte_map_t app_beam_byte_map[] = { + APP_BEAM_BYTE_FIELDS(APP_BEAM_MAP_ROW) +}; +#undef APP_BEAM_MAP_ROW + +/* Widening either side of a mapped field must fail to compile here rather than + * silently truncate through the byte copy. */ +#define APP_BEAM_MAP_CHECK(w, v) \ + _Static_assert(sizeof(((app_beam_channel_t *)0)->w) == 1u, #w); \ + _Static_assert(sizeof(((VFO_Info_t *)0)->v) == 1u, #v); +APP_BEAM_BYTE_FIELDS(APP_BEAM_MAP_CHECK) +#undef APP_BEAM_MAP_CHECK + +_Static_assert(sizeof(VFO_Info_t) <= 256u && sizeof(app_beam_channel_t) <= 256u, + "app_beam_byte_map offsets must fit in uint8_t"); + +/* Copy every mapped byte field in one direction (to_vfo = save, else export). */ +static void app_beam_copy_bytes(app_beam_channel_t *wire, VFO_Info_t *vfo, bool to_vfo) +{ + for (unsigned i = 0; i < sizeof(app_beam_byte_map) / sizeof(app_beam_byte_map[0]); i++) { + uint8_t *w = (uint8_t *)wire + app_beam_byte_map[i].wire_off; + uint8_t *v = (uint8_t *)vfo + app_beam_byte_map[i].vfo_off; + if (to_vfo) *v = *w; + else *w = *v; + } +} + +static void app_beam_get(app_beam_channel_t *out) +{ + if (out == NULL) + return; + memset(out, 0, sizeof(*out)); + VFO_Info_t *vfo = &gEeprom.VfoInfo[gEeprom.TX_VFO]; + app_beam_copy_bytes(out, vfo, false); /* plain one-byte fields */ + out->rx_frequency = vfo->freq_config_RX.Frequency; + out->tx_offset_frequency = vfo->TX_OFFSET_FREQUENCY; + out->rx_codetype = vfo->freq_config_RX.CodeType; + out->tx_codetype = vfo->freq_config_TX.CodeType; + out->modulation = vfo->Modulation; + out->frequency_reverse = vfo->FrequencyReverse; + out->dtmf_ptt_id_mode = vfo->DTMF_PTT_ID_TX_MODE; + out->step_setting = vfo->STEP_SETTING; + out->band = vfo->Band; + if (IS_MR_CHANNEL(vfo->CHANNEL_SAVE)) + SETTINGS_FetchChannelName(out->name, vfo->CHANNEL_SAVE); + else + memcpy(out->name, vfo->Name, sizeof(out->name)); +} + +static uint16_t app_beam_save(const app_beam_channel_t *in) +{ + if (in == NULL) + return 0xFFFFu; + + /* Only one external-flash write can be deferred per app run. Preserve the + first successfully received channel if an older app tries to queue more. */ + if (app_beam_dirty) + return 0xFFFFu; + + uint16_t channel = MR_CHANNEL_FIRST; + while (IS_MR_CHANNEL(channel) && RADIO_CheckValidChannel(channel, false, 0)) + channel++; + if (!IS_MR_CHANNEL(channel)) + return 0xFFFFu; + + /* External flash cannot be written while the overlay executes from its + sector-cache RAM. Keep the pointer-free payload separate from the radio + VFO: app_beam_prepare() may reuse that VFO before the app returns. */ + memcpy(&app_beam_pending, in, sizeof(app_beam_pending)); + app_beam_pending_channel = channel; + app_beam_dirty = true; + return channel; +} + +/* Called only after the overlay has returned and its code no longer executes + * from the PY25Q16 sector cache. */ +static void app_beam_commit(void) +{ + if (!app_beam_dirty) + return; + app_beam_dirty = false; + + const uint16_t channel = app_beam_pending_channel; + + /* The overlay has returned, so the temporary radio VFO is now free to + become the channel-save staging object. */ + RADIO_InitInfo(&app_beam_vfo, channel, app_beam_pending.rx_frequency); + app_beam_copy_bytes(&app_beam_pending, &app_beam_vfo, true); + app_beam_vfo.TX_OFFSET_FREQUENCY = app_beam_pending.tx_offset_frequency; + app_beam_vfo.freq_config_RX.CodeType = app_beam_pending.rx_codetype; + app_beam_vfo.freq_config_TX.CodeType = app_beam_pending.tx_codetype; + app_beam_vfo.Modulation = app_beam_pending.modulation; + app_beam_vfo.FrequencyReverse = app_beam_pending.frequency_reverse; + app_beam_vfo.DTMF_PTT_ID_TX_MODE = app_beam_pending.dtmf_ptt_id_mode; + app_beam_vfo.STEP_SETTING = app_beam_pending.step_setting < STEP_N_ELEM + ? app_beam_pending.step_setting : STEP_12_5kHz; + app_beam_vfo.StepFrequency = gStepFrequencyTable[app_beam_vfo.STEP_SETTING]; + memcpy(app_beam_vfo.Name, app_beam_pending.name, sizeof(app_beam_vfo.Name)); + app_beam_vfo.Name[sizeof(app_beam_vfo.Name) - 1u] = '\0'; + + SETTINGS_SaveChannel(channel, gEeprom.TX_VFO, &app_beam_vfo, 3); +#ifndef ENABLE_KEEP_MEM_NAME + SETTINGS_SaveChannelName(channel, app_beam_vfo.Name); +#endif + + gEeprom.MrChannel[gEeprom.TX_VFO] = channel; + gEeprom.ScreenChannel[gEeprom.TX_VFO] = channel; + RADIO_ConfigureChannel(gEeprom.TX_VFO, VFO_CONFIGURE_RELOAD); + RADIO_SelectVfos(); + RADIO_SetupRegisters(true); + PY25Q16_InvalidateCache(); +} + +static void app_beam_send(uint16_t *packet) +{ + if (packet == NULL) + return; + RADIO_SetTxParameters(); + BK4819_SendFSKData(packet); + BK4819_SetupPowerAmplifier(0, 0); + BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, false); + RADIO_SelectVfos(); + RADIO_SetupRegisters(true); +} + +static void app_beam_rx(bool start) +{ + app_beam_fsk_index = 0; + if (start) + BK4819_PrepareFSKReceive(); + else + BK4819_ResetFSK(); +} + +static uint8_t app_beam_rx_poll(uint16_t *packet) +{ + if (packet == NULL) + return APP_BEAM_RX_ERROR; + + while (BK4819_ReadRegister(BK4819_REG_0C) & 1u) { + BK4819_WriteRegister(BK4819_REG_02, 0); + const uint16_t irq = BK4819_ReadRegister(BK4819_REG_02); + if (irq & (BK4819_REG_02_FSK_FIFO_ALMOST_FULL | BK4819_REG_02_FSK_RX_FINISHED)) { + const unsigned words = (irq & BK4819_REG_02_FSK_RX_FINISHED) + ? (app_beam_fsk_index < 36u ? 36u - app_beam_fsk_index : 0u) + : 4u; + for (unsigned i = 0; i < words; i++) { + const uint16_t word = BK4819_ReadRegister(BK4819_REG_5F); + if (app_beam_fsk_index < 36u) + packet[app_beam_fsk_index++] = word; + } + } + } + + if (app_beam_fsk_index < 36u) + return APP_BEAM_RX_WAIT; + + app_beam_fsk_index = 0; + const uint16_t status = BK4819_ReadRegister(BK4819_REG_0B); + BK4819_PrepareFSKReceive(); + return (status & 0x0010u) ? APP_BEAM_RX_ERROR : APP_BEAM_RX_READY; +} + +static void app_beam_draw(const char *status) +{ + UI_DisplayStatus(); + UI_DisplayMain(); +#ifdef ENABLE_FEAT_F4HWN + const uint8_t line = (gEeprom.DUAL_WATCH == DUAL_WATCH_OFF && + gEeprom.CROSS_BAND_RX_TX == CROSS_BAND_OFF) ? 5u : 3u; +#else + const uint8_t line = 3u; +#endif + memset(gFrameBuffer[line], 0, LCD_WIDTH); + UI_PrintStringSmallBold(status, 2, LCD_WIDTH - 1u, line); +} +#endif + +/* ---- radio wrappers ---- */ +static int16_t app_rssi_dbm(void) { return BK4819_GetRSSI_dBm() + dBmCorrTable[gRxVfo->Band]; } +static uint16_t app_bk_read(uint8_t r) { return BK4819_ReadRegister((BK4819_REGISTER_t)r); } +static void app_bk_write(uint8_t r, uint16_t v) { BK4819_WriteRegister((BK4819_REGISTER_t)r, v); } +static void app_set_af(uint8_t m) { BK4819_SetAF((BK4819_AF_Type_t)m); } +static void app_audio_path(bool on){ if (on) AUDIO_AudioPathOn(); else AUDIO_AudioPathOff(); } +static void app_prepare_tone(void) { BK4819_PrepareToPlayTone(true); } +static void app_play_tone_raw(uint16_t hz, uint16_t ms) { BK4819_PlayToneRaw(hz, ms); } +static void app_tones_off_rx(void) { BK4819_TurnsOffTones_TurnsOnRX(); } +static uint32_t app_rx_freq(void) { return gRxVfo->pRX->Frequency; } + +/* ---- v2 config (deferred, flash-backed) ---- + * Stored per app slot in the header sector, just after the 64-byte header. cfg_load + * reads flash at launch (ReadBuffer bypasses the overlay cache). cfg_save only stages + * into RAM - the app runs from the sector cache, so it cannot write flash itself; the + * loader commits the staged bytes to flash after the app returns (RMW preserves the + * slot header). Erasing/reinstalling a slot resets its config, which is intended. */ +#define APP_CFG_OFFSET 0x40u /* config area within the header sector */ +static uint8_t app_cfg_buf[16]; +static uint8_t app_cfg_len; /* staged length; 0 = nothing to commit */ +static uint8_t app_run_slot; /* slot of the app currently running */ + +static void app_cfg_load(uint8_t *buf, uint8_t len) +{ + if (len > sizeof(app_cfg_buf)) len = sizeof(app_cfg_buf); + PY25Q16_ReadBuffer(APP_SLOT_BASE(app_run_slot) + APP_CFG_OFFSET, buf, len); +} +static void app_cfg_save(const uint8_t *buf, uint8_t len) +{ + if (len > sizeof(app_cfg_buf)) len = sizeof(app_cfg_buf); + memcpy(app_cfg_buf, buf, len); + app_cfg_len = len; /* mark dirty; the loader commits after the app returns */ +} + +/* ---- v2 battery / backlight ---- */ +static void app_draw_battery(void) +{ + char t[8]; + UI_DrawStatusBattery(gStatusLine, t); +} +static void app_battery_sample(void) +{ + /* The resident scheduler deliberately skips ADC battery updates while the + * PA is keyed. Do the same for Triple VFO: sampling the loaded voltage as + * capacity made an 80% pack appear to fall immediately to about 16%. */ +#ifdef ENABLE_FEAT_F4HWN_OVERLAY_TRIVFO + if (app_trivfo_transmitting) + return; +#endif + + BATTERY_Sample(false); +} + +/* ---- v2 TX (beacon) ---- */ +static uint8_t app_tx_state(void) +{ + if (TX_freq_check(gTxVfo->pTX->Frequency) != 0 && gTxVfo->TX_LOCK) return 1; /* TX disable */ + if (gBatteryDisplayLevel == 0) return 2; /* battery low */ + if (gBatteryDisplayLevel > 6) return 3; /* voltage high */ + if (gTxVfo->Modulation != MODULATION_FM) return 1; + return 0; +} +static void app_tx_set_params(void) { RADIO_SetTxParameters(); } +static void app_tx_tone(uint16_t hz) { BK4819_TransmitTone(false, hz); } +static void app_tx_mute(bool on) { if (on) BK4819_EnterTxMute(); else BK4819_ExitTxMute(); } +static void app_tx_end(void) { BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, false); RADIO_SetupRegisters(true); } +static void app_tx_carrier(bool on) { BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, on); } +static uint32_t app_tx_freq(void) { return gTxVfo->pTX->Frequency; } +static void app_boot_callsign(char *buf, uint8_t len) +{ + char raw[12]; uint8_t n = 0; + PY25Q16_ReadBuffer(SETTINGS_BOOT_MESSAGE_LINE1_ADDR, raw, sizeof(raw)); + for (uint8_t i = 0; i < sizeof(raw) && (uint8_t)(n + 1) < len; i++) { + char c = raw[i]; + if (c == '\0' || (uint8_t)c == 0xFFu) break; + if (c >= 'a' && c <= 'z') c -= 32; + if ((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '/') buf[n++] = c; + } + buf[n] = '\0'; +} + +#ifdef ENABLE_FMRADIO +/* ---- v2 broadcast FM (BK1080), sovereign (no BK4819 dual-watch) ---- */ +static void app_fm_enter(uint16_t f, uint8_t b) +{ + BK1080_Init(f, b); + BK4819_PickRXFilterPathBasedOnFrequency(10320000); /* FM band antenna filter */ + AUDIO_AudioPathOn(); + gEnableSpeaker = true; +} +static void app_fm_exit(void) +{ + AUDIO_AudioPathOff(); + gEnableSpeaker = false; + BK1080_Init0(); + BK4819_PickRXFilterPathBasedOnFrequency(gRxVfo->pRX->Frequency); /* restore RX filter */ +} +static void app_fm_set_freq(uint16_t f, uint8_t b) { BK1080_SetFrequency(f, b); } +static uint16_t app_fm_lo(uint8_t b) { return BK1080_GetFreqLoLimit(b); } +static uint16_t app_fm_hi(uint8_t b) { return BK1080_GetFreqHiLimit(b); } +static void app_fm_mute(bool m) { BK1080_Mute(m); } +static int8_t app_fm_valid(uint16_t f, uint16_t lo) { return (int8_t)FM_CheckFrequencyLock(f, lo); } + +static bool app_fm_dirty; /* deferred: SETTINGS_SaveFM committed after the app returns */ +static void app_fm_state(app_fm_state_t *s, bool write) +{ + if (write) { + gEeprom.FM_FrequencyPlaying = s->freq_playing; + gEeprom.FM_SelectedFrequency = s->sel_freq; + gEeprom.FM_Band = s->band & 3u; + gEeprom.FM_IsMrMode = s->is_mr ? true : false; + gEeprom.FM_SelectedChannel = s->sel_ch; + } else { + s->freq_playing = gEeprom.FM_FrequencyPlaying; + s->sel_freq = gEeprom.FM_SelectedFrequency; + s->band = gEeprom.FM_Band; + s->is_mr = gEeprom.FM_IsMrMode; + s->sel_ch = gEeprom.FM_SelectedChannel; + } +} +static void app_fm_commit(void) { app_fm_dirty = true; } +#endif + +uint8_t APP_ValidateSlot(uint8_t slot, app_header_t *out_header) +{ + if (slot >= APP_SLOT_COUNT) + return APP_ERR_SLOT; + + app_header_t h; + PY25Q16_ReadBuffer(APP_SLOT_BASE(slot), &h, sizeof(h)); + + if (h.magic != APP_MAGIC) return APP_ERR_MAGIC; + if (h.hdr_version != APP_HDR_VERSION) return APP_ERR_MAGIC; + if (h.abi_major != APP_ABI_MAJOR || h.api_min == 0u || + h.api_min > APP_API_LEVEL) return APP_ERR_ABI; + if (!(h.flags & APP_FLAG_COMMITTED)) return APP_ERR_NOT_COMMITTED; + if (h.required_caps & ~APP_AVAILABLE_CAPS) return APP_ERR_CAP; + if (h.code_size < 2u || h.code_size > APP_OVERLAY_MAX || + (uint32_t)h.entry_off > h.code_size - 2u || /* leave room for a 2-byte Thumb insn */ + (h.entry_off & 1u) != 0u) /* entry must be Thumb-aligned (even) */ + return APP_ERR_SIZE; + + if (out_header) + *out_header = h; + return APP_OK; +} + +static bool app_shortcuts_cached; +static uint8_t app_shortcut_mask; +static uint8_t app_shortcut_slots[4]; + +static int8_t app_shortcut_index(uint8_t shortcut) +{ + if (shortcut == APP_SHORTCUT_FM) return 0; + if (shortcut == APP_SHORTCUT_FOXHUNT) return 1; + if (shortcut == APP_SHORTCUT_BEACON) return 2; + if (shortcut == APP_SHORTCUT_BEAM) return 3; + return -1; +} + +static void app_cache_shortcuts(void) +{ + if (app_shortcuts_cached) + return; + + app_shortcut_mask = 0; + const uint32_t overlay_vma = (uint32_t)PY25Q16_OverlayBuffer(); + + for (uint8_t slot = 0; slot < APP_SLOT_COUNT; slot++) { + app_header_t h; + if (APP_ValidateSlot(slot, &h) != APP_OK || h.link_vma != overlay_vma) + continue; + + const uint8_t shortcut = (uint8_t)((h.flags & APP_FLAG_SHORTCUT_MASK) >> + APP_FLAG_SHORTCUT_SHIFT); + const int8_t index = app_shortcut_index(shortcut); + if (index >= 0) { + if (!(app_shortcut_mask & shortcut)) { + app_shortcut_mask |= shortcut; + app_shortcut_slots[index] = slot; + } + } + } + + app_shortcuts_cached = true; +} + +uint8_t APP_OverlayShortcutMask(void) +{ + app_cache_shortcuts(); + return app_shortcut_mask; +} + +uint8_t APP_LaunchOverlayShortcut(uint8_t shortcut) +{ + const int8_t index = app_shortcut_index(shortcut); + if (index < 0) + return APP_ERR_MAGIC; + + app_cache_shortcuts(); + return (app_shortcut_mask & shortcut) + ? APP_LaunchOverlay(app_shortcut_slots[index]) + : APP_ERR_MAGIC; +} + +uint8_t APP_SlotInfo(uint8_t slot, app_header_t *out_header) +{ + if (slot >= APP_SLOT_COUNT) + return APP_ERR_SLOT; + app_header_t h; + PY25Q16_ReadBuffer(APP_SLOT_BASE(slot), &h, sizeof(h)); + if (out_header) + *out_header = h; + return (h.magic == APP_MAGIC) ? APP_OK : APP_ERR_MAGIC; +} + +/* All services are immutable. Keeping the table in flash avoids rebuilding a + * roughly quarter-kilobyte automatic object on every launch and removes that + * object from the launcher's stack frame. Callbacks must also obey the ABI's + * no-external-flash-write rule while entry() is running. */ +static const app_api_t app_api = { + .abi_major = APP_ABI_MAJOR, + .api_level = APP_API_LEVEL, + .api_size = sizeof(app_api_t), + .fb = gFrameBuffer, + .display_clear = UI_DisplayClear, + .status_clear = UI_StatusClear, + .draw_line = UI_DrawLineBuffer, + .draw_rect = UI_DrawRectangleBuffer, + .print_bold = UI_PrintStringSmallBold, + .print_tiny = GUI_DisplaySmallest, +#ifdef ENABLE_FEAT_F4HWN_K5VIEWER + .blit_full = app_blit_full, +#else + .blit_full = ST7565_BlitFullScreen, +#endif + .blit_line = ST7565_BlitLine, + .blit_status = ST7565_BlitStatusLine, + .get_key = app_get_key, + .delay_ms = SYSTEM_DelayMs, + .play_tone = app_play_tone, + .led = app_led, + .print_normal = UI_PrintStringSmallNormal, + .print_inverse = GUI_DisplaySmallestInverse, + .display_freq = UI_DisplayFrequency, + .rssi_dbm = app_rssi_dbm, + .bk_read = app_bk_read, + .bk_write = app_bk_write, + .set_agc = BK4819_SetAGC, + .set_af = app_set_af, + .audio_path = app_audio_path, + .prepare_tone = app_prepare_tone, + .play_tone_raw = app_play_tone_raw, + .tones_off_rx = app_tones_off_rx, + .rx_freq = app_rx_freq, + .cfg_load = app_cfg_load, + .cfg_save = app_cfg_save, + .draw_battery = app_draw_battery, + .battery_sample = app_battery_sample, + .backlight_on = app_backlight_on, + .backlight_update = app_backlight_update, + .audio_scope = UI_DisplayAudioScopeOverlay, + .status_line = gStatusLine, + .tx_state = app_tx_state, + .tx_set_params = app_tx_set_params, + .tx_tone = app_tx_tone, + .tx_mute = app_tx_mute, + .tx_end = app_tx_end, + .tx_carrier = app_tx_carrier, + .tx_freq = app_tx_freq, + .boot_callsign = app_boot_callsign, + .print_string = UI_PrintString, +#ifdef ENABLE_FMRADIO + .fm_enter = app_fm_enter, + .fm_exit = app_fm_exit, + .fm_set_freq = app_fm_set_freq, + .fm_lo = app_fm_lo, + .fm_hi = app_fm_hi, + .fm_mute = app_fm_mute, + .fm_valid = app_fm_valid, + .fm_channels = gFM_Channels, + .fm_state = app_fm_state, + .fm_commit = app_fm_commit, +#endif + .nav_dir = app_nav_dir, +#ifdef ENABLE_FEAT_F4HWN_OVERLAY_TRIVFO + .trivfo_enter = app_trivfo_enter, + .trivfo_leave = app_trivfo_leave, + .trivfo_get = app_trivfo_get, + .trivfo_select = app_trivfo_select, + .trivfo_step = app_trivfo_step, + .trivfo_tick = app_trivfo_tick, + .trivfo_ptt = app_trivfo_ptt, +#endif +#ifdef ENABLE_FEAT_F4HWN_OVERLAY_BEAM + .beam_prepare = app_beam_prepare, + .beam_leave = app_beam_leave, + .beam_get = app_beam_get, + .beam_save = app_beam_save, + .beam_send = app_beam_send, + .beam_rx = app_beam_rx, + .beam_rx_poll = app_beam_rx_poll, + .beam_draw = app_beam_draw, +#endif +}; + +uint8_t APP_LaunchOverlay(uint8_t slot) +{ + app_header_t h; + uint8_t rc = APP_ValidateSlot(slot, &h); + if (rc != APP_OK) + return rc; + + /* The app's absolute data references only resolve if it runs at the exact + * VMA it was linked for. The overlay VMA varies with the firmware's RAM + * layout (per preset/features), so the app records its link VMA and we + * refuse a mismatch cleanly instead of jumping into misaddressed code. */ + uint8_t *ws = PY25Q16_OverlayBuffer(); + if (h.link_vma != (uint32_t)ws) + return APP_ERR_VMA; + + /* Repurpose the sector cache: drop any cached config sector, load the code + * straight in (ReadBuffer bypasses the cache), and verify it in RAM before + * trusting it. Zeroing first leaves the app's .bss clean. */ + PY25Q16_InvalidateCache(); + memset(ws, 0, APP_OVERLAY_MAX); + PY25Q16_ReadBuffer(APP_SLOT_BASE(slot) + APP_CODE_OFFSET, ws, h.code_size); + + if (MB_Crc32Bytes(ws, h.code_size) != h.code_crc32) { + PY25Q16_InvalidateCache(); + return APP_ERR_CRC; + } + + /* Ensure every store to the overlay is visible before we branch into it. */ + __DSB(); + __ISB(); + + app_run_slot = slot; /* for cfg_load / cfg_save */ + app_cfg_len = 0; +#ifdef ENABLE_FMRADIO + app_fm_dirty = false; +#endif +#ifdef ENABLE_FEAT_F4HWN_OVERLAY_BEAM + app_beam_dirty = false; +#endif + + app_allow_screen_saver = (h.flags & APP_FLAG_SCREEN_SAVER) != 0; + app_screen_saver_wake = false; + APP_ModalScreenSaverExit(); + BACKLIGHT_TurnOn(); + +#ifdef ENABLE_FEAT_F4HWN_K5VIEWER + /* The caller enters from a debounced key event, so the resident key state + * still contains that trigger while the modal app is running. Clear it so + * K5Viewer is allowed to mirror overlay frames immediately. */ + gKeyReading0 = KEY_INVALID; + gKeyReading1 = KEY_INVALID; +#endif + + /* Pin RX to the user-selected VFO before the app runs. Under dual watch + * gRxVfo is whichever VFO the receiver was parked on when F+7 was pressed, + * so an RF app (FoxHunt, a future S-meter, ...) would measure and display a + * VFO the user did not pick - sometimes A, sometimes B. Point RX at the + * selected (TX) VFO and retune so rx_freq(), rssi_dbm() and the tuned + * hardware all agree on the selected channel. Save all three pointers: + * radio apps such as BEAM temporarily replace them while they run. */ + const uint8_t saved_rx_vfo = gEeprom.RX_VFO; + VFO_Info_t *const saved_rx = gRxVfo; + VFO_Info_t *const saved_tx = gTxVfo; + VFO_Info_t *const saved_current = gCurrentVfo; + gEeprom.RX_VFO = gEeprom.TX_VFO; + gRxVfo = gTxVfo; + RADIO_SetupRegisters(true); + + app_entry_t entry = (app_entry_t)(((uint32_t)ws + h.entry_off) | 1u); + entry(&app_api); + + APP_ModalScreenSaverExit(); + app_allow_screen_saver = false; + app_screen_saver_wake = false; + + /* A defensive leave also covers an app returning through an error path. */ +#ifdef ENABLE_FEAT_F4HWN_OVERLAY_TRIVFO + app_trivfo_leave(); +#endif + + /* Restore the resident RX/dual-watch tuning the app ran on top of. */ + gEeprom.RX_VFO = saved_rx_vfo; + gRxVfo = saved_rx; + gTxVfo = saved_tx; + gCurrentVfo = saved_current; + RADIO_SetupRegisters(true); + + /* The overlay held app code, not a valid config sector. */ + PY25Q16_InvalidateCache(); + +#ifdef ENABLE_FEAT_F4HWN_OVERLAY_BEAM + app_beam_commit(); +#endif + +#ifdef ENABLE_FEAT_F4HWN_OVERLAY_TRIVFO + if (app_trivfo_ab_dirty) { + SETTINGS_SaveVfoIndices(); + app_trivfo_ab_dirty = false; + } + + for (uint8_t i = 0; i < 2u; i++) { + if (app_trivfo_freq_dirty & (1u << i)) + SETTINGS_SaveChannel(gEeprom.VfoInfo[i].CHANNEL_SAVE, i, + &gEeprom.VfoInfo[i], 1); + } + app_trivfo_freq_dirty = 0; +#endif + + /* Commit any deferred config the app staged (RMW keeps the slot header). */ + if (app_cfg_len) { + PY25Q16_WriteBuffer(APP_SLOT_BASE(slot) + APP_CFG_OFFSET, app_cfg_buf, app_cfg_len, false); + PY25Q16_InvalidateCache(); + } +#ifdef ENABLE_FMRADIO + /* Commit the FM config + 48 channels the app edited (shared with resident FM). */ + if (app_fm_dirty) { + app_fm_dirty = false; + SETTINGS_SaveFM(); + PY25Q16_InvalidateCache(); + } +#endif + return APP_OK; +} + +uint8_t APP_SlotErase(uint8_t slot) +{ + if (slot >= APP_SLOT_COUNT) + return APP_ERR_SLOT; + uint32_t base = APP_SLOT_BASE(slot); + for (uint32_t off = 0; off < APP_SLOT_STRIDE; off += APP_SECTOR_SIZE) + PY25Q16_SectorErase(base + off); + PY25Q16_InvalidateCache(); + app_shortcuts_cached = false; + return APP_OK; +} + +uint8_t APP_SlotWrite(uint8_t slot, uint32_t offset, const uint8_t *data, uint32_t len) +{ + if (slot >= APP_SLOT_COUNT) + return APP_ERR_SLOT; + if (offset > APP_SLOT_STRIDE || len > APP_SLOT_STRIDE - offset) + return APP_ERR_SIZE; + PY25Q16_WriteBuffer(APP_SLOT_BASE(slot) + offset, data, len, false); + PY25Q16_InvalidateCache(); + app_shortcuts_cached = false; + return APP_OK; +} + +#endif /* ENABLE_FEAT_F4HWN_OVERLAY_APPS */ diff --git a/App/apps/app_overlay.h b/App/apps/app_overlay.h new file mode 100644 index 00000000..89c4f546 --- /dev/null +++ b/App/apps/app_overlay.h @@ -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 +#include +#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 */ diff --git a/App/apps/beacon/app.ld b/App/apps/beacon/app.ld new file mode 100644 index 00000000..bd7a08d2 --- /dev/null +++ b/App/apps/beacon/app.ld @@ -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.*) } +} diff --git a/App/apps/beacon/beacon_app.c b/App/apps/beacon/beacon_app.c new file mode 100644 index 00000000..361fdcf0 --- /dev/null +++ b/App/apps/beacon/beacon_app.c @@ -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 / " 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 +#include +#include +#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 a0) 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;kprint_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;xfb[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;xfb[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;idelay_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]=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); +} diff --git a/App/apps/beacon/build.sh b/App/apps/beacon/build.sh new file mode 100755 index 00000000..bcbf1b2d --- /dev/null +++ b/App/apps/beacon/build.sh @@ -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" diff --git a/App/apps/beam/app.ld b/App/apps/beam/app.ld new file mode 100644 index 00000000..ab4f428b --- /dev/null +++ b/App/apps/beam/app.ld @@ -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.*) } +} diff --git a/App/apps/beam/beam_app.c b/App/apps/beam/beam_app.c new file mode 100644 index 00000000..33940743 --- /dev/null +++ b/App/apps/beam/beam_app.c @@ -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 +#include +#include +#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(); +} diff --git a/App/apps/beam/build.sh b/App/apps/beam/build.sh new file mode 100755 index 00000000..f8850ed5 --- /dev/null +++ b/App/apps/beam/build.sh @@ -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" diff --git a/App/apps/breakout/app.ld b/App/apps/breakout/app.ld new file mode 100644 index 00000000..b7439278 --- /dev/null +++ b/App/apps/breakout/app.ld @@ -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.*) } +} diff --git a/App/apps/breakout/breakout_app.c b/App/apps/breakout/breakout_app.c new file mode 100644 index 00000000..c85bb99b --- /dev/null +++ b/App/apps/breakout/breakout_app.c @@ -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 +#include +#include +#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(); + } +} diff --git a/App/apps/breakout/build.sh b/App/apps/breakout/build.sh new file mode 100755 index 00000000..a17a7159 --- /dev/null +++ b/App/apps/breakout/build.sh @@ -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" diff --git a/App/apps/cube3d/app.ld b/App/apps/cube3d/app.ld new file mode 100644 index 00000000..0ea0222e --- /dev/null +++ b/App/apps/cube3d/app.ld @@ -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.*) } +} diff --git a/App/apps/cube3d/build.sh b/App/apps/cube3d/build.sh new file mode 100755 index 00000000..cadc1df6 --- /dev/null +++ b/App/apps/cube3d/build.sh @@ -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" diff --git a/App/apps/cube3d/cube3d_app.c b/App/apps/cube3d/cube3d_app.c new file mode 100644 index 00000000..b588d102 --- /dev/null +++ b/App/apps/cube3d/cube3d_app.c @@ -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 +#include +#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 */ + } +} diff --git a/App/apps/fm/app.ld b/App/apps/fm/app.ld new file mode 100644 index 00000000..11762b66 --- /dev/null +++ b/App/apps/fm/app.ld @@ -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.*) } +} diff --git a/App/apps/fm/build.sh b/App/apps/fm/build.sh new file mode 100755 index 00000000..5500c65e --- /dev/null +++ b/App/apps/fm/build.sh @@ -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" diff --git a/App/apps/fm/fm_app.c b/App/apps/fm/fm_app.c new file mode 100644 index 00000000..5fa382b1 --- /dev/null +++ b/App/apps/fm/fm_app.c @@ -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 +#include +#include +#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(fhi)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=lo && ch[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;iprint_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){ 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;ifm_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(c1 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_playinghi){ 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_playinghi) st.freq_playing=lo; + if(st.sel_freqhi) 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(); +} diff --git a/App/apps/foxhunt/app.ld b/App/apps/foxhunt/app.ld new file mode 100644 index 00000000..0b5d0a1e --- /dev/null +++ b/App/apps/foxhunt/app.ld @@ -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.*) } +} diff --git a/App/apps/foxhunt/build.sh b/App/apps/foxhunt/build.sh new file mode 100755 index 00000000..2d77d1f0 --- /dev/null +++ b/App/apps/foxhunt/build.sh @@ -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" diff --git a/App/apps/foxhunt/foxhunt_app.c b/App/apps/foxhunt/foxhunt_app.c new file mode 100644 index 00000000..fba3aacf --- /dev/null +++ b/App/apps/foxhunt/foxhunt_app.c @@ -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 +#include +#include +#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_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;idelay_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;idraw_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)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=(ydraw_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(attSteprx_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;kprint_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]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;idelay_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=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); +} diff --git a/App/apps/pack_app.py b/App/apps/pack_app.py new file mode 100644 index 00000000..077b2d21 --- /dev/null +++ b/App/apps/pack_app.py @@ -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 ` 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() diff --git a/App/apps/plasma/app.ld b/App/apps/plasma/app.ld new file mode 100644 index 00000000..c315d2af --- /dev/null +++ b/App/apps/plasma/app.ld @@ -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.*) } +} diff --git a/App/apps/plasma/build.sh b/App/apps/plasma/build.sh new file mode 100755 index 00000000..69959c3d --- /dev/null +++ b/App/apps/plasma/build.sh @@ -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" diff --git a/App/apps/plasma/plasma_app.c b/App/apps/plasma/plasma_app.c new file mode 100644 index 00000000..32c036c2 --- /dev/null +++ b/App/apps/plasma/plasma_app.c @@ -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 +#include +#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(); + } +} diff --git a/App/apps/tetris/app.ld b/App/apps/tetris/app.ld new file mode 100644 index 00000000..6baa4570 --- /dev/null +++ b/App/apps/tetris/app.ld @@ -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.*) } +} diff --git a/App/apps/tetris/build.sh b/App/apps/tetris/build.sh new file mode 100755 index 00000000..d543593f --- /dev/null +++ b/App/apps/tetris/build.sh @@ -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" diff --git a/App/apps/tetris/tetris_app.c b/App/apps/tetris/tetris_app.c new file mode 100644 index 00000000..f195700f --- /dev/null +++ b/App/apps/tetris/tetris_app.c @@ -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 +#include +#include +#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); +} diff --git a/App/apps/trivfo/app.ld b/App/apps/trivfo/app.ld new file mode 100644 index 00000000..32ba57b3 --- /dev/null +++ b/App/apps/trivfo/app.ld @@ -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.*) } +} diff --git a/App/apps/trivfo/build.sh b/App/apps/trivfo/build.sh new file mode 100755 index 00000000..233a5136 --- /dev/null +++ b/App/apps/trivfo/build.sh @@ -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" diff --git a/App/apps/trivfo/trivfo_app.c b/App/apps/trivfo/trivfo_app.c new file mode 100644 index 00000000..9bca5e30 --- /dev/null +++ b/App/apps/trivfo/trivfo_app.c @@ -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 +#include +#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->channelchannel+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;ifb[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->channelname); + 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;itrivfo_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;iaudio_scope(0u,true); + else { + A->audio_scope(0u,false); + for(uint8_t i=0;iblit_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;idelay_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(); +} diff --git a/App/audio.c b/App/audio.c index 42308e96..12407f7e 100644 --- a/App/audio.c +++ b/App/audio.c @@ -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 diff --git a/App/bitmaps.c b/App/bitmaps.c index 141a7026..32f1bc52 100644 --- a/App/bitmaps.c +++ b/App/bitmaps.c @@ -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, diff --git a/App/bitmaps.h b/App/bitmaps.h index f64c332e..a79eb7a4 100644 --- a/App/bitmaps.h +++ b/App/bitmaps.h @@ -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]; diff --git a/App/board.c b/App/board.c index b1b94efd..e6c62f9b 100644 --- a/App/board.c +++ b/App/board.c @@ -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 diff --git a/App/debugging.h b/App/debugging.h index aa0bb692..6679bbc2 100644 --- a/App/debugging.h +++ b/App/debugging.h @@ -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) { diff --git a/App/driver/backlight.c b/App/driver/backlight.c index bbf9bab2..584c7da0 100644 --- a/App/driver/backlight.c +++ b/App/driver/backlight.c @@ -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; } diff --git a/App/driver/backlight.h b/App/driver/backlight.h index b0d0e7b9..f64e2609 100644 --- a/App/driver/backlight.h +++ b/App/driver/backlight.h @@ -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(); diff --git a/App/driver/bk4819.c b/App/driver/bk4819.c index 168adf45..2e15dc4e 100644 --- a/App/driver/bk4819.c +++ b/App/driver/bk4819.c @@ -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 diff --git a/App/driver/bk4819.h b/App/driver/bk4819.h index 9bbab159..79870bff 100644 --- a/App/driver/bk4819.h +++ b/App/driver/bk4819.h @@ -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); diff --git a/App/driver/bk4829.c b/App/driver/bk4829.c index 78c38c9e..27e99d6f 100644 --- a/App/driver/bk4829.c +++ b/App/driver/bk4829.c @@ -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 diff --git a/App/driver/eeprom.c b/App/driver/eeprom.c index 4357f658..b00a7e1b 100644 --- a/App/driver/eeprom.c +++ b/App/driver/eeprom.c @@ -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); } diff --git a/App/driver/eeprom.h b/App/driver/eeprom.h index 545c48c9..e0213595 100644 --- a/App/driver/eeprom.h +++ b/App/driver/eeprom.h @@ -20,7 +20,6 @@ #include 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 - diff --git a/App/driver/eeprom_compat.c b/App/driver/eeprom_compat.c index ed9d9c7c..a7b751a1 100644 --- a/App/driver/eeprom_compat.c +++ b/App/driver/eeprom_compat.c @@ -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; diff --git a/App/driver/mb_flash.c b/App/driver/mb_flash.c new file mode 100644 index 00000000..05fd6a5f --- /dev/null +++ b/App/driver/mb_flash.c @@ -0,0 +1,1171 @@ +/* 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 +#include + +#include "driver/mb_flash.h" +#include "driver/py25q16.h" + +#include "py32f0xx.h" + +/* Internal-flash program/erase keys (FLASH_KEY1 / FLASH_KEY2). */ +#define MB_FLASH_KEY1 0x45670123u +#define MB_FLASH_KEY2 0xCDEF89ABu + +/* Internal flash granularity (PY32F071xB): program & page-erase = 256 bytes. */ +#define MB_FLASH_PAGE 256u + +/* External SPI flash chip-select is on PA3 (see driver/py25q16.c). */ +#define MB_CS_PIN (1u << 3) + +/* LCD control pins used only for RAM-resident progress updates. */ +#define MB_LCD_CS_PIN (1u << 2) /* PB2 */ +#define MB_LCD_A0_PIN (1u << 6) /* PA6 */ + +/* Rounded progress gauge geometry, matching ScanProgress_DrawGaugeLine(). */ +#define MB_PROGRESS_COLS 118u +#define MB_PROGRESS_FIRST_COL 5u +#define MB_PROGRESS_FILLED 0x2Du + +/* Number of erase/program retries per page before giving up (and resetting + * anyway - the region is already erased, so USB recovery is the only option). */ +#define MB_PAGE_RETRIES 3u + +/* Bounded waits used by the RAM-only copier. A timeout forces an immediate + * reset instead of hanging forever with IRQs disabled. */ +#define MB_RAM_SPI_TIMEOUT 100000u +#define MB_RAM_FLASH_TIMEOUT 10000000u + +/* + * Factory flash-timing parameter records, held in Puya system memory. + * Mirror of the HAL's _FlashTimmingParam[] table (the HAL module is not built + * in this project). Indexed by the HSI frequency setting (RCC->ICSCR HSI_FS). + * Each entry is the address of a 5-word record read at +0/+8/+16/+24/+32. + */ +static const uint32_t mb_flash_timing[8] = { + 0x1FFF3238, 0x1FFF3260, 0x1FFF3288, 0x1FFF32B0, + 0x1FFF32D8, 0x1FFF3238, 0x1FFF3238, 0x1FFF3238 +}; + +/* -------------------------------------------------------------------------- */ +/* Flash-resident preparation (runs while the flash is still readable). */ +/* -------------------------------------------------------------------------- */ + +static void MB_PrepareInternalFlash(void) +{ + /* Unlock the internal flash control register. */ + if (FLASH->CR & FLASH_CR_LOCK) + { + FLASH->KEYR = MB_FLASH_KEY1; + FLASH->KEYR = MB_FLASH_KEY2; + } + + /* Program/erase timing sequence (factory calibrated), replicating + * __HAL_FLASH_TIMMING_SEQUENCE_CONFIG(). These registers persist, so it is + * enough to set them once here, before the RAM copier starts erasing. */ + uint32_t base = mb_flash_timing[(RCC->ICSCR & RCC_ICSCR_HSI_FS) >> RCC_ICSCR_HSI_FS_Pos]; + uint32_t p0 = *(volatile uint32_t *)(base + 0); + uint32_t p1 = *(volatile uint32_t *)(base + 8); + uint32_t p2 = *(volatile uint32_t *)(base + 16); + uint32_t p3 = *(volatile uint32_t *)(base + 24); + uint32_t p4 = *(volatile uint32_t *)(base + 32); + + FLASH->TS0 = p0 & 0xFFu; + FLASH->TS1 = (p0 >> 16) & 0x1FFu; + FLASH->TS3 = (p0 >> 8) & 0xFFu; + FLASH->TS2P = p1 & 0xFFu; + FLASH->TPS3 = (p1 >> 16) & 0x7FFu; + FLASH->PERTPE = p2 & 0x1FFFFu; + FLASH->SMERTPE = p3 & 0x1FFFFu; + FLASH->PRGTPE = p4 & 0xFFFFu; + FLASH->PRETPE = (p4 >> 16) & 0x3FFFu; +} + +/* -------------------------------------------------------------------------- */ +/* RAM-resident copier. */ +/* */ +/* This runs while the internal application flash is being erased/programmed, */ +/* during which the flash bus is unavailable. It must therefore NOT fetch any */ +/* code from flash nor read any flash data: it uses raw register access only */ +/* (no external calls), reads the source from the external SPI flash in */ +/* polled mode, and resets the MCU when done. Normally it is placed in */ +/* .RamFunc, which startup copies to RAM alongside .data. With the overlay */ +/* enabled it is linked in .MBRamFunc and copied over the PY25Q16 sector cache */ +/* only immediately before use. */ +/* -------------------------------------------------------------------------- */ + +/* These primitives are called repeatedly while application flash is offline. + * Keep one copy of each in the copied RAM section: noinline/noclone prevents + * GCC from silently duplicating one back into MB_RamReflash. */ +#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY + #define MB_RAM_SECTION ".MBRamFunc" +#else + #define MB_RAM_SECTION ".RamFunc" +#endif +#define MB_RAM_HELPER __attribute__((section(MB_RAM_SECTION), noinline, noclone, used)) + +/* Polled single-byte SPI2 transfer. The result is returned through out so + * timeout and received 0xFF remain distinguishable. */ +MB_RAM_HELPER static bool mb_ram_spi(uint8_t v, uint8_t *out) +{ + uint32_t timeout = MB_RAM_SPI_TIMEOUT; + while (!(SPI2->SR & SPI_SR_TXE)) + if (!--timeout) + return false; + + *(volatile uint8_t *)&SPI2->DR = v; + + timeout = MB_RAM_SPI_TIMEOUT; + while (!(SPI2->SR & SPI_SR_RXNE)) + if (!--timeout) + return false; + + *out = *(volatile uint8_t *)&SPI2->DR; + return true; +} + +MB_RAM_HELPER static bool mb_ram_flash_idle(void) +{ + uint32_t timeout = MB_RAM_FLASH_TIMEOUT; + while (FLASH->SR & FLASH_SR_BSY) + if (!--timeout) + return false; + return true; +} + +MB_RAM_HELPER __attribute__((noreturn)) static void mb_ram_reset(void) +{ + __DSB(); + + SCB->AIRCR = (0x5FAu << SCB_AIRCR_VECTKEY_Pos) | SCB_AIRCR_SYSRESETREQ_Msk; + __DSB(); + for (;;) { } +} + +/* Minimal SPI1 LCD writer. A display timeout merely disables progress updates: + * it must never abort or delay the safety-critical flash copy. */ +MB_RAM_HELPER static bool mb_ram_lcd_spi(uint8_t v) +{ + uint32_t timeout = MB_RAM_SPI_TIMEOUT; + while (!(SPI1->SR & SPI_SR_TXE)) + if (!--timeout) + return false; + *(volatile uint8_t *)&SPI1->DR = v; + + timeout = MB_RAM_SPI_TIMEOUT; + while (!(SPI1->SR & SPI_SR_RXNE)) + if (!--timeout) + return false; + (void)*(volatile uint8_t *)&SPI1->DR; + return true; +} + +__attribute__((always_inline)) static inline bool mb_ram_progress_blit(const uint8_t *line) +{ + uint32_t ok = 1u; + GPIOB->BRR = MB_LCD_CS_PIN; + GPIOA->BRR = MB_LCD_A0_PIN; /* command */ + + if (!mb_ram_lcd_spi(0xB7u) || /* LCD page 7 */ + !mb_ram_lcd_spi(0x10u) || /* column high nibble */ + !mb_ram_lcd_spi(0x04u)) /* visible RAM starts at column 4 */ + ok = 0u; + + GPIOA->BSRR = MB_LCD_A0_PIN; /* data */ + if (ok) + for (uint32_t i = 0; i < 128u; i++) + if (!mb_ram_lcd_spi(line[i])) + { + ok = 0u; + break; + } + GPIOB->BSRR = MB_LCD_CS_PIN; + return ok != 0u; +} + +/* Blank the whole LCD RAM (all 8 pages) right before the reset. The MCU reset + * leaves the display controller powered and still showing the "Restore slot N" + * screen; it stays visible through the next boot until ST7565_Init re-inits the + * panel. Wiping it here means the reboot window shows nothing instead of a + * stale restore screen. Best-effort: a display timeout just leaves it as-is. */ +__attribute__((always_inline)) static inline void mb_ram_lcd_clear(void) +{ + for (uint8_t page = 0; page < 8u; page++) + { + GPIOB->BRR = MB_LCD_CS_PIN; + GPIOA->BRR = MB_LCD_A0_PIN; /* command */ + if (!mb_ram_lcd_spi((uint8_t)(0xB0u | page)) || /* set page 0..7 */ + !mb_ram_lcd_spi(0x10u) || /* column high nibble */ + !mb_ram_lcd_spi(0x04u)) /* visible RAM starts at column 4 */ + { + GPIOB->BSRR = MB_LCD_CS_PIN; + return; + } + GPIOA->BSRR = MB_LCD_A0_PIN; /* data */ + for (uint32_t i = 0; i < 128u; i++) + if (!mb_ram_lcd_spi(0x00u)) + { + GPIOB->BSRR = MB_LCD_CS_PIN; + return; + } + GPIOB->BSRR = MB_LCD_CS_PIN; + } +} +__attribute__((section(MB_RAM_SECTION), noinline, used)) +static void MB_RamReflash(uint32_t intAddr, uint32_t extAddr, uint32_t imageSize, + uint8_t *progressLine) +{ + /* 4-byte aligned so the 64-word page program can read it as uint32_t + * (Cortex-M0+ cannot do unaligned word accesses). */ + uint8_t buf[MB_FLASH_PAGE] __attribute__((aligned(4))); + uint32_t remaining = imageSize; + uint32_t regionRemaining = MB_INT_APP_SIZE; + uint32_t pagesDone = 0; + uint32_t progressAccumulator = 0; + uint32_t progressFilled = 0; + uint32_t lcdEnabled = progressLine != NULL; + + + __disable_irq(); + + if (FLASH->CR & FLASH_CR_LOCK) + { + FLASH->KEYR = MB_FLASH_KEY1; + FLASH->KEYR = MB_FLASH_KEY2; + } + + FLASH->SR = FLASH_SR_EOP | FLASH_SR_WRPERR | FLASH_SR_OPTVERR; + + /* Rebuild the complete application region. Bytes past imageSize are never + * read from the external slot: they are forced to erased 0xFF, preventing + * unvalidated padding or remnants of an older, longer firmware. */ + while (regionRemaining >= MB_FLASH_PAGE) + { + uint32_t readSize = remaining < MB_FLASH_PAGE ? remaining : MB_FLASH_PAGE; + uint32_t needProgram = 0; + uint32_t success = 0; + uint8_t ignored; + + /* Volatile stores prevent GCC from replacing this loop with a call + * to flash-resident memset while the application flash is unavailable. */ + volatile uint8_t *fill = buf; + for (uint32_t i = 0; i < MB_FLASH_PAGE; i++) + fill[i] = 0xFFu; + + if (readSize) + { + /* Read only CRC-validated image bytes. The rest of the page stays + * 0xFF when imageSize is not page-aligned. */ + GPIOA->BRR = MB_CS_PIN; /* CS low */ + if (!mb_ram_spi(0x03u, &ignored) || + !mb_ram_spi((extAddr >> 16) & 0xFFu, &ignored) || + !mb_ram_spi((extAddr >> 8) & 0xFFu, &ignored) || + !mb_ram_spi(extAddr & 0xFFu, &ignored)) + goto fatal_reset; + + for (uint32_t i = 0; i < readSize; i++) + if (!mb_ram_spi(0xFFu, &buf[i])) + goto fatal_reset; + + GPIOA->BSRR = MB_CS_PIN; /* CS high */ + } + + for (uint32_t i = 0; i < MB_FLASH_PAGE; i++) + { + if (buf[i] != 0xFFu) + { + needProgram = 1u; + break; + } + } + + for (uint32_t retry = 0; retry < MB_PAGE_RETRIES; retry++) + { + const uint32_t *src = (const uint32_t *)(const void *)buf; + volatile uint32_t *dst = (volatile uint32_t *)intAddr; + + uint32_t i; + uint32_t ok = 1u; + + /* --- page erase (256 bytes) --- */ + if (!mb_ram_flash_idle()) + goto fatal_reset; + FLASH->CR |= FLASH_CR_PER; + *(volatile uint32_t *)intAddr = 0xFFFFFFFFu; + if (!mb_ram_flash_idle()) + goto fatal_reset; + FLASH->CR &= ~FLASH_CR_PER; + FLASH->SR = FLASH_SR_EOP | FLASH_SR_WRPERR | FLASH_SR_OPTVERR; + + if (needProgram) + { + /* Page program: 64 words, PGSTRT before the last word. */ + FLASH->CR |= FLASH_CR_PG; + for (i = 0; i < 64u; i++) + { + dst[i] = src[i]; + if (i == 62u) + FLASH->CR |= FLASH_CR_PGSTRT; + } + if (!mb_ram_flash_idle()) + goto fatal_reset; + FLASH->CR &= ~FLASH_CR_PG; + FLASH->SR = FLASH_SR_EOP | FLASH_SR_WRPERR | FLASH_SR_OPTVERR; + } + + /* --- verify (read-back compare) --- */ + for (i = 0; i < 64u; i++) + { + if (dst[i] != src[i]) + { + ok = 0u; + break; + } + } + if (ok) + { + success = 1u; + break; + } + } + + /* Never silently continue after an unprogrammable page. Returning to + * flash-resident code is unsafe once the application has been erased. */ + if (!success) + goto fatal_reset; + + /* Advance the gauge without division (which could call a helper + * in erased flash). Refresh once per 8 KiB internal sector. */ + if (lcdEnabled) + { + pagesDone++; + progressAccumulator += MB_PROGRESS_COLS; + while (progressAccumulator >= (MB_INT_APP_SIZE / MB_FLASH_PAGE)) + { + progressAccumulator -= (MB_INT_APP_SIZE / MB_FLASH_PAGE); + if (progressFilled < MB_PROGRESS_COLS) + { + progressLine[MB_PROGRESS_FIRST_COL + progressFilled] = MB_PROGRESS_FILLED; + progressFilled++; + } + } + if ((pagesDone & 31u) == 0u || regionRemaining == MB_FLASH_PAGE) + lcdEnabled = mb_ram_progress_blit(progressLine); + } + + intAddr += MB_FLASH_PAGE; + extAddr += readSize; + remaining -= readSize; + regionRemaining -= MB_FLASH_PAGE; + } + + FLASH->CR |= FLASH_CR_LOCK; + if (lcdEnabled) + mb_ram_lcd_clear(); + mb_ram_reset(); + +fatal_reset: + /* Release the external flash and reset immediately. If failure happened + * after an erase, the factory USB/DFU bootloader remains the recovery path. */ + GPIOA->BSRR = MB_CS_PIN; + FLASH->CR &= ~(FLASH_CR_PER | FLASH_CR_PG | FLASH_CR_PGSTRT); + FLASH->CR |= FLASH_CR_LOCK; + mb_ram_reset(); +} + +/* -------------------------------------------------------------------------- */ +/* Public API. */ +/* -------------------------------------------------------------------------- */ + +/* Set when a polled SPI wait below times out (external flash unresponsive). */ +static volatile int mb_spi_err; + +#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY +/* Linker-provided load/execution bounds for the restore stub. Its execution + * address aliases the PY25Q16 sector cache; its load image remains in flash. */ +extern uint8_t __mb_ramfunc_load_start; +extern uint8_t __mb_ramfunc_start; +extern uint8_t __mb_ramfunc_end; + +static bool mb_load_ram_reflash_overlay(void) +{ + const volatile uint8_t *src = &__mb_ramfunc_load_start; + volatile uint8_t *dst = &__mb_ramfunc_start; + volatile uint8_t *end = &__mb_ramfunc_end; + + /* Volatile byte copies prevent a library memcpy call. The copy itself runs + * while internal flash is still fully available. */ + while (dst < end) + *dst++ = *src++; + + /* Cortex-M0+ has no instruction cache, but the barriers ensure that every + * store is visible before the following BLX starts fetching the stub. */ + __DSB(); + __ISB(); + + src = &__mb_ramfunc_load_start; + dst = &__mb_ramfunc_start; + while (dst < end) + if (*dst++ != *src++) + return false; + + return true; +} +#endif + +/* Polled single-byte SPI2 transfer (flash-resident; runs in normal context). + * Bounded so a wedged SPI can never freeze the firmware: on timeout it sets + * mb_spi_err and returns 0xFF, letting the caller fail gracefully. */ +#define MB_SPI_TIMEOUT 20000u /* ~a few ms max per byte; a healthy transfer + * completes in well under a microsecond */ +static uint8_t mb_spi_byte(uint8_t v) +{ + uint32_t to = MB_SPI_TIMEOUT; + while (!(SPI2->SR & SPI_SR_TXE)) { if (!--to) { mb_spi_err = 1; return 0xFFu; } } + *(volatile uint8_t *)&SPI2->DR = v; + to = MB_SPI_TIMEOUT; + while (!(SPI2->SR & SPI_SR_RXNE)) { if (!--to) { mb_spi_err = 1; return 0xFFu; } } + return *(volatile uint8_t *)&SPI2->DR; +} + +/* + * Put SPI2 into clean polled mode before a manual read. The flash driver leaves + * SPI2 in "DMA mode": the RX/TX DMA requests stay on and the DMA channels stay + * armed (confirmed by the SPI-state diagnostic: RD.EN=1 WR.EN=1). A polled read + * then loses every received byte to the still-armed DMA, so RXNE never sets and + * the read hangs forever. Disabling the DMA requests + channels and draining the + * RX FIFO restores plain polled behaviour. The next driver operation re-arms DMA + * on its own, so this is safe. + */ +static void mb_spi_polled_mode(void) +{ + SPI2->CR2 &= ~(SPI_CR2_RXDMAEN | SPI_CR2_TXDMAEN); + DMA1_Channel4->CCR &= ~DMA_CCR_EN; + DMA1_Channel5->CCR &= ~DMA_CCR_EN; + /* Drain any pending RX. Bounded: the RX FIFO is only a few bytes deep, so a + * stuck/overrun RXNE (which would otherwise loop forever) can't hang here. */ + for (uint32_t guard = 64; (SPI2->SR & SPI_SR_RXNE) && guard; guard--) + (void)*(volatile uint8_t *)&SPI2->DR; +} + +/* + * CRC-32 of `len` bytes of external flash starting at `addr`, read in ONE + * continuous polled transfer (command 0x03, CS held low, auto-incrementing + * address). This avoids issuing hundreds of tiny back-to-back DMA reads through + * PY25Q16_ReadBuffer(), which is not reliable at that rate. + */ +#define MB_READ_CHUNK 2048u + +static uint32_t mb_ext_image_crc32(uint32_t addr, uint32_t len) +{ + uint32_t crc = 0xFFFFFFFFu; + + mb_spi_polled_mode(); + + /* Read in chunks. IRQs are masked during each chunk's continuous transfer + * (an interrupt mid-transfer desyncs the polled SPI - the same reason the + * RAM copier masks them), but re-enabled between chunks so the USB stack + * keeps being serviced and the reply can go out afterwards. */ + while (len && !mb_spi_err) + { + uint32_t chunk = (len < MB_READ_CHUNK) ? len : MB_READ_CHUNK; + uint32_t primask = __get_PRIMASK(); + __disable_irq(); + + GPIOA->BRR = MB_CS_PIN; /* CS low */ + mb_spi_byte(0x03u); + mb_spi_byte((addr >> 16) & 0xFFu); + mb_spi_byte((addr >> 8) & 0xFFu); + mb_spi_byte(addr & 0xFFu); + for (uint32_t i = 0; i < chunk && !mb_spi_err; i++) + { + crc ^= mb_spi_byte(0xFFu); + for (int k = 0; k < 8; k++) + crc = (crc >> 1) ^ (0xEDB88320u & (0u - (crc & 1u))); + } + GPIOA->BSRR = MB_CS_PIN; /* CS high */ + + __set_PRIMASK(primask); /* let IRQs / USB breathe */ + addr += chunk; + len -= chunk; + } + + return crc ^ 0xFFFFFFFFu; +} + +/* Polled read of `len` bytes from external flash into `buf` (no DMA), matching + * the technique the RAM copier uses. Sets mb_spi_err on a stuck SPI. */ +static void mb_ext_read(uint32_t addr, uint8_t *buf, uint32_t len) +{ + mb_spi_polled_mode(); + + /* IRQs off during the transfer (see mb_ext_image_crc32); break on timeout. */ + uint32_t primask = __get_PRIMASK(); + __disable_irq(); + + GPIOA->BRR = MB_CS_PIN; + mb_spi_byte(0x03u); + mb_spi_byte((addr >> 16) & 0xFFu); + mb_spi_byte((addr >> 8) & 0xFFu); + mb_spi_byte(addr & 0xFFu); + while (len-- && !mb_spi_err) + *buf++ = mb_spi_byte(0xFFu); + GPIOA->BSRR = MB_CS_PIN; + + __set_PRIMASK(primask); +} + +/* -------------------------------------------------------------------------- */ +/* External flash raw erase/program (polled, flash-resident). */ +/* */ +/* Used only by the M4 slot-management commands. These bypass the stateful */ +/* PY25Q16 driver (its sector cache would desync when we erase and program a */ +/* slot behind its back, and its per-chunk read-modify-write would erase a */ +/* sector on every small chunk). Each CS-framed transaction masks IRQs for */ +/* its own burst only - an interrupt mid-transfer desyncs the polled SPI, the */ +/* same reason mb_ext_image_crc32 masks them - and re-enables them between */ +/* transactions so USB keeps being serviced (notably across the long erase). */ +/* -------------------------------------------------------------------------- */ + +#define MB_EXT_CMD_WREN 0x06u /* write enable */ +#define MB_EXT_CMD_PP 0x02u /* page program (<=256 B) */ +#define MB_EXT_CMD_SE 0x20u /* 4 KiB sector erase */ +#define MB_EXT_CMD_RDSR 0x05u /* read status register 1 */ +#define MB_EXT_SECTOR 0x1000u /* PY25Q16 erase granularity */ +#define MB_EXT_PAGE 0x100u /* PY25Q16 program granularity */ +#define MB_EXT_WIP_TIMEOUT 5000000u /* status polls before giving up (~seconds) */ + +static void mb_ext_wren(void) +{ + uint32_t primask = __get_PRIMASK(); + __disable_irq(); + GPIOA->BRR = MB_CS_PIN; + mb_spi_byte(MB_EXT_CMD_WREN); + GPIOA->BSRR = MB_CS_PIN; + __set_PRIMASK(primask); +} + +/* Poll WIP until the erase/program finishes (or mb_spi_err / timeout). IRQs are + * masked only for each 2-byte status read, not the whole wait, so a ~300 ms + * erase does not starve USB. */ +static bool mb_ext_wait_wip(void) +{ + for (uint32_t i = 0; i < MB_EXT_WIP_TIMEOUT; i++) + { + uint32_t primask = __get_PRIMASK(); + __disable_irq(); + GPIOA->BRR = MB_CS_PIN; + mb_spi_byte(MB_EXT_CMD_RDSR); + uint8_t status = mb_spi_byte(0xFFu); + GPIOA->BSRR = MB_CS_PIN; + __set_PRIMASK(primask); + + if (mb_spi_err) + return false; + if (!(status & 1u)) /* WIP clear */ + return true; + } + return false; +} + +static bool mb_ext_sector_erase(uint32_t addr) +{ + mb_ext_wren(); + if (mb_spi_err) + return false; + + uint32_t primask = __get_PRIMASK(); + __disable_irq(); + GPIOA->BRR = MB_CS_PIN; + mb_spi_byte(MB_EXT_CMD_SE); + mb_spi_byte((addr >> 16) & 0xFFu); + mb_spi_byte((addr >> 8) & 0xFFu); + mb_spi_byte(addr & 0xFFu); + GPIOA->BSRR = MB_CS_PIN; + __set_PRIMASK(primask); + + if (mb_spi_err) + return false; + return mb_ext_wait_wip(); +} + +/* Program up to one 256-byte page; the caller must not cross a page boundary. */ +static bool mb_ext_page_program(uint32_t addr, const uint8_t *data, uint32_t len) +{ + mb_ext_wren(); + if (mb_spi_err) + return false; + + uint32_t primask = __get_PRIMASK(); + __disable_irq(); + GPIOA->BRR = MB_CS_PIN; + mb_spi_byte(MB_EXT_CMD_PP); + mb_spi_byte((addr >> 16) & 0xFFu); + mb_spi_byte((addr >> 8) & 0xFFu); + mb_spi_byte(addr & 0xFFu); + for (uint32_t i = 0; i < len && !mb_spi_err; i++) + mb_spi_byte(data[i]); + GPIOA->BSRR = MB_CS_PIN; + __set_PRIMASK(primask); + + if (mb_spi_err) + return false; + return mb_ext_wait_wip(); +} + +/* Program an arbitrary range, split on 256-byte page boundaries (a page program + * that crosses a page boundary wraps within the page instead of advancing). */ +static bool mb_ext_program(uint32_t addr, const uint8_t *data, uint32_t len) +{ + while (len) + { + uint32_t pageRem = MB_EXT_PAGE - (addr & (MB_EXT_PAGE - 1u)); + uint32_t n = (len < pageRem) ? len : pageRem; + if (!mb_ext_page_program(addr, data, n)) + return false; + addr += n; + data += n; + len -= n; + } + return true; +} + +/* Read + validate a slot header only (no CRC recompute), via polled reads. */ +static uint8_t mb_read_header(uint8_t slot, mb_slot_header_t *hdr) +{ + if (slot >= MB_SLOT_COUNT) + return MB_ERR_SLOT; + + const uint32_t slotBase = MB_SLOT0_EXT_BASE + (uint32_t)slot * MB_SLOT_STRIDE; + + mb_spi_err = 0; + mb_ext_read(slotBase, (uint8_t *)hdr, sizeof(*hdr)); + if (mb_spi_err) return MB_ERR_SPI; + if (hdr->magic != MB_SLOT_MAGIC) return MB_ERR_MAGIC; + if (hdr->hdr_version > MB_HDR_VERSION) return MB_ERR_VERSION; + if (!(hdr->flags & MB_FLAG_COMMITTED)) return MB_ERR_NOT_COMMITTED; + if (hdr->image_size == 0 || hdr->image_size > MB_INT_APP_SIZE) return MB_ERR_SIZE; + return MB_OK; +} + +/* Validate one slot (header + image CRC-32), entirely via polled reads. + * Never touches the internal flash. */ +static uint8_t mb_validate(uint8_t slot, mb_slot_header_t *hdr, uint32_t *crcOut) +{ + uint8_t err = mb_read_header(slot, hdr); + if (err != MB_OK) + return err; + + const uint32_t slotBase = MB_SLOT0_EXT_BASE + (uint32_t)slot * MB_SLOT_STRIDE; + + mb_spi_err = 0; + uint32_t crc = mb_ext_image_crc32(slotBase + MB_SLOT_IMG_OFFSET, hdr->image_size); + if (crcOut) + *crcOut = crc; + if (mb_spi_err) return MB_ERR_SPI; + if (crc != hdr->image_crc32) return MB_ERR_CRC; + return MB_OK; +} + +uint8_t MB_ValidateSlot(uint8_t slot, mb_slot_header_t *out_header, uint32_t *out_crc) +{ + mb_slot_header_t local; + return mb_validate(slot, out_header ? out_header : &local, out_crc); +} + +uint8_t MB_RestoreSlot(uint8_t slot, uint8_t *progress_line) +{ + mb_slot_header_t hdr; + uint8_t err = mb_validate(slot, &hdr, NULL); + if (err != MB_OK) + return err; + + const uint32_t slotBase = MB_SLOT0_EXT_BASE + (uint32_t)slot * MB_SLOT_STRIDE; + +#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY + /* No PY25Q16 driver access is allowed after this point: the sector cache is + * about to become executable storage and the RAM stub always ends in reset. + * Mask IRQs first, then explicitly stop SPI2 DMA before overwriting the + * cache. Keeping both operations local avoids relying on validation's + * current polled-SPI implementation and closes any IRQ re-arm window. */ + const uint32_t primask = __get_PRIMASK(); + __disable_irq(); + mb_spi_polled_mode(); + PY25Q16_InvalidateCache(); + if (!mb_load_ram_reflash_overlay()) + { + __set_PRIMASK(primask); + return MB_ERR_RAM_LOAD; + } +#endif + + /* Valid and, for the overlay path, safely loaded: the RAM stub copies + * exactly image_size bytes, pads the partial page with 0xFF and erases the + * remainder of the application region. Keep flash locked until this point + * so an overlay-copy failure can return without changing flash state. */ + MB_PrepareInternalFlash(); + + /* Call through a volatile pointer so the compiler emits an absolute 'blx' + * (the RAM copy sits far beyond a Cortex-M0+ 'bl' reach from flash). */ + void (*volatile ramReflash)(uint32_t, uint32_t, uint32_t, uint8_t *) = MB_RamReflash; + ramReflash(MB_INT_APP_BASE, slotBase + MB_SLOT_IMG_OFFSET, + hdr.image_size, progress_line); + + return MB_OK; /* not reached */ +} + +/* -------------------------------------------------------------------------- */ +/* M4 slot management (host tool). External flash only - never brick-critical.*/ +/* -------------------------------------------------------------------------- */ + +uint8_t MB_SlotInfo(uint8_t slot, mb_slot_header_t *out_header) +{ + mb_slot_header_t local; + return mb_read_header(slot, out_header ? out_header : &local); +} + +uint8_t MB_SlotErase(uint8_t slot) +{ + /* Slot 0 is the firmware-managed base backup: never erasable from the host. */ + if (slot >= MB_SLOT_COUNT || slot == MB_SLOT_BACKUP) + return MB_ERR_SLOT; + + const uint32_t base = MB_SLOT0_EXT_BASE + (uint32_t)slot * MB_SLOT_STRIDE; + + mb_spi_err = 0; + mb_spi_polled_mode(); + for (uint32_t off = 0; off < MB_SLOT_STRIDE; off += MB_EXT_SECTOR) + if (!mb_ext_sector_erase(base + off)) + return MB_ERR_SPI; + + return MB_OK; +} + +uint8_t MB_SlotWrite(uint8_t slot, uint32_t offset, const uint8_t *data, uint32_t len) +{ + /* Slot 0 is the firmware-managed base backup: never writable from the host. */ + if (slot >= MB_SLOT_COUNT || slot == MB_SLOT_BACKUP) + return MB_ERR_SLOT; + if (len == 0) + return MB_OK; + if (offset > MB_SLOT_STRIDE || len > MB_SLOT_STRIDE - offset) + return MB_ERR_SIZE; + + const uint32_t base = MB_SLOT0_EXT_BASE + (uint32_t)slot * MB_SLOT_STRIDE; + + mb_spi_err = 0; + mb_spi_polled_mode(); + if (!mb_ext_program(base + offset, data, len)) + return MB_ERR_SPI; + + return MB_OK; +} + +/* -------------------------------------------------------------------------- */ +/* Config banks and the active-state marker. */ +/* */ +/* The banking offset itself lives in the flash driver (PY25Q16_SetBankBase); */ +/* here we only own the active-state marker and the bank -> base mapping. */ +/* All external-flash only, never brick-critical. */ +/* -------------------------------------------------------------------------- */ + +_Static_assert(sizeof(mb_state_t) == 24u, + "FMP2/FMP3 marker layout must stay 24 bytes"); +_Static_assert(offsetof(mb_state_t, firmware_slot) == 16u, + "FMP2 migration reads the coupled index at byte 16"); +_Static_assert(offsetof(mb_state_t, state_crc32) == 20u, + "state CRC must cover the first 20 bytes (magic..bank_inv)"); + +uint32_t MB_Crc32Bytes(const uint8_t *p, uint32_t len) +{ + uint32_t crc = 0xFFFFFFFFu; + + for (uint32_t i = 0; i < len; i++) + { + crc ^= p[i]; + for (int k = 0; k < 8; k++) + crc = (crc >> 1) ^ (0xEDB88320u & (0u - (crc & 1u))); + } + return crc ^ 0xFFFFFFFFu; +} + +uint32_t MB_BankBase(uint8_t bank) +{ + if (bank == 0) + return 0; /* bank 0 = historical config region */ + if (bank < MB_BANK_COUNT) + return MB_BANK1_EXT_BASE + (uint32_t)(bank - 1u) * MB_BANK_SIZE; + return 0; /* out of range -> safe default */ +} + +static mb_mark_status_t mb_read_state_copy(uint32_t base, mb_state_t *st) +{ + mb_spi_err = 0; + mb_ext_read(base, (uint8_t *)st, sizeof(*st)); + if (mb_spi_err) return MB_MARK_IO; + if (st->magic == 0xFFFFFFFFu) return MB_MARK_MISSING; + if (st->magic == MB_STATE_LEGACY_MAGIC) + { + /* FMP1 was { magic, index, ~index, reserved[2] }, with index coupling the + * firmware slot and the config bank. Keep it as BOTH fields so boot + * resolution can honour that choice before migrating the record to FMP3. */ + const uint8_t legacy_index = ((const uint8_t *)st)[4]; + const uint8_t legacy_inv = ((const uint8_t *)st)[5]; + if ((uint8_t)~legacy_inv != legacy_index || legacy_index >= MB_BANK_COUNT) + return MB_MARK_CORRUPT; + memset(st, 0, sizeof(*st)); + st->magic = MB_STATE_LEGACY_MAGIC; + st->firmware_slot = legacy_index; + st->slot_inv = legacy_inv; + st->config_bank = legacy_index; + st->bank_inv = legacy_inv; + return MB_MARK_LEGACY; + } + + if (st->magic == MB_STATE_V2_MAGIC) + { + /* FMP2 stored one index for both the firmware slot and config bank at + * byte 16, followed by its inverse and two zeroed reserved bytes. + * Validate the on-flash record before normalizing it in RAM to FMP3. */ + const uint8_t legacy_index = ((const uint8_t *)st)[16]; + const uint8_t legacy_inv = ((const uint8_t *)st)[17]; + if ((uint8_t)~legacy_inv != legacy_index || + legacy_index >= MB_BANK_COUNT) + return MB_MARK_CORRUPT; + if (st->image_size == 0u || st->image_size > MB_INT_APP_SIZE) + return MB_MARK_CORRUPT; + if (MB_Crc32Bytes((const uint8_t *)st, + sizeof(*st) - sizeof(st->state_crc32)) != st->state_crc32) + return MB_MARK_CORRUPT; + + st->magic = MB_STATE_MAGIC; + st->firmware_slot = legacy_index; + st->slot_inv = (uint8_t)~legacy_index; + st->config_bank = legacy_index; + st->bank_inv = (uint8_t)~legacy_index; + st->state_crc32 = MB_Crc32Bytes((const uint8_t *)st, + sizeof(*st) - sizeof(st->state_crc32)); + return MB_MARK_VALID; + } + + if (st->magic != MB_STATE_MAGIC) return MB_MARK_CORRUPT; + if ((uint8_t)~st->slot_inv != st->firmware_slot) + return MB_MARK_CORRUPT; + if (st->firmware_slot >= MB_SLOT_COUNT) return MB_MARK_CORRUPT; + if ((uint8_t)~st->bank_inv != st->config_bank) + return MB_MARK_CORRUPT; + if (st->config_bank >= MB_BANK_COUNT) return MB_MARK_CORRUPT; + if (st->image_size == 0u || + st->image_size > MB_INT_APP_SIZE) return MB_MARK_CORRUPT; + if (MB_Crc32Bytes((const uint8_t *)st, + sizeof(*st) - sizeof(st->state_crc32)) != st->state_crc32) + return MB_MARK_CORRUPT; + return MB_MARK_VALID; +} + +static bool mb_generation_newer(uint32_t a, uint32_t b) +{ + return (int32_t)(a - b) > 0; +} + +static mb_mark_status_t mb_read_active_state(mb_state_t *state, + uint32_t *state_base) +{ + mb_state_t a; + mb_state_t b; + mb_mark_status_t sa = mb_read_state_copy(MB_STATE_A_BASE, &a); + mb_mark_status_t sb = mb_read_state_copy(MB_STATE_B_BASE, &b); + + /* If either sector could not be read, it might contain the newest record. + * Do not silently select an older state and risk loading the wrong bank. */ + if (sa == MB_MARK_IO || sb == MB_MARK_IO) + return MB_MARK_IO; + + const mb_state_t *chosen = NULL; + uint32_t chosen_base = 0; + if (sa == MB_MARK_VALID && sb == MB_MARK_VALID) + { + if (mb_generation_newer(b.generation, a.generation)) + { + chosen = &b; + chosen_base = MB_STATE_B_BASE; + } + else + { + chosen = &a; + chosen_base = MB_STATE_A_BASE; + } + } + else if (sa == MB_MARK_VALID) + { + chosen = &a; + chosen_base = MB_STATE_A_BASE; + } + else if (sb == MB_MARK_VALID) + { + chosen = &b; + chosen_base = MB_STATE_B_BASE; + } + + if (chosen) + { + if (state) + *state = *chosen; + if (state_base) + *state_base = chosen_base; + return MB_MARK_VALID; + } + + /* A legacy record is usable for migration but has no firmware identity. + * Prefer A if both somehow exist; the next successful repair writes FMP3. */ + if (sa == MB_MARK_LEGACY || sb == MB_MARK_LEGACY) + { + const bool use_b = sa != MB_MARK_LEGACY; + if (state) + *state = use_b ? b : a; + if (state_base) + *state_base = use_b ? MB_STATE_B_BASE : MB_STATE_A_BASE; + return MB_MARK_LEGACY; + } + + return (sa == MB_MARK_MISSING && sb == MB_MARK_MISSING) + ? MB_MARK_MISSING : MB_MARK_CORRUPT; +} + +mb_mark_status_t MB_ReadActiveState(mb_state_t *state) +{ + return mb_read_active_state(state, NULL); +} + +/* Commit a marker whose identity, firmware_slot and config_bank are set: + * fill the volatile fields (magic, generation, inverse bytes, state CRC), pick + * the inactive redundant sector, program it and read it back to confirm. The + * previous valid record is left untouched until the new one verifies, so this is + * power-loss safe. `current`/`current_base` come from mb_read_active_state. */ +static uint8_t mb_commit_state(mb_state_t *st, + mb_mark_status_t current_status, + const mb_state_t *current, + uint32_t current_base) +{ + st->magic = MB_STATE_MAGIC; + st->generation = (current_status == MB_MARK_VALID) ? current->generation + 1u : 0u; + st->slot_inv = (uint8_t)~st->firmware_slot; + st->bank_inv = (uint8_t)~st->config_bank; + st->state_crc32 = MB_Crc32Bytes((const uint8_t *)st, + sizeof(*st) - sizeof(st->state_crc32)); + + const uint32_t target_base = ((current_status == MB_MARK_VALID || + current_status == MB_MARK_LEGACY) && + current_base == MB_STATE_A_BASE) + ? MB_STATE_B_BASE + : MB_STATE_A_BASE; + + mb_spi_err = 0; + mb_spi_polled_mode(); + if (!mb_ext_sector_erase(target_base)) + return MB_ERR_SPI; + if (!mb_ext_program(target_base, (const uint8_t *)st, sizeof(*st))) + return MB_ERR_SPI; + + /* Verify the new copy directly. The previous valid sector has not been + * touched, so any failure here remains power-loss safe. */ + mb_state_t chk; + mb_mark_status_t chk_status = mb_read_state_copy(target_base, &chk); + if (chk_status == MB_MARK_IO) return MB_ERR_SPI; + if (chk_status != MB_MARK_VALID || + memcmp(&chk, st, sizeof(*st)) != 0) return MB_ERR_CRC; + + return MB_OK; +} + +uint8_t MB_SetActiveSlot(uint8_t slot) +{ + mb_slot_header_t hdr; + mb_state_t st; + mb_state_t current; + uint32_t current_base = 0; + + if (slot >= MB_SLOT_COUNT) + return MB_ERR_SLOT; + + uint8_t hdr_err = mb_read_header(slot, &hdr); + if (hdr_err != MB_OK) + return hdr_err; + + mb_mark_status_t current_status = mb_read_active_state(¤t, ¤t_base); + if (current_status == MB_MARK_IO) + return MB_ERR_SPI; + + memset(&st, 0, sizeof(st)); + st.image_size = hdr.image_size; /* expect this slot's image (reflash) */ + st.image_crc32 = hdr.image_crc32; + st.firmware_slot = slot; + st.config_bank = slot; + return mb_commit_state(&st, current_status, ¤t, current_base); +} + +uint8_t MB_SetActiveBank(uint8_t bank) +{ + mb_state_t st; + mb_state_t current; + uint32_t current_base = 0; + + if (bank >= MB_BANK_COUNT) + return MB_ERR_SLOT; + + /* Keep the running firmware's identity from the current marker so switching a + * bank never looks like an out-of-multiboot firmware change at the next boot. + * A valid marker is required for that identity - every normal boot leaves one. */ + mb_mark_status_t current_status = mb_read_active_state(¤t, ¤t_base); + if (current_status == MB_MARK_IO) + return MB_ERR_SPI; + if (current_status != MB_MARK_VALID) + return MB_ERR_MAGIC; + + memset(&st, 0, sizeof(st)); + st.image_size = current.image_size; /* keep the running firmware's identity */ + st.image_crc32 = current.image_crc32; + st.firmware_slot = current.firmware_slot; + st.config_bank = bank; + return mb_commit_state(&st, current_status, ¤t, current_base); +} + +uint8_t MB_BankErase(uint8_t bank) +{ + /* Bank 0 (the base config) is not resettable from the host: reset it by + * factory-resetting the running base firmware instead. Banks 1..N live + * in their own 64 KiB banks, clear of the shared calibration at 0x010000. */ + if (bank == 0 || bank >= MB_BANK_COUNT) + return MB_ERR_SLOT; + + const uint32_t base = MB_BankBase(bank); + + /* Invalidate before the first raw erase so an early failure cannot leave a + * cache entry referring to a sector that was already erased. */ + PY25Q16_InvalidateCache(); + + mb_spi_err = 0; + mb_spi_polled_mode(); + for (uint32_t off = 0; off < MB_BANK_SIZE; off += MB_EXT_SECTOR) + if (!mb_ext_sector_erase(base + off)) + return MB_ERR_SPI; + + return MB_OK; +} + +/* -------------------------------------------------------------------------- */ +/* Slot 0 self-backup (base firmware). */ +/* -------------------------------------------------------------------------- */ + +/* Bounded copy of a NUL-terminated string into a fixed field, zero-padded. */ +static void mb_copy_str(char *dst, uint8_t cap, const char *src) +{ + uint8_t n = 0; + while (n + 1u < cap && src[n]) + { + dst[n] = src[n]; + n++; + } + while (n < cap) + dst[n++] = 0; +} + +/* CRC-32 (zlib) of the internal application flash, which is memory-mapped so + * no SPI is involved. Same polynomial as the external slot CRC. */ +static uint32_t mb_int_image_crc32(uint32_t len) +{ + const uint8_t *p = (const uint8_t *)MB_INT_APP_BASE; + return MB_Crc32Bytes(p, len); +} + +static bool mb_internal_matches(uint32_t image_size, uint32_t image_crc32) +{ + return mb_int_image_crc32(image_size) == image_crc32; +} + +mb_fw_match_t MB_InternalMatchesSlot(uint8_t slot) +{ + mb_slot_header_t hdr; + uint8_t err = mb_read_header(slot, &hdr); + + if (err == MB_ERR_SPI) + return MB_FW_IO; /* couldn't read the header: never conclude mismatch */ + if (err != MB_OK) + return MB_FW_MISMATCH; /* no valid header: definitely not this firmware */ + + return mb_internal_matches(hdr.image_size, hdr.image_crc32) + ? MB_FW_MATCH : MB_FW_MISMATCH; +} + +bool MB_InternalMatchesState(const mb_state_t *state) +{ + if (!state || state->image_size == 0u || state->image_size > MB_INT_APP_SIZE) + return false; + return mb_internal_matches(state->image_size, state->image_crc32); +} + +uint8_t MB_BackupInternalToSlot0(mb_progress_fn progress) +{ + const uint8_t *img = (const uint8_t *)MB_INT_APP_BASE; + const uint32_t size = MB_INT_APP_SIZE; + const uint32_t base = MB_SLOT0_EXT_BASE + (uint32_t)MB_SLOT_BACKUP * MB_SLOT_STRIDE; + + mb_spi_err = 0; + mb_spi_polled_mode(); + + /* Erase the header sector + image area (rounded up to 4 KiB sectors). */ + for (uint32_t off = 0; off < MB_SLOT_IMG_OFFSET + size; off += MB_EXT_SECTOR) + if (!mb_ext_sector_erase(base + off)) + return MB_ERR_SPI; + + /* Program the whole internal application region verbatim, in chunks, + * reporting progress. It is an exact copy of what executes, so restoring + * it later reproduces the running firmware bit-for-bit. */ + for (uint32_t done = 0; done < size; ) + { + uint32_t n = (size - done < MB_EXT_SECTOR) ? (size - done) : MB_EXT_SECTOR; + if (!mb_ext_program(base + MB_SLOT_IMG_OFFSET + done, img + done, n)) + return MB_ERR_SPI; + done += n; + if (progress) + progress(done, size); + } + + /* Validate the stored image before committing its header. Until that final + * header write the slot remains invalid, so a failed CRC or interrupted + * backup is guaranteed to retry at the next boot. */ + const uint32_t image_crc = mb_int_image_crc32(size); + uint32_t ext_crc = mb_ext_image_crc32(base + MB_SLOT_IMG_OFFSET, size); + if (mb_spi_err) return MB_ERR_SPI; + if (ext_crc != image_crc) return MB_ERR_CRC; + + /* Write the COMMITTED header only after the external image verified. */ + mb_slot_header_t hdr; + memset(&hdr, 0, sizeof(hdr)); + hdr.magic = MB_SLOT_MAGIC; + hdr.hdr_version = MB_HDR_VERSION; + hdr.flags = MB_FLAG_COMMITTED; + hdr.image_size = size; + hdr.image_crc32 = image_crc; + mb_copy_str(hdr.name, MB_NAME_LEN, EDITION_STRING); + mb_copy_str(hdr.fw_version, MB_VERSION_LEN, VERSION_STRING_2); + + if (!mb_ext_program(base, (const uint8_t *)&hdr, sizeof(hdr))) + return MB_ERR_SPI; + + return MB_OK; +} diff --git a/App/driver/mb_flash.h b/App/driver/mb_flash.h new file mode 100644 index 00000000..14b3bdf3 --- /dev/null +++ b/App/driver/mb_flash.h @@ -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 +#include + +/* 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 */ diff --git a/App/driver/py25q16.c b/App/driver/py25q16.c index 3d9990a1..618d08bc 100644 --- a/App/driver/py25q16.c +++ b/App/driver/py25q16.c @@ -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)); diff --git a/App/driver/py25q16.h b/App/driver/py25q16.h index fae064e0..73631588 100644 --- a/App/driver/py25q16.h +++ b/App/driver/py25q16.h @@ -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 diff --git a/App/driver/st7565.c b/App/driver/st7565.c index 23d93e48..0736f43c 100644 --- a/App/driver/st7565.c +++ b/App/driver/st7565.c @@ -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 diff --git a/App/functions.c b/App/functions.c index 32be4045..68a2bbc0 100644 --- a/App/functions.c +++ b/App/functions.c @@ -17,13 +17,13 @@ #include #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 diff --git a/App/helper/battery.c b/App/helper/battery.c index c02a1814..244078cd 100644 --- a/App/helper/battery.c +++ b/App/helper/battery.c @@ -17,6 +17,7 @@ #include #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; diff --git a/App/helper/battery.h b/App/helper/battery.h index eb7515a8..28fa412e 100644 --- a/App/helper/battery.h +++ b/App/helper/battery.h @@ -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); diff --git a/App/helper/boot.c b/App/helper/boot.c index 838cfb13..97a70e87 100644 --- a/App/helper/boot.c +++ b/App/helper/boot.c @@ -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); } diff --git a/App/helper/boot.h b/App/helper/boot.h index f36448fd..1c21fcb2 100644 --- a/App/helper/boot.h +++ b/App/helper/boot.h @@ -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 }; diff --git a/App/k5viewer.c b/App/k5viewer.c index 5752ad34..2167dc40 100644 --- a/App/k5viewer.c +++ b/App/k5viewer.c @@ -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 { diff --git a/App/main.c b/App/main.c index fba5be1e..c28fe525 100644 --- a/App/main.c +++ b/App/main.c @@ -18,10 +18,6 @@ #include #include // 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) { diff --git a/App/misc.c b/App/misc.c index 63242354..e1b8987e 100644 --- a/App/misc.c +++ b/App/misc.c @@ -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 diff --git a/App/misc.h b/App/misc.h index 465aa296..dd5d6e3a 100644 --- a/App/misc.h +++ b/App/misc.h @@ -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; diff --git a/App/radio.c b/App/radio.c index d1cc3cd3..fb84c2d9 100644 --- a/App/radio.c +++ b/App/radio.c @@ -17,12 +17,11 @@ #include "driver/bk4819-regs.h" #include -#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 { diff --git a/App/radio.h b/App/radio.h index 5d53b399..cb61f582 100644 --- a/App/radio.h +++ b/App/radio.h @@ -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 }; diff --git a/App/scheduler.c b/App/scheduler.c index 0d2031a1..3401e5ad 100644 --- a/App/scheduler.c +++ b/App/scheduler.c @@ -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); diff --git a/App/settings.c b/App/settings.c index 6cd43647..56a051f0 100644 --- a/App/settings.c +++ b/App/settings.c @@ -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) { diff --git a/App/settings.h b/App/settings.h index a4e4bcb2..3aa2d734 100644 --- a/App/settings.h +++ b/App/settings.h @@ -25,6 +25,10 @@ #include "radio.h" #include +/* 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 diff --git a/App/ui/aircopy.c b/App/ui/aircopy.c index 135d95c5..daba1a45 100644 --- a/App/ui/aircopy.c +++ b/App/ui/aircopy.c @@ -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 \ No newline at end of file +#endif diff --git a/App/ui/fmradio.c b/App/ui/fmradio.c index 38d9da06..684a182f 100644 --- a/App/ui/fmradio.c +++ b/App/ui/fmradio.c @@ -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 { diff --git a/App/ui/helper.c b/App/ui/helper.c index 1de0067b..ca4392b2 100644 --- a/App/ui/helper.c +++ b/App/ui/helper.c @@ -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)); } diff --git a/App/ui/helper.h b/App/ui/helper.h index 0b62abb6..5dc4ecea 100644 --- a/App/ui/helper.h +++ b/App/ui/helper.h @@ -20,7 +20,6 @@ #include #include -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 diff --git a/App/ui/main.c b/App/ui/main.c index 4202d29f..ab28353b 100644 --- a/App/ui/main.c +++ b/App/ui/main.c @@ -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; diff --git a/App/ui/main.h b/App/ui/main.h index db839d06..e3e7a932 100644 --- a/App/ui/main.h +++ b/App/ui/main.h @@ -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 diff --git a/App/ui/menu.c b/App/ui/menu.c index cb16b751..1c355706 100644 --- a/App/ui/menu.c +++ b/App/ui/menu.c @@ -14,9 +14,11 @@ * limitations under the License. */ +#include #include #include +#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!"; diff --git a/App/ui/menu.h b/App/ui/menu.h index 9cd676ed..2f75f736 100644 --- a/App/ui/menu.h +++ b/App/ui/menu.h @@ -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 diff --git a/App/ui/multiboot.c b/App/ui/multiboot.c new file mode 100644 index 00000000..aeb08a05 --- /dev/null +++ b/App/ui/multiboot.c @@ -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 + +#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); +} diff --git a/App/ui/multiboot.h b/App/ui/multiboot.h new file mode 100644 index 00000000..596af5b6 --- /dev/null +++ b/App/ui/multiboot.h @@ -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 + +/* 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 diff --git a/App/ui/status.c b/App/ui/status.c index 94b2e0f4..789deee3 100644 --- a/App/ui/status.c +++ b/App/ui/status.c @@ -21,7 +21,7 @@ #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 "app/scanner.h" @@ -54,6 +54,29 @@ static void convertTime(uint8_t *line, uint8_t type) #endif #endif +#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) +void UI_DrawStatusBattery(uint8_t *line, char *str) +#else +static __attribute__((always_inline)) inline void UI_DrawStatusBattery(uint8_t *line, char *str) +#endif +{ + unsigned int x = LCD_WIDTH - sizeof(BITMAP_BatteryLevel1); + + UI_DrawBattery(line + x, gBatteryDisplayLevel, gLowBatteryBlink); + + if (gSetting_battery_text == 1) { + const uint16_t voltage = MIN(gBatteryVoltageAverage, 999); + sprintf(str, "%u.%02u", voltage / 100, voltage % 100); + } else if (gSetting_battery_text == 2) { + sprintf(str, "%02u%%", BATTERY_VoltsToPercent(gBatteryVoltageAverage)); + } else { + return; + } + + x -= 7 * strlen(str); + UI_PrintStringSmallBufferNormal(str, line + x); +} + void UI_DisplayStatus() { gUpdateStatus = false; @@ -314,39 +337,7 @@ void UI_DisplayStatus() memcpy(line + x + 1, src, size); } - // Battery - unsigned int x2 = LCD_WIDTH - sizeof(BITMAP_BatteryLevel1) - 0; - - UI_DrawBattery(line + x2, gBatteryDisplayLevel, gLowBatteryBlink); - - switch (gSetting_battery_text) { - default: - case 0: - break; - - case 1: // voltage - case 2: // percentage - if (gSetting_battery_text == 1) { - const uint16_t voltage = MIN(gBatteryVoltageAverage, 999); // limit to 9.99V - sprintf(str, "%u.%02u", voltage / 100, voltage % 100); - } else { - //gBatteryVoltageAverage = 999; - sprintf(str, "%02u%%", BATTERY_VoltsToPercent(gBatteryVoltageAverage)); - } - - x2 -= (7 * strlen(str)); - UI_PrintStringSmallBufferNormal(str, line + x2); - /* - uint8_t shift = (strlen(str) < 5) ? 92 : 88; - GUI_DisplaySmallest(str, shift, 1, true, true); - - for (uint8_t i = shift - 2; i < 110; i++) { - gStatusLine[i] ^= 0x7F; // invert - } - */ - - break; - } + UI_DrawStatusBattery(line, str); // ************** diff --git a/App/ui/status.h b/App/ui/status.h index ae71d1db..84bb71fa 100644 --- a/App/ui/status.h +++ b/App/ui/status.h @@ -17,7 +17,11 @@ #ifndef UI_STATUS_H #define UI_STATUS_H +#include + +#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) +void UI_DrawStatusBattery(uint8_t *line, char *str); +#endif void UI_DisplayStatus(); #endif - diff --git a/App/ui/ui.c b/App/ui/ui.c index 489c7613..b509d2ec 100644 --- a/App/ui/ui.c +++ b/App/ui/ui.c @@ -19,7 +19,7 @@ #include "app/chFrScanner.h" #include "app/dtmf.h" -#ifdef ENABLE_FMRADIO +#ifdef ENABLE_FMRADIO_EMBEDDED #include "app/fm.h" #endif #include "driver/keyboard.h" @@ -27,7 +27,7 @@ #ifdef ENABLE_AIRCOPY #include "ui/aircopy.h" #endif -#ifdef ENABLE_FMRADIO +#ifdef ENABLE_FMRADIO_EMBEDDED #include "ui/fmradio.h" #endif #ifdef ENABLE_FEAT_F4HWN_RXTX_LOG @@ -53,8 +53,14 @@ void (*const UI_DisplayFunctions[])(void) = { [DISPLAY_MENU] = &UI_DisplayMenu, [DISPLAY_SCANNER] = &UI_DisplayScanner, -#ifdef ENABLE_FMRADIO +#ifdef ENABLE_FMRADIO_EMBEDDED [DISPLAY_FM] = &UI_DisplayFM, +#elif defined(ENABLE_FMRADIO) + /* The FM overlay app replaces the resident FM screen (ui/fmradio.c is not + compiled). DISPLAY_FM still exists in the enum, so the slot must stay + initialised to keep ARRAY_SIZE == DISPLAY_N_ELEM; point it at a + never-reached stub - the resident FM screen can no longer open. */ + [DISPLAY_FM] = &UI_DisplayMain, #endif #ifdef ENABLE_AIRCOPY @@ -88,7 +94,7 @@ void GUI_SelectNextDisplay(GUI_DisplayType_t Display) gIsInSubMenu = false; gCssBackgroundScan = false; gScanStateDir = SCAN_OFF; - #ifdef ENABLE_FMRADIO + #ifdef ENABLE_FMRADIO_EMBEDDED gFM_ScanState = FM_SCAN_OFF; #endif gAskForConfirmation = 0; diff --git a/App/ui/welcome.c b/App/ui/welcome.c index 0547f468..a41f45e4 100644 --- a/App/ui/welcome.c +++ b/App/ui/welcome.c @@ -115,15 +115,10 @@ extern uint8_t _edata; // End of .data in RAM extern uint8_t _sbss; // Start of .bss in RAM extern uint8_t _ebss; // End of .bss in RAM -// _eflash_used must be defined in the linker script immediately after the last -// section with a FLASH load address (after .noncacheable). Example: -// -// .noncacheable : { -// ... -// } > RAM AT> FLASH -// _eflash_used = LOADADDR(.noncacheable) + SIZEOF(.noncacheable); -// -// This gives the exact byte count that the linker reports as FLASH used. +// _eflash_used is defined by the linker at the end of the final section with a +// FLASH load image. This is currently .mb_ramfunc (empty without the overlay), +// after the load images for .data and .noncacheable. It therefore gives the +// exact byte count that the linker reports as FLASH used. extern uint8_t _eflash_used; // Absolute symbols: their *address* IS the numeric size value (ARM/CMSIS convention). @@ -152,8 +147,8 @@ static void build_usage(uint32_t* ram_used, uint32_t* flash_used) const uint32_t stack_size = (uint32_t)(uintptr_t)&_Min_Stack_Size; *ram_used = span(&_sdata, &_ebss) + heap_size + stack_size; - // FLASH: _eflash_used is placed by the linker script right after the last - // section copied to FLASH (.data LMA + .noncacheable LMA). + // FLASH: _eflash_used follows the final FLASH load image (.mb_ramfunc, + // after the .data and .noncacheable load images). // Note: _etext is NOT usable here because this linker script places .rodata // sections AFTER _etext, making it an unreliable end-of-flash marker. *flash_used = span((void*)FLASH_BASE, &_eflash_used); @@ -260,15 +255,17 @@ void UI_DisplayWelcome(void) } #endif else { - char WelcomeString0[16]; - char WelcomeString1[16]; + char WelcomeString0[17]; + char WelcomeString1[17]; char WelcomeString2[16]; char WelcomeString3[32]; // 0x0EB0 - PY25Q16_ReadBuffer(0x00A0C8, WelcomeString0, 16); + PY25Q16_ReadBuffer(SETTINGS_BOOT_MESSAGE_LINE1_ADDR, WelcomeString0, 16); + WelcomeString0[16] = '\0'; // 0x0EC0 - PY25Q16_ReadBuffer(0x00A0D8, WelcomeString1, 16); + PY25Q16_ReadBuffer(SETTINGS_BOOT_MESSAGE_LINE2_ADDR, WelcomeString1, 16); + WelcomeString1[16] = '\0'; sprintf(WelcomeString2, "%u.%02uV %u%%", gBatteryVoltageAverage / 100, diff --git a/CMakeLists.txt b/CMakeLists.txt index 0c13acd8..9e1fd523 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -110,10 +110,37 @@ set_target_properties(${EXE_NAME} PROPERTIES ADDITIONAL_CLEAN_FILES ${EXE_NAME}. # Force linker to ignore RWX segment warning even if cache exists target_link_options(${EXE_NAME} PRIVATE -Wl,--no-warn-rwx-segment) +# ENABLE_NO_HEAP: no malloc/sbrk is linked (libc is discarded in py32f071xb.ld), +# so the reserved heap is dead weight. Zero it to reclaim ~512 B of RAM. This is +# a global toolchain optimization, independent of multiboot. The linker script +# reads __min_heap_size when defined, else keeps its 0x200 default. +if(ENABLE_NO_HEAP) + target_link_options(${EXE_NAME} PRIVATE "-Wl,--defsym=__min_heap_size=0") +endif() + # ----------------------------------- # Post build processing # +# The restore stub executes while application flash is unavailable. Reject an +# overlay build if the linked stub branches outside its RAM section or if its +# input section refers to any external code/data. This is deliberately a hard +# build gate: either condition could make a restore unrecoverable without DFU. +if(ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY) + add_custom_command( + TARGET ${EXE_NAME} + POST_BUILD + COMMAND ${CMAKE_COMMAND} + "-DOBJDUMP=${CMAKE_OBJDUMP}" + "-DELF=$" + "-DMAP=${CMAKE_CURRENT_BINARY_DIR}/${EXE_NAME}.map" + "-DBINARY_DIR=${CMAKE_CURRENT_BINARY_DIR}" + -P "${CMAKE_SOURCE_DIR}/cmake/check_mb_ramfunc.cmake" + COMMENT "Checking multiboot RAM stub isolation" + VERBATIM + ) +endif() + # Generate .bin file add_custom_command( TARGET ${EXE_NAME} diff --git a/CMakePresets.json b/CMakePresets.json index 28b3d02d..204c1abe 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -4,6 +4,16 @@ { "name": "default", "hidden": true, + "vendor": { + "github.com/armel/uv-k1-k5v3-firmware-custom/support-policy/1.0": { + "notice": [ + "SUPPORT POLICY", + "Most options outside ENABLE_FEAT_F4HWN_* that are intentionally set to false in this preset are unsupported unless documented otherwise.", + "Enabling them is at your own risk and may cause bugs, side effects, or instability.", + "Do not expect troubleshooting assistance for such configurations." + ] + } + }, "generator": "Ninja", "binaryDir": "${sourceDir}/build/${presetName}", "toolchainFile": "${sourceDir}/cmake/gcc-arm-none-eabi.cmake", @@ -11,6 +21,7 @@ "CMAKE_BUILD_TYPE": "Release", "DEV": false, "ENABLE_LTO": false, + "ENABLE_NO_HEAP": false, "ENABLE_FMRADIO": false, "ENABLE_UART": true, "ENABLE_USB": true, @@ -18,7 +29,6 @@ "ENABLE_NOAA": false, "ENABLE_VOICE": false, "ENABLE_VOX": true, - "ENABLE_ALARM": false, "ENABLE_TX1750": true, "ENABLE_PWRON_PASSWORD": false, "ENABLE_DTMF_CALLING": false, @@ -29,7 +39,6 @@ "ENABLE_CUSTOM_MENU_LAYOUT": true, "ENABLE_KEEP_MEM_NAME": true, "ENABLE_WIDE_RX": true, - "ENABLE_TX_WHEN_AM": false, "ENABLE_F_CAL_MENU": false, "ENABLE_CTCSS_TAIL_PHASE_SHIFT": false, "ENABLE_BOOT_BEEPS": false, @@ -43,7 +52,6 @@ "ENABLE_COPY_CHAN_TO_VFO": true, "ENABLE_REDUCE_LOW_MID_TX_POWER": false, "ENABLE_BYP_RAW_DEMODULATORS": false, - "ENABLE_BLMIN_TMP_OFF": false, "ENABLE_SCAN_RANGES": true, "ENABLE_EXTRA_UART_CMD": false, "ENABLE_FEAT_F4HWN": true, @@ -74,6 +82,7 @@ "ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE": false, "ENABLE_FEAT_F4HWN_BEAM": false, "ENABLE_FEAT_F4HWN_FOXHUNT": false, + "ENABLE_FEAT_F4HWN_BEACON": false, "ENABLE_FEAT_F4HWN_ACTION_PICKER": false, "ENABLE_FEAT_F4HWN_RXTX_LOG": false, "ENABLE_FEAT_F4HWN_RXTX_LOG_WRAP": false, @@ -81,127 +90,40 @@ "ENABLE_FEAT_F4HWN_LOGO": false, "ENABLE_FEAT_F4HWN_LOGO_SAV": false, "ENABLE_FEAT_F4HWN_MENU_CAT": false, + "ENABLE_FEAT_F4HWN_MULTIBOOT": false, + "ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY": false, + "ENABLE_FEAT_F4HWN_OVERLAY_APPS": false, "ENABLE_AGC_SHOW_DATA": false, "ENABLE_UART_RW_BK_REGS": false, "ENABLE_SWD": false, "VERSION_STRING_1": "v0.22", - "VERSION_STRING_2": "v5.9.0" + "VERSION_STRING_2": "v6.0.0" } }, { "name": "Custom", "inherits": "default", "cacheVariables": { + "ENABLE_FEAT_F4HWN_QRCODE": true, + "ENABLE_FEAT_F4HWN_MEM": true, "EDITION_STRING": "Custom", "TARGET": "f4hwn.custom" } }, - { - "name": "Bandscope", - "inherits": "default", - "cacheVariables": { - "ENABLE_SPECTRUM": true, - "ENABLE_FMRADIO": false, - "ENABLE_VOX": false, - "ENABLE_AIRCOPY": true, - "ENABLE_FEAT_F4HWN_K5VIEWER": true, - "ENABLE_FEAT_F4HWN_GAME": false, - "ENABLE_FEAT_F4HWN_PMR": true, - "ENABLE_FEAT_F4HWN_GMRS_FRS_MURS": true, - "ENABLE_FEAT_F4HWN_RESCUE_OPS": false, - "EDITION_STRING": "Bandscope", - "TARGET": "f4hwn.bandscope" - } - }, - { - "name": "Broadcast", - "inherits": "default", - "cacheVariables": { - "ENABLE_SPECTRUM": false, - "ENABLE_FMRADIO": true, - "ENABLE_VOX": true, - "ENABLE_AIRCOPY": true, - "ENABLE_FEAT_F4HWN_K5VIEWER": true, - "ENABLE_FEAT_F4HWN_GAME": false, - "ENABLE_FEAT_F4HWN_PMR": true, - "ENABLE_FEAT_F4HWN_GMRS_FRS_MURS": true, - "ENABLE_FEAT_F4HWN_RESCUE_OPS": false, - "EDITION_STRING": "Broadcast", - "TARGET": "f4hwn.broadcast" - } - }, - { - "name": "Basic", - "inherits": "default", - "cacheVariables": { - "ENABLE_SPECTRUM": true, - "ENABLE_FMRADIO": true, - "ENABLE_VOX": false, - "ENABLE_AIRCOPY": false, - "ENABLE_FEAT_F4HWN_K5VIEWER": false, - "ENABLE_FEAT_F4HWN_GAME": false, - "ENABLE_FEAT_F4HWN_SPECTRUM": false, - "ENABLE_FEAT_F4HWN_PMR": true, - "ENABLE_FEAT_F4HWN_GMRS_FRS_MURS": true, - "ENABLE_AUDIO_BAR": false, - "ENABLE_FEAT_F4HWN_RESUME_STATE": false, - "ENABLE_FEAT_F4HWN_CHARGING_C": false, - "ENABLE_FEAT_F4HWN_INV": true, - "ENABLE_FEAT_F4HWN_CTR": false, - "ENABLE_FEAT_F4HWN_NARROWER": true, - "ENABLE_FEAT_F4HWN_RESCUE_OPS": false, - "EDITION_STRING": "Basic", - "TARGET": "f4hwn.basic" - } - }, - { - "name": "RescueOps", - "inherits": "default", - "cacheVariables": { - "ENABLE_SPECTRUM": false, - "ENABLE_FMRADIO": false, - "ENABLE_VOX": true, - "ENABLE_AIRCOPY": true, - "ENABLE_FEAT_F4HWN_K5VIEWER": true, - "ENABLE_FEAT_F4HWN_GAME": false, - "ENABLE_FEAT_F4HWN_PMR": true, - "ENABLE_FEAT_F4HWN_GMRS_FRS_MURS": true, - "ENABLE_NOAA": true, - "ENABLE_FEAT_F4HWN_RESCUE_OPS": true, - "EDITION_STRING": "RescueOps", - "TARGET": "f4hwn.rescueops" - } - }, - { - "name": "Game", - "inherits": "default", - "cacheVariables": { - "ENABLE_SPECTRUM": false, - "ENABLE_FMRADIO": true, - "ENABLE_VOX": false, - "ENABLE_AIRCOPY": true, - "ENABLE_FEAT_F4HWN_K5VIEWER": true, - "ENABLE_FEAT_F4HWN_GAME": true, - "ENABLE_FEAT_F4HWN_PMR": true, - "ENABLE_FEAT_F4HWN_GMRS_FRS_MURS": true, - "ENABLE_FEAT_F4HWN_RESCUE_OPS": false, - "EDITION_STRING": "Game", - "TARGET": "f4hwn.game" - } - }, { "name": "Fusion", "inherits": "default", "cacheVariables": { + "ENABLE_NO_HEAP": true, "ENABLE_SPECTRUM": true, "ENABLE_FMRADIO": true, "ENABLE_VOX": true, - "ENABLE_AIRCOPY": true, + "ENABLE_AIRCOPY": false, "ENABLE_FEAT_F4HWN_K5VIEWER": true, - "ENABLE_FEAT_F4HWN_GAME": true, + "ENABLE_FEAT_F4HWN_GAME": false, "ENABLE_FEAT_F4HWN_PMR": true, "ENABLE_FEAT_F4HWN_GMRS_FRS_MURS": true, - "ENABLE_FEAT_F4HWN_RESCUE_OPS": true, + "ENABLE_FEAT_F4HWN_RESCUE_OPS": false, "ENABLE_FEAT_F4HWN_VOL": true, "ENABLE_FEAT_F4HWN_AUDIO": true, "ENABLE_FEAT_F4HWN_AUDIO_SCOPE": true, @@ -209,9 +131,9 @@ "ENABLE_FEAT_F4HWN_SCAN_FASTER": true, "ENABLE_FEAT_F4HWN_SCAN_RSSI": true, "ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE": true, - "ENABLE_FEAT_F4HWN_BEAM": true, - "ENABLE_FEAT_F4HWN_FOXHUNT": true, - "ENABLE_FEAT_F4HWN_ACTION_PICKER": true, + "ENABLE_FEAT_F4HWN_BEAM": false, + "ENABLE_FEAT_F4HWN_FOXHUNT": false, + "ENABLE_FEAT_F4HWN_BEACON": false, "ENABLE_FEAT_F4HWN_RXTX_LOG": true, "ENABLE_FEAT_F4HWN_RXTX_LOG_WRAP": false, "ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER": true, @@ -220,10 +142,67 @@ "ENABLE_FEAT_F4HWN_LOGO": true, "ENABLE_FEAT_F4HWN_LOGO_SAV": true, "ENABLE_FEAT_F4HWN_MENU_CAT": true, + "ENABLE_FEAT_F4HWN_ACTION_PICKER": true, + "ENABLE_FEAT_F4HWN_MULTIBOOT": true, + "ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY": true, + "ENABLE_FEAT_F4HWN_OVERLAY_APPS": false, "ENABLE_SWD": true, "EDITION_STRING": "Fusion", "TARGET": "f4hwn.fusion" } + }, + { + "name": "FieldOps", + "inherits": "Fusion", + "cacheVariables": { + "ENABLE_FEAT_F4HWN_RESCUE_OPS": true, + "ENABLE_FEAT_F4HWN_FOXHUNT": true, + "ENABLE_FEAT_F4HWN_BEACON": true, + "EDITION_STRING": "FieldOps", + "TARGET": "f4hwn.fieldops" + } + }, + { + "name": "Transfer", + "inherits": "Fusion", + "cacheVariables": { + "ENABLE_AIRCOPY": true, + "ENABLE_FEAT_F4HWN_BEAM": true, + "EDITION_STRING": "Transfer", + "TARGET": "f4hwn.transfer" + } + }, + { + "name": "Labs", + "inherits": "Fusion", + "cacheVariables": { + "ENABLE_FEAT_F4HWN_RESCUE_OPS": true, + "ENABLE_FEAT_F4HWN_FOXHUNT": false, + "ENABLE_FEAT_F4HWN_BEACON": false, + "ENABLE_AIRCOPY": true, + "ENABLE_FEAT_F4HWN_BEAM": false, + "ENABLE_FEAT_F4HWN_GAME": false, + "ENABLE_FEAT_F4HWN_OVERLAY_APPS": true, + "ENABLE_FEAT_F4HWN_OVERLAY_TRIVFO": false, + "ENABLE_FEAT_F4HWN_OVERLAY_BEAM": true, + "EDITION_STRING": "Labs", + "TARGET": "f4hwn.labs" + } + }, + { + "name": "Max", + "inherits": "Fusion", + "cacheVariables": { + "ENABLE_FEAT_F4HWN_RESCUE_OPS": true, + "ENABLE_FEAT_F4HWN_FOXHUNT": true, + "ENABLE_FEAT_F4HWN_BEACON": true, + "ENABLE_AIRCOPY": true, + "ENABLE_FEAT_F4HWN_BEAM": true, + "ENABLE_FEAT_F4HWN_GAME": true, + "ENABLE_FEAT_F4HWN_OVERLAY_APPS": false, + "EDITION_STRING": "Max", + "TARGET": "f4hwn.max" + } } ], "buildPresets": [ @@ -231,29 +210,25 @@ "name": "Custom", "configurePreset": "Custom" }, - { - "name": "Bandscope", - "configurePreset": "Bandscope" - }, - { - "name": "Broadcast", - "configurePreset": "Broadcast" - }, - { - "name": "Basic", - "configurePreset": "Basic" - }, - { - "name": "RescueOps", - "configurePreset": "RescueOps" - }, - { - "name": "Game", - "configurePreset": "Game" - }, { "name": "Fusion", "configurePreset": "Fusion" + }, + { + "name": "Transfer", + "configurePreset": "Transfer" + }, + { + "name": "FieldOps", + "configurePreset": "FieldOps" + }, + { + "name": "Labs", + "configurePreset": "Labs" + }, + { + "name": "Max", + "configurePreset": "Max" } ] } diff --git a/Core/py32f071xb.ld b/Core/py32f071xb.ld index bedcc0af..8156a024 100644 --- a/Core/py32f071xb.ld +++ b/Core/py32f071xb.ld @@ -55,7 +55,9 @@ ENTRY(Reset_Handler) /* Highest address of the user mode stack */ _estack = ORIGIN(RAM) + LENGTH(RAM); /* end of RAM */ /* Generate a link error if heap and stack don't fit into RAM */ -_Min_Heap_Size = 0x200; /* required amount of heap */ +/* ENABLE_NO_HEAP passes --defsym=__min_heap_size=0 to drop the reserved heap + * (no malloc/sbrk is linked); default stays 0x200 otherwise. */ +_Min_Heap_Size = DEFINED(__min_heap_size) ? __min_heap_size : 0x200; /* required amount of heap */ _Min_Stack_Size = 0x400; /* required amount of stack */ /* Specify the memory areas */ @@ -134,10 +136,14 @@ SECTIONS _sidata = LOADADDR(.data); /* Initialized data sections goes into RAM, load LMA copy after code */ - .data : + .data : { . = ALIGN(4); _sdata = .; /* create a global symbol at data start */ + + *(.RamFunc) /* functions that must execute from RAM (e.g. internal */ + *(.RamFunc*) /* flash reprogramming); copied to RAM with .data */ + *(.data) /* .data sections */ *(.data*) /* .data* sections */ @@ -153,7 +159,63 @@ SECTIONS *(.noncacheable*) . = ALIGN(4); } >RAM AT> FLASH - _eflash_used = LOADADDR(.noncacheable) + SIZEOF(.noncacheable); + + /* Optional multiboot overlay. The restore stub is linked at the same RAM + address as the 4 KiB PY25Q16 sector cache, but its load image remains in + FLASH and is copied into the cache only immediately before reflashing. + + The VMA is PINNED to a fixed RAM address (ORIGIN + 0x280) so a single + overlay-app blob runs on EVERY preset: without pinning, the sector-cache + VMA shifts with the per-preset .data size (Fusion 0x1dc, Transfer/Max 0x1e0, + ...), and a blob linked for one preset is refused by the loader's VMA guard + on another. 0x280 clears the ~0x1ec of RAM used before it; the assert fails + loudly (raise the anchor) if a preset ever needs more. */ + ASSERT((ADDR(.noncacheable) + SIZEOF(.noncacheable)) <= (ORIGIN(RAM) + 0x280), + "RAM before the overlay exceeds the fixed 0x280 anchor - raise MB_OVERLAY_VMA") + OVERLAY (ORIGIN(RAM) + 0x280) : NOCROSSREFS AT (LOADADDR(.noncacheable) + SIZEOF(.noncacheable)) + { + .mb_ramfunc + { + . = ALIGN(4); + __mb_ramfunc_start = .; + KEEP(*(.MBRamFunc)) + KEEP(*(.MBRamFunc*)) + . = ALIGN(4); + __mb_ramfunc_end = .; + } + + .mb_workspace + { + . = ALIGN(4); + __mb_workspace_start = .; + KEEP(*(.bss.mb_workspace)) + KEEP(*(.bss.mb_workspace*)) + . = ALIGN(4); + __mb_workspace_end = .; + } + } >RAM + + __mb_ramfunc_load_start = LOADADDR(.mb_ramfunc); + __mb_ramfunc_size = SIZEOF(.mb_ramfunc); + __mb_workspace_size = SIZEOF(.mb_workspace); + + ASSERT(__mb_ramfunc_size <= __mb_workspace_size, + "Multiboot RAM stub does not fit in PY25Q16 sector-cache overlay") + ASSERT((__mb_ramfunc_size == 0) || (__mb_workspace_size == 0x1000), + "Multiboot overlay workspace is not the 4 KiB sector cache") + ASSERT((__mb_ramfunc_size == 0) || + (ADDR(.mb_ramfunc) == ADDR(.mb_workspace)), + "Multiboot RAM stub and workspace do not share the same VMA") + ASSERT((__mb_ramfunc_size == 0) || + (LOADADDR(.mb_ramfunc) != ADDR(.mb_ramfunc)), + "Multiboot RAM stub load and execution addresses must differ") + ASSERT((LOADADDR(.mb_ramfunc) + SIZEOF(.mb_ramfunc)) <= + (ORIGIN(FLASH) + LENGTH(FLASH)), + "FLASH overflowed by multiboot RAM-stub load image") + + /* .mb_ramfunc is the final section with a FLASH load image. The workspace + member is NOLOAD/BSS storage and therefore contributes RAM only. */ + _eflash_used = LOADADDR(.mb_ramfunc) + SIZEOF(.mb_ramfunc); /* Uninitialized data section */ . = ALIGN(4); @@ -194,5 +256,3 @@ SECTIONS .ARM.attributes 0 : { *(.ARM.attributes) } } - - diff --git a/README.md b/README.md index c7b353b1..78ebce31 100644 --- a/README.md +++ b/README.md @@ -59,18 +59,26 @@ Special thanks to Jean-Cyrille F6IWW (3 times), Fabrice 14RC123, David F4BPP, Ol ### Fusion edition -Fusion is the reference edition of the project. It provides an all-in-one firmware for the UV-K1 and UV-K5 V3, including: +Fusion is the generic reference edition for the UV-K1 and UV-K5 V3. It is intended +for everyday use and is the base inherited by the specialized editions. It includes: - Fagci's spectrum analyzer, -- broadcast FM radio, -- VOX and AirCopy, -- BEAM wireless channel transfer, +- broadcast FM radio and VOX, - [UV Studio](https://armel.github.io/uvstudio/) with integrated K5Viewer screen mirroring, screenshots and remote keyboard control, - advanced RX audio profiles and Audio Scope, -- first-responder-oriented controls, -- the Breakout game, - automatic RX/TX activity logging with RF Log, -- full Fox Hunt and Morse Beacon support. +- multiboot support. + +Specialized presets extend Fusion for specific uses: + +- **Transfer** adds AirCopy and BEAM wireless channel transfer. +- **FieldOps** adds first-responder controls, Fox Hunt and Morse Beacon support. +- **Labs** is the experimental edition. It carries the broad feature selection of the + other releases and adds the overlay-apps platform (apps loaded from external Flash and + run in a 4 KiB RAM overlay) — the newest, least-settled work. Expect rough edges. It is + not a strict superset of every other edition: features may be exchanged between releases + to preserve stability and memory headroom. +- **Custom** remains a manually configured build based directly on the hidden technical default. ### Radio and signal handling @@ -179,7 +187,7 @@ Fusion is the reference edition of the project. It provides an all-in-one firmwa - selectable RF attenuation, - silent, Geiger-style and received-audio modes, - long-press `F` keypad lock (attenuation stays adjustable with the arrow keys). -- Integrated Morse Beacon transmitter with: +- Independent Morse Beacon transmitter with: - `MOE`, `MOI`, `MOS`, `MOH`, `MO5` and `MO` identifiers, - optional callsign identification, - configurable TX and idle periods, @@ -256,7 +264,8 @@ Fusion is the reference edition of the project. It provides an all-in-one firmwa - offset removal, - BEAM, - RF Log, - - Fox Hunt / Beacon. + - Fox Hunt, + - Beacon. ### Reliability and optimization @@ -269,11 +278,10 @@ Fusion is the reference edition of the project. It provides an all-in-one firmwa - Improved VFO persistence and restoration. - Extensive code refactoring and memory optimization. - DTMF calling and the scrambler remain disabled in Fusion. -- Legacy AM Fix code is not used by this firmware. +- Legacy AM Fix support has been removed. ## Main features from Egzumer: * many of OneOfEleven mods: - * AM fix, huge improvement in reception quality * long press buttons functions replicating F+ action * fast scanning * channel name editing in the menu @@ -324,9 +332,10 @@ But, they are nice toys for the price, fun to play with. ## Compiling and Building from Docker -This project provides a Docker-based build system to compile the Fusion firmware for the UV-K1 and UV-K5 V3. Everything is handled through the `compile-with-docker.sh` helper script. - -The documented build output is generated inside `build/Fusion`, using the CMake presets defined in `CMakePresets.json`. +This project provides a Docker-based build system for the UV-K1 and UV-K5 V3. +Everything is handled through the `compile-firmware.sh` helper script. Fusion is +the default generic preset, while specialized builds are generated in their own +`build/` directories. ### Prerequisites @@ -335,29 +344,37 @@ The documented build output is generated inside `build/Fusion`, using the CMake ### Build Script Overview -The script `compile-with-docker.sh` performs the following actions: +The script `compile-firmware.sh`: 1. Builds the Docker image (`uvk1-uvk5v3`) if it does not already exist. -2. Removes any previous `build` directory to ensure a clean configuration. -3. Runs CMake using the `Fusion` preset inside the Docker container. -4. Builds the firmware and outputs `.elf`, `.bin` and `.hex` files. +2. Configures the selected preset with `cmake --fresh`. +3. Builds the firmware and outputs matching `.elf`, `.bin` and `.hex` files. +4. Displays Flash and RAM usage; `All` keeps the individual build logs quiet. ### Usage ```bash -./compile-with-docker.sh Fusion [extra CMake options] +./compile-firmware.sh [Preset] [extra CMake options] ``` -### Documented Preset +The default preset is **Fusion**. Available presets are: +- **Custom** - **Fusion** +- **Transfer** +- **FieldOps** +- **Labs** +- **All** (Fusion, Transfer, FieldOps and Labs) -### Examples - -Build Fusion: +Examples: ```bash -./compile-with-docker.sh Fusion +./compile-firmware.sh +./compile-firmware.sh Fusion +./compile-firmware.sh Transfer +./compile-firmware.sh FieldOps +./compile-firmware.sh Labs +./compile-firmware.sh All ``` ### Passing Additional CMake Options @@ -368,15 +385,15 @@ These are forwarded directly to `cmake --preset` inside the container. Examples: ```bash -./compile-with-docker.sh Fusion -DENABLE_SPECTRUM=ON -./compile-with-docker.sh Fusion -DENABLE_FEAT_F4HWN_GAME=ON -DENABLE_NOAA=ON -./compile-with-docker.sh Fusion -DSQL_TONE=600 +./compile-firmware.sh FieldOps -DENABLE_VOX=OFF +./compile-firmware.sh Fusion -DENABLE_FEAT_F4HWN_GAME=ON +./compile-firmware.sh Fusion -DSQL_TONE=600 ``` To prepare the rolling development firmware: ```bash -./compile-with-docker.sh Fusion -DDEV=ON +./compile-firmware.sh Fusion -DDEV=ON ``` This keeps the regular build output in `build/Fusion` and also updates diff --git a/cmake/check_mb_ramfunc.cmake b/cmake/check_mb_ramfunc.cmake new file mode 100644 index 00000000..50757a6c --- /dev/null +++ b/cmake/check_mb_ramfunc.cmake @@ -0,0 +1,138 @@ +# Validate the multiboot restore stub after linking. The application flash is +# unavailable while this code runs, so every direct control-flow target and +# every symbolic code/data reference must remain inside .mb_ramfunc. + +foreach(required OBJDUMP ELF MAP BINARY_DIR) + if(NOT DEFINED ${required} OR "${${required}}" STREQUAL "") + message(FATAL_ERROR "check_mb_ramfunc: missing -D${required}=...") + endif() +endforeach() + +if(NOT EXISTS "${ELF}") + message(FATAL_ERROR "check_mb_ramfunc: ELF not found: ${ELF}") +endif() +if(NOT EXISTS "${MAP}") + message(FATAL_ERROR "check_mb_ramfunc: map file not found: ${MAP}") +endif() + +execute_process( + COMMAND "${OBJDUMP}" -h "${ELF}" + RESULT_VARIABLE headers_result + OUTPUT_VARIABLE headers + ERROR_VARIABLE headers_error +) +if(NOT headers_result EQUAL 0) + message(FATAL_ERROR + "check_mb_ramfunc: objdump section scan failed (${headers_result}):\n${headers_error}") +endif() + +string(REGEX MATCH + "[ \t]\\.mb_ramfunc[ \t]+([0-9A-Fa-f]+)[ \t]+([0-9A-Fa-f]+)" + section_header "${headers}") +if(section_header STREQUAL "") + message(FATAL_ERROR "check_mb_ramfunc: .mb_ramfunc not found in ${ELF}") +endif() +set(section_size_hex "${CMAKE_MATCH_1}") +set(section_start_hex "${CMAKE_MATCH_2}") +math(EXPR section_size "0x${section_size_hex}") +math(EXPR section_start "0x${section_start_hex}") +math(EXPR section_end "${section_start} + ${section_size}") +math(EXPR section_end_hex "${section_end}" OUTPUT_FORMAT HEXADECIMAL) +if(section_size EQUAL 0) + message(FATAL_ERROR "check_mb_ramfunc: .mb_ramfunc is empty in an overlay build") +endif() + +execute_process( + COMMAND "${OBJDUMP}" --no-show-raw-insn -d -j .mb_ramfunc "${ELF}" + RESULT_VARIABLE disassembly_result + OUTPUT_VARIABLE disassembly + ERROR_VARIABLE disassembly_error +) +if(NOT disassembly_result EQUAL 0) + message(FATAL_ERROR + "check_mb_ramfunc: disassembly failed (${disassembly_result}):\n${disassembly_error}") +endif() + +# Check all Thumb branch forms. An indirect BLX/BX cannot be proven safe; only +# BX LR (a normal function return) is accepted. Direct branches must stay in the +# linked RAM section, including compiler-generated tail calls and veneers. +string(REPLACE "\n" ";" disassembly_lines "${disassembly}") +set(branch_count 0) +foreach(line IN LISTS disassembly_lines) + if(line MATCHES + "^[ \t]*[0-9A-Fa-f]+:[ \t]+([A-Za-z0-9.]+)([ \t]+(.*))?") + set(mnemonic "${CMAKE_MATCH_1}") + set(operands "${CMAKE_MATCH_3}") + + if(mnemonic MATCHES + "^(b|bl|blx|bx|beq|bne|bcs|bcc|bhs|blo|bmi|bpl|bvs|bvc|bhi|bls|bge|blt|bgt|ble)(\\.[nw])?$" + OR mnemonic MATCHES "^(cbz|cbnz)$") + math(EXPR branch_count "${branch_count} + 1") + + if(mnemonic MATCHES "^bx(\\.[nw])?$") + string(STRIP "${operands}" register_name) + if(NOT register_name STREQUAL "lr" AND NOT register_name STREQUAL "r14") + message(FATAL_ERROR + "check_mb_ramfunc: unsafe indirect branch in RAM stub:\n${line}") + endif() + continue() + endif() + + # BL/B operands contain the address directly; CBZ/CBNZ put it after + # the register and comma. In both forms it is the final numeric + # operand before objdump's optional annotation. + if(NOT operands MATCHES + "(^|[, \t])((0[xX])?[0-9A-Fa-f]+)([ \t]+<[^>]+>)?[ \t]*$") + message(FATAL_ERROR + "check_mb_ramfunc: unresolved/indirect branch in RAM stub:\n${line}") + endif() + set(target_hex "${CMAKE_MATCH_2}") + string(REGEX REPLACE "^0[xX]" "" target_hex "${target_hex}") + math(EXPR target "0x${target_hex}") + if(target LESS section_start OR NOT target LESS section_end) + message(FATAL_ERROR + "check_mb_ramfunc: branch leaves .mb_ramfunc:\n${line}\n" + "Allowed range: 0x${section_start_hex}..${section_end_hex}") + endif() + endif() + endif() +endforeach() + +# Locate the exact input object from the linker map rather than assuming a +# generator-specific CMakeFiles path. Any relocation in .MBRamFunc would be an +# external symbolic reference (code or data), because all approved helpers share +# this same input section and their local branches are assembler-resolved. +file(READ "${MAP}" map_contents) +string(REGEX MATCH + "[^\r\n]*\\.MBRamFunc[^\r\n]*mb_flash\\.c\\.(obj|o)" + object_line "${map_contents}") +if(object_line STREQUAL "") + message(FATAL_ERROR + "check_mb_ramfunc: mb_flash object not found in ${MAP} " + "(the overlay safety gate requires a non-LTO input object)") +endif() +string(REGEX MATCH "[^ \t]+mb_flash\\.c\\.(obj|o)" object_path "${object_line}") +if(NOT IS_ABSOLUTE "${object_path}") + set(object_path "${BINARY_DIR}/${object_path}") +endif() +if(NOT EXISTS "${object_path}") + message(FATAL_ERROR "check_mb_ramfunc: input object not found: ${object_path}") +endif() + +execute_process( + COMMAND "${OBJDUMP}" -r -j .MBRamFunc "${object_path}" + RESULT_VARIABLE relocations_result + OUTPUT_VARIABLE relocations + ERROR_VARIABLE relocations_error +) +if(NOT relocations_result EQUAL 0) + message(FATAL_ERROR + "check_mb_ramfunc: relocation scan failed (${relocations_result}):\n${relocations_error}") +endif() +if(relocations MATCHES "R_ARM_") + message(FATAL_ERROR + "check_mb_ramfunc: .MBRamFunc has an external code/data reference:\n${relocations}") +endif() + +message(STATUS + "Multiboot RAM stub isolation OK: ${section_size} bytes, ${branch_count} checked branches") diff --git a/cmake/gcc-arm-none-eabi.cmake b/cmake/gcc-arm-none-eabi.cmake index 10971efb..962505fe 100644 --- a/cmake/gcc-arm-none-eabi.cmake +++ b/cmake/gcc-arm-none-eabi.cmake @@ -13,6 +13,7 @@ set(CMAKE_ASM_COMPILER ${CMAKE_C_COMPILER}) set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}g++) set(CMAKE_LINKER ${TOOLCHAIN_PREFIX}g++) set(CMAKE_OBJCOPY ${TOOLCHAIN_PREFIX}objcopy) +set(CMAKE_OBJDUMP ${TOOLCHAIN_PREFIX}objdump) set(CMAKE_SIZE ${TOOLCHAIN_PREFIX}size) set(CMAKE_EXECUTABLE_SUFFIX_ASM ".elf") diff --git a/compile-app.sh b/compile-app.sh new file mode 100755 index 00000000..c89bcfef --- /dev/null +++ b/compile-app.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +# --------------------------------------------------------------------------- +# Build overlay-app blobs (.app), the counterpart of compile-firmware.sh. +# +# Usage: +# ./compile-app.sh # build every app +# ./compile-app.sh All # build every app +# ./compile-app.sh breakout # build one app +# ./compile-app.sh fm foxhunt # build several apps +# +# Each app is compiled in the uvk1-uvk5v3 Docker image, linked at the pinned +# overlay VMA, packed into build/Apps/.app (named after the app's human +# label, spaces stripped), and reported against the 4 KiB overlay budget. Apps +# are auto-discovered from App/apps//build.sh. +# --------------------------------------------------------------------------- +set -euo pipefail + +IMAGE=uvk1-uvk5v3 +APPS_DIR=App/apps +OUT_DIR=build/Apps +OVERLAY_MAX=4096 +APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld) + +cd "$(dirname "$0")" + +# --- discover apps (any App/apps//build.sh) --- +ALL_APPS=() +for d in "$APPS_DIR"/*/; do + [ -f "${d}build.sh" ] && ALL_APPS+=("$(basename "$d")") +done +if [ "${#ALL_APPS[@]}" -eq 0 ]; then + echo "❌ No apps found under $APPS_DIR/*/build.sh"; exit 1 +fi + +# --- select targets --- +if [ "$#" -eq 0 ] || [[ "${1:-}" == [Aa][Ll][Ll] ]]; then + TARGETS=("${ALL_APPS[@]}") +else + TARGETS=() + for want in "$@"; do + ok=0 + for a in "${ALL_APPS[@]}"; do [ "$a" = "$want" ] && ok=1; done + if [ "$ok" -eq 0 ]; then + echo "❌ Unknown app '$want'. Available: ${ALL_APPS[*]} (or All)"; exit 1 + fi + TARGETS+=("$want") + done +fi + +command -v docker >/dev/null 2>&1 || { echo "❌ docker not found"; exit 1; } +if [[ "$(docker images -q "$IMAGE")" == "" ]]; then + echo "🐳 Building Docker image..."; docker build -q -t "$IMAGE" . >/dev/null +fi +mkdir -p "$OUT_DIR" + +# Give the container a pseudo-TTY when we are interactive, so build.sh's +# single-line [k/3] progress refreshes in place (same trick as the firmware +# build gives Ninja). Batch/redirected runs stay plain and line-oriented. +TTY_ARG=""; [[ -t 1 ]] && TTY_ARG="-t" + +echo +echo "🚀 Building overlay apps" +printf ' VMA %s · budget %d B / %d.00 KiB · out %s/\n\n' \ + "$APP_VMA" "$OVERLAY_MAX" "$((OVERLAY_MAX / 1024))" "$OUT_DIR" + +files=(); sizes=(); vmas=(); states=() +fail=0 + +read_u32le() { od -An -tx1 -j"$2" -N4 "$1" | awk '{printf "0x%s%s%s%s",$4,$3,$2,$1}'; } + +for app in "${TARGETS[@]}"; do + if docker run --rm ${TTY_ARG:+"$TTY_ARG"} -u "$(id -u):$(id -g)" \ + -v "$PWD":/work -w "/work/$APPS_DIR/$app" \ + -e PATH="/opt/toolchain/bin:/usr/bin:/bin" -e APP_VMA="$APP_VMA" \ + "$IMAGE" bash ./build.sh; then + appfile=$(ls -1 "$APPS_DIR/$app"/*.app 2>/dev/null | head -1) + if [ -n "$appfile" ] && [ -f "$appfile" ]; then + cp -f "$appfile" "$OUT_DIR/" + base=$(basename "$appfile") + code=$(( $(wc -c < "$appfile") - 64 )) + vma=$(read_u32le "$appfile" 52) + state="✅ OK"; [ "$code" -gt "$OVERLAY_MAX" ] && { state="🚨 OVERFLOW"; fail=1; } + else + base="$app.app"; code=-1; vma="--"; state="❌ NO BLOB"; fail=1 + fi + else + base="$app.app"; code=-1; vma="--"; state="❌ BUILD FAIL"; fail=1 + fi + files+=("$base"); sizes+=("$code"); vmas+=("$vma"); states+=("$state") +done + +# --- memory report (styled like the firmware Flash/RAM tables) --- +echo +printf '📦 Overlay apps (budget: %d B / %d.00 KiB)\n' "$OVERLAY_MAX" "$((OVERLAY_MAX / 1024))" +printf '%-16s | %9s | %9s | %9s | %9s | %7s | %s\n' \ + "File" "Code (B)" "Code(KiB)" "Free (B)" "Free(KiB)" "Usage" "Status" +printf '%-16s-+-%9s-+-%9s-+-%9s-+-%9s-+-%7s-+-%s\n' \ + "----------------" "---------" "---------" "---------" "---------" "-------" "----------" +for i in "${!files[@]}"; do + c=${sizes[$i]} + if [ "$c" -ge 0 ]; then + free=$(( OVERLAY_MAX - c )) + bp=$(( (c * 10000 + OVERLAY_MAX / 2) / OVERLAY_MAX )) + ck100=$(( (c * 100 + 512) / 1024 )); fk100=$(( (free * 100 + 512) / 1024 )) + printf -v pct '%d.%02d%%' "$((bp/100))" "$((bp%100))" + printf -v ckib '%d.%02d' "$((ck100/100))" "$((ck100%100))" + printf -v fkib '%d.%02d' "$((fk100/100))" "$((fk100%100))" + printf '%-16s | %9d | %9s | %9d | %9s | %7s | %s\n' \ + "${files[$i]}" "$c" "$ckib" "$free" "$fkib" "$pct" "${states[$i]}" + else + printf '%-16s | %9s | %9s | %9s | %9s | %7s | %s\n' \ + "${files[$i]}" "-" "-" "-" "-" "-" "${states[$i]}" + fi +done + +echo +if [ "$fail" -eq 0 ]; then + if [ "${#files[@]}" -eq 1 ]; then + echo "🎉 App ${files[0]} built successfully -> $OUT_DIR/" + else + echo "🎉 All ${#files[@]} apps built successfully -> $OUT_DIR/" + fi +else + echo "⚠️ Some apps failed or overflowed — see the table above." +fi +exit $fail diff --git a/compile-firmware.sh b/compile-firmware.sh new file mode 100755 index 00000000..6e997594 --- /dev/null +++ b/compile-firmware.sh @@ -0,0 +1,255 @@ +#!/usr/bin/env bash +set -euo pipefail + +# --------------------------------------------- +# Usage: +# ./compile-firmware.sh [Preset] [CMake options...] +# Examples: +# ./compile-firmware.sh Fusion +# ./compile-firmware.sh Transfer +# ./compile-firmware.sh FieldOps +# ./compile-firmware.sh Labs +# ./compile-firmware.sh Fusion -DDEV=ON +# ./compile-firmware.sh FieldOps -DENABLE_VOX=OFF +# ./compile-firmware.sh All +# Default preset: "Fusion" +# --------------------------------------------- + +IMAGE=uvk1-uvk5v3 +RELEASE_PRESETS=(Fusion Transfer FieldOps Labs Max) +FLASH_LIMIT=$((118 * 1024)) +RAM_LIMIT=$((16 * 1024)) + +PRESET=${1:-Fusion} +shift || true # remove preset from arguments if present + +# Any remaining args will be treated as CMake cache variables +EXTRA_ARGS=("$@") + +# --------------------------------------------- +# Validate preset name +# --------------------------------------------- +if [[ ! "$PRESET" =~ ^(Custom|Fusion|Transfer|FieldOps|Labs|Max|All)$ ]]; then + echo "❌ Unknown preset: '$PRESET'" + echo "Valid presets are: Custom, Fusion, Transfer, FieldOps, Labs, Max, All" + exit 1 +fi + +QUIET=0 +if [[ "$PRESET" == "All" ]]; then + QUIET=1 +fi + +# Remember whether the script itself was started from an interactive terminal. +# The build output is piped through tee below, so checking stdout later from +# run_preset_build() would always report a non-terminal. +INTERACTIVE=0 +if [[ -t 1 ]]; then + INTERACTIVE=1 +fi + +# --------------------------------------------- +# Build the Docker image (only needed once) +# --------------------------------------------- +if [[ "$(docker images -q "$IMAGE")" == "" ]]; then + echo "Building Docker image..." + docker build -q -t "$IMAGE" . >/dev/null +fi +export MSYS_NO_PATHCONV=1 + +RESULT_PRESETS=() +RESULT_FLASH_SIZES=() +RESULT_RAM_SIZES=() + +run_preset_build() { + local preset="$1" + local docker_tty_arg="" + + # Give Ninja a pseudo-terminal for an interactive single-preset build. This + # lets it refresh its [current/total] progress on one line. Batch/redirected + # builds keep plain line-oriented output suitable for logs and CI. + if (( INTERACTIVE && ! QUIET )); then + docker_tty_arg="-t" + fi + + # The ${var:+...} and ${array[@]+...} forms avoid expanding an empty array, + # which Bash 3.2 treats as an unbound variable when nounset is enabled. + docker run --rm ${docker_tty_arg:+"$docker_tty_arg"} \ + -u "$(id -u):$(id -g)" \ + -v "$PWD":/src -w /src "$IMAGE" \ + bash -c 'which arm-none-eabi-gcc && arm-none-eabi-gcc --version && + cmake --fresh --preset "$1" "${@:2}" && + cmake --build --preset "$1" -j' \ + bash "$preset" ${EXTRA_ARGS[@]+"${EXTRA_ARGS[@]}"} +} + +build_preset() { + local preset="$1" + local preset_slug log_file bin_file flash_size ram_size status + + # macOS ships Bash 3.2, which does not support Bash 4's ${var,,} syntax. + preset_slug="$(printf '%s' "$preset" | tr '[:upper:]' '[:lower:]')" + log_file="$(mktemp)" + bin_file="" + + find "build/${preset}" -maxdepth 1 -type f -name 'f4hwn.*' -delete 2>/dev/null || true + + if (( QUIET )); then + printf "Building %-10s ... " "$preset" + if run_preset_build "$preset" >"$log_file" 2>&1; then + status=0 + else + status=$? + fi + else + echo "" + echo "=== 🚀 Building preset: ${preset} ===" + echo "---------------------------------------------" + if (( INTERACTIVE )); then + # Do not put a line-oriented filter after tee here: it would buffer + # Ninja's carriage-return progress updates and defeat the TTY display. + if run_preset_build "$preset" 2>&1 | tee "$log_file"; then + status=0 + else + status=$? + fi + else + if run_preset_build "$preset" 2>&1 | tee "$log_file" | awk ' + /^Memory region[[:space:]]+Used Size[[:space:]]+Region Size/ { next } + /^[[:space:]]+RAM:[[:space:]]/ { next } + /^[[:space:]]+FLASH:[[:space:]]/ { next } + { print; fflush() } + '; then + status=0 + else + status=$? + fi + fi + fi + + if (( status != 0 )); then + if (( QUIET )); then + printf "FAILED\n\n" + cat "$log_file" + else + echo "Failed: ${preset}" + fi + rm -f -- "$log_file" + return "$status" + fi + + bin_file="build/${preset}/f4hwn.${preset_slug}.bin" + if [[ -z "$bin_file" || ! -f "$bin_file" ]]; then + if (( QUIET )); then + printf "FAILED\n" + fi + echo "Expected binary not found: $bin_file" + if (( QUIET )); then + cat "$log_file" + fi + rm -f -- "$log_file" + return 1 + fi + + flash_size="$(wc -c < "$bin_file")" + ram_size="$(awk '$1 == "RAM:" { + value = $2 + if ($3 == "KB") value *= 1024 + else if ($3 == "MB") value *= 1024 * 1024 + printf "%.0f\n", value + }' "$log_file" | tail -n 1)" + + if [[ ! "$ram_size" =~ ^[0-9]+$ ]]; then + if (( QUIET )); then + printf "FAILED\n" + fi + echo "Could not read RAM usage from linker output" + if (( QUIET )); then + cat "$log_file" + fi + rm -f -- "$log_file" + return 1 + fi + + RESULT_PRESETS+=("$preset") + RESULT_FLASH_SIZES+=("$flash_size") + RESULT_RAM_SIZES+=("$ram_size") + + if (( QUIET )); then + printf "OK\n" + else + echo "✅ Done: ${preset}" + fi + rm -f -- "$log_file" +} + +print_summary() { + local i + local flash_used flash_free flash_bp flash_pct + local flash_used_kib100 flash_free_kib100 flash_used_kib flash_free_kib + local ram_used ram_free ram_bp ram_pct + local ram_used_kib100 ram_free_kib100 ram_used_kib ram_free_kib + + echo "" + printf "Flash (limit: %d B / %d.00 KiB)\n" "$FLASH_LIMIT" "$((FLASH_LIMIT / 1024))" + printf "%-10s | %10s | %10s | %10s | %10s | %7s\n" \ + "Preset" "Used (B)" "Used (KiB)" "Free (B)" "Free (KiB)" "Usage" + printf "%-10s-+-%10s-+-%10s-+-%10s-+-%10s-+-%7s\n" \ + "----------" "----------" "----------" "----------" "----------" "-------" + + for i in "${!RESULT_PRESETS[@]}"; do + flash_used="${RESULT_FLASH_SIZES[$i]}" + flash_free=$((FLASH_LIMIT - flash_used)) + flash_bp=$(((flash_used * 10000 + FLASH_LIMIT / 2) / FLASH_LIMIT)) + flash_used_kib100=$(((flash_used * 100 + 512) / 1024)) + flash_free_kib100=$(((flash_free * 100 + 512) / 1024)) + printf -v flash_pct "%d.%02d%%" "$((flash_bp / 100))" "$((flash_bp % 100))" + printf -v flash_used_kib "%d.%02d" "$((flash_used_kib100 / 100))" "$((flash_used_kib100 % 100))" + printf -v flash_free_kib "%d.%02d" "$((flash_free_kib100 / 100))" "$((flash_free_kib100 % 100))" + + printf "%-10s | %10d | %10s | %10d | %10s | %7s\n" \ + "${RESULT_PRESETS[$i]}" "$flash_used" "$flash_used_kib" \ + "$flash_free" "$flash_free_kib" "$flash_pct" + done + + echo "" + printf "RAM (limit: %d B / %d.00 KiB)\n" "$RAM_LIMIT" "$((RAM_LIMIT / 1024))" + printf "%-10s | %10s | %10s | %10s | %10s | %7s\n" \ + "Preset" "Used (B)" "Used (KiB)" "Free (B)" "Free (KiB)" "Usage" + printf "%-10s-+-%10s-+-%10s-+-%10s-+-%10s-+-%7s\n" \ + "----------" "----------" "----------" "----------" "----------" "-------" + + for i in "${!RESULT_PRESETS[@]}"; do + ram_used="${RESULT_RAM_SIZES[$i]}" + ram_free=$((RAM_LIMIT - ram_used)) + ram_bp=$(((ram_used * 10000 + RAM_LIMIT / 2) / RAM_LIMIT)) + ram_used_kib100=$(((ram_used * 100 + 512) / 1024)) + ram_free_kib100=$(((ram_free * 100 + 512) / 1024)) + printf -v ram_pct "%d.%02d%%" "$((ram_bp / 100))" "$((ram_bp % 100))" + printf -v ram_used_kib "%d.%02d" "$((ram_used_kib100 / 100))" "$((ram_used_kib100 % 100))" + printf -v ram_free_kib "%d.%02d" "$((ram_free_kib100 / 100))" "$((ram_free_kib100 % 100))" + + printf "%-10s | %10d | %10s | %10d | %10s | %7s\n" \ + "${RESULT_PRESETS[$i]}" "$ram_used" "$ram_used_kib" \ + "$ram_free" "$ram_free_kib" "$ram_pct" + done + + echo "" + if (( ${#RESULT_PRESETS[@]} == 1 )); then + echo "🎉 Preset ${RESULT_PRESETS[0]} built successfully!" + else + echo "🎉 All presets built successfully!" + fi +} + +# --------------------------------------------- +# Handle 'All' preset +# --------------------------------------------- +if [[ "$PRESET" == "All" ]]; then + for p in "${RELEASE_PRESETS[@]}"; do + build_preset "$p" + done +else + build_preset "$PRESET" +fi +print_summary diff --git a/compile-with-docker.sh b/compile-with-docker.sh index 369acd1a..4bae1bf5 100755 --- a/compile-with-docker.sh +++ b/compile-with-docker.sh @@ -1,75 +1,8 @@ #!/usr/bin/env bash -set -euo pipefail - -# --------------------------------------------- -# Usage: -# ./compile-with-docker.sh [Preset] [CMake options...] -# Examples: -# ./compile-with-docker.sh Custom -# ./compile-with-docker.sh Bandscope -DENABLE_SPECTRUM=ON -# ./compile-with-docker.sh Broadcast -DENABLE_FEAT_F4HWN_GAME=ON -DENABLE_NOAA=ON -# ./compile-with-docker.sh Fusion -DDEV=ON -# ./compile-with-docker.sh All -# Default preset: "Custom" -# --------------------------------------------- - -IMAGE=uvk1-uvk5v3 -PRESET=${1:-Custom} -shift || true # remove preset from arguments if present - -# Any remaining args will be treated as CMake cache variables -EXTRA_ARGS=("$@") - -# --------------------------------------------- -# Validate preset name -# --------------------------------------------- -if [[ ! "$PRESET" =~ ^(Custom|Bandscope|Broadcast|Basic|RescueOps|Game|Fusion|All)$ ]]; then - echo "❌ Unknown preset: '$PRESET'" - echo "Valid presets are: Custom, Bandscope, Broadcast, Basic, RescueOps, Game, Fusion, All" - exit 1 -fi - -# --------------------------------------------- -# Build the Docker image (only needed once) -# --------------------------------------------- -if [[ "$(docker images -q $IMAGE)" == "" ]]; then - echo "Building Docker image..." - docker build -t "$IMAGE" . -fi - -# --------------------------------------------- -# Clean existing CMake cache to ensure toolchain reload -# --------------------------------------------- -rm -rf build -export MSYS_NO_PATHCONV=1 -# --------------------------------------------- -# Function to build one preset -# --------------------------------------------- -build_preset() { - local preset="$1" - echo "" - echo "=== 🚀 Building preset: ${preset} ===" - echo "---------------------------------------------" - docker run --rm \ - -u $(id -u):$(id -g) \ - -it -v "$PWD":/src -w /src "$IMAGE" \ - bash -c 'which arm-none-eabi-gcc && arm-none-eabi-gcc --version && - cmake --preset "$1" "${@:2}" && - cmake --build --preset "$1" -j' \ - bash "${preset}" ${EXTRA_ARGS[@]+"${EXTRA_ARGS[@]}"} - echo "✅ Done: ${preset}" -} - -# --------------------------------------------- -# Handle 'All' preset -# --------------------------------------------- -if [[ "$PRESET" == "All" ]]; then - PRESETS=(Bandscope Broadcast Basic RescueOps Game Fusion) - for p in "${PRESETS[@]}"; do - build_preset "$p" - done - echo "" - echo "🎉 All presets built successfully!" -else - build_preset "$PRESET" -fi +# --------------------------------------------------------------------------- +# DEPRECATED: this script was renamed to compile-firmware.sh (paired with +# compile-app.sh). This thin wrapper forwards to it for backward compatibility. +# Please update your scripts, docs and habits to call compile-firmware.sh. +# --------------------------------------------------------------------------- +echo "note: 'compile-with-docker.sh' was renamed to 'compile-firmware.sh' - forwarding." >&2 +exec "$(dirname "$0")/compile-firmware.sh" "$@" diff --git a/tools/mb_test.html b/tools/mb_test.html new file mode 100644 index 00000000..f5d32790 --- /dev/null +++ b/tools/mb_test.html @@ -0,0 +1,333 @@ + + + + + +Multiboot M1 - bench test + + + +
+

Multiboot M1 — bench test

+
UV-K1 / UV-K5 V3 · F4HWN · commandes cachées 0x0710 / 0x0712 via Web Serial
+ +
+ Radio sacrifiable uniquement. Nécessite un firmware compilé avec + -DENABLE_FEAT_F4HWN_MULTIBOOT=ON. Le bootloader d'usine n'est jamais touché : + en cas d'échec du restore, re-flasher via USB/DFU dans UV Studio. + Ferme UV Studio / K5Viewer avant de te connecter (le port série ne peut être ouvert qu'une fois). +
+ +
+ Web Serial indisponible. Utilise Chrome, Edge, Brave ou Opera sur ordinateur. + Si tu as ouvert ce fichier en file:// et que ça ne marche pas, sers-le en local : + python -m http.server puis ouvre http://localhost:8000/tools/mb_test.html. +
+ +
+
+ + + + déconnecté +
+
+ +
+
    +
  1. Backup — copie l'app interne vers le slot 0 externe (sans danger). Attends l'ack.
  2. +
  3. Optionnel : dumpe le slot 0 (0x020000) dans UV Studio pour vérifier.
  4. +
  5. Restore + reset — reprogramme l'interne depuis le slot 0. Succès = la radio reboote sur le même firmware.
  6. +
+
+ + +
+
+ + + + +
+
+ Corrompre → le prochain Restore doit être refusé (CRC32). Valider → contrôle header + CRC32 sans rien reflasher, renvoie le verdict et le CRC calculé (ne peut pas se bloquer). +
+
+ +
+
+ Journal + +
+
+
+
+ + + + diff --git a/tools/misc/main.cpp b/tools/misc/main.cpp index ec5d917a..03097329 100644 --- a/tools/misc/main.cpp +++ b/tools/misc/main.cpp @@ -20,332 +20,6 @@ #include #include -// ************************************************************************ -// create a front end gain table for the firmware - - // <9:8> = LNA Gain Short - // 3 = 0dB < original value - // 2 = -24dB // was -11 - // 1 = -30dB // was -16 - // 0 = -33dB // was -19 - // - // <7:5> = LNA Gain - // 7 = 0dB - // 6 = -2dB - // 5 = -4dB - // 4 = -6dB - // 3 = -9dB - // 2 = -14dB < original value - // 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 - -typedef struct -{ - uint8_t lna_short; - uint8_t lna; - uint8_t mixer; - uint8_t pga; - int16_t lna_short_dB; - int16_t lna_dB; - int16_t mixer_dB; - int16_t pga_dB; - int16_t sum_dB; -} t_gain_table; - -void create_gain_table(const char *filename) -{ - std::vector gain_table; - - if (filename == NULL) - return; - - // front end register dB values -// const int16_t lna_short_dB[4] = { (-19), (-16), (-11), (0)}; // was - const int16_t lna_short_dB[4] = { (-33), (-30), (-24), (0)}; // corrected - const int16_t lna_dB[8] = { (-24), (-19), (-14), (-9), (-6), (-4), (-2), (0)}; - const int16_t mixer_dB[4] = { (-8), (-6), (-3), (0)}; - const int16_t pga_dB[8] = { (-33), (-27), (-21), (-15), (-9), (-6), (-3), (0)}; - - const uint8_t orig_lna_short = 3; - const uint8_t orig_lna = 2; - const uint8_t orig_mixer = 3; - const uint8_t orig_pga = 6; - - const int16_t orig_gain_dB = - lna_short_dB[orig_lna_short] + - lna_dB[orig_lna] + - mixer_dB[orig_mixer] + - pga_dB[orig_pga]; - - #if 1 - // full table - const uint8_t lna_short_min = 0; // 0 - const uint8_t lna_min = 0; // 0 - const uint8_t mixer_min = 0; // 0 - const uint8_t pga_min = 0; // 0 - - const uint8_t lna_short_max = 3; // 3 - const uint8_t lna_max = 7; // 5 - const uint8_t mixer_max = 3; // 3 - const uint8_t pga_max = 7; // 7 - #else - // just one register changes - const uint8_t lna_short_min = 0; - const uint8_t lna_min = 2; - const uint8_t mixer_min = 3; - const uint8_t pga_min = 6; - - const uint8_t lna_short_max = 3; - const uint8_t lna_max = 2; - const uint8_t mixer_max = 3; - const uint8_t pga_max = 6; - #endif - - uint8_t lna_short = lna_short_min; - uint8_t lna = lna_min; - uint8_t mixer = mixer_min; - uint8_t pga = pga_min; - - unsigned int original_index = 0; - - while (true) - { - t_gain_table entry; - - entry.lna_short = lna_short; - entry.lna = lna; - entry.mixer = mixer; - entry.pga = pga; - - entry.lna_short_dB = lna_short_dB[lna_short]; - entry.lna_dB = lna_dB[lna]; - entry.mixer_dB = mixer_dB[mixer]; - entry.pga_dB = pga_dB[pga]; - - entry.sum_dB = lna_short_dB[lna_short] + lna_dB[lna] + mixer_dB[mixer] + pga_dB[pga]; - - if (entry.sum_dB != orig_gain_dB) - gain_table.push_back(entry); - else - if (lna_short == orig_lna_short && lna == orig_lna && mixer == orig_mixer && pga == orig_pga) - gain_table.push_back(entry); - - if (++pga <= pga_max) - continue; - pga = pga_min; - - if (++mixer <= mixer_max) - continue; - mixer = mixer_min; - - if (++lna <= lna_max) - continue; - lna = lna_min; - - if (++lna_short <= lna_short_max) - continue; -// lna_short = lna_short_min; - - break; - } - - // sort the table according top the sum dB - for (unsigned int i = 0; i < (gain_table.size() - 1); i++) - { - t_gain_table entry1 = gain_table[i]; - for (unsigned int k = i + 1; k < gain_table.size(); k++) - { - t_gain_table entry2 = gain_table[k]; - if (entry2.sum_dB < entry1.sum_dB) - { // swap - const t_gain_table entry = entry1; - entry1 = entry2; - entry2 = entry; - gain_table[i] = entry1; - gain_table[k] = entry2; - } - } - } - - { // remove sum_dB duplicates - unsigned int i = 0; - while (i < gain_table.size()) - { - const t_gain_table entry1 = gain_table[i++]; - - if (entry1.lna_short == orig_lna_short && - entry1.lna == orig_lna && - entry1.mixer == orig_mixer && - entry1.pga == orig_pga) - continue; // leave the original inplace - - while (i < gain_table.size()) - { - const t_gain_table entry2 = gain_table[i]; - - if (entry2.lna_short == orig_lna_short && - entry2.lna == orig_lna && - entry2.mixer == orig_mixer && - entry2.pga == orig_pga) - break; // leave the original inplace - - if (entry2.sum_dB != entry1.sum_dB) - break; - - gain_table.erase(gain_table.begin() + i, gain_table.begin() + i + 1); - } - } - } - - // find the index for the original Quansheng register settings - for (int i = (int)gain_table.size() - 1; i >= 0; i--) - { - const t_gain_table entry = gain_table[i]; - - if (entry.sum_dB != orig_gain_dB) - continue; - - if (entry.lna_short != orig_lna_short || - entry.lna != orig_lna || - entry.mixer != orig_mixer || - entry.pga != orig_pga) - continue; - - original_index = i; - break; - } - - // *************************** - // save the table to a file - -/* - typedef struct - { - #if 1 - // bitfields take up less flash bytes - uint8_t lna_short:2; // 0 ~ 3 - uint8_t lna:3; // 0 ~ 7 - uint8_t mixer:2; // 0 ~ 3 - uint8_t pga:3; // 0 ~ 7 - #else - uint8_t lna_short; // 0 ~ 3 - uint8_t lna; // 0 ~ 7 - uint8_t mixer; // 0 ~ 3 - uint8_t pga; // 0 ~ 7 - #endif - } t_am_fix_gain_table; - //} __attribute__((packed)) t_am_fix_gain_table; -*/ - - FILE *file = fopen(filename, "w"); - if (file == NULL) - return; - - fprintf(file, "\n"); - fprintf(file, "\tstatic const t_am_fix_gain_table am_fix_gain_table[] =\n"); - fprintf(file, "\t{\n"); - - #if 0 - fprintf(file, "\t\t{.lna_short = 3, .lna = 2, .mixer = 3, .pga = 6}, // 0 0dB -14dB 0dB -3dB .. -17dB original\n\n"); - - for (unsigned int i = 0; i < gain_table.size(); i++) - { - char s[1024]; - - const t_gain_table entry = gain_table[i]; - - sprintf(s, "\t\t{%u, %u, %u, %u}, // %3u .. %3ddB %3ddB %2ddB %3ddB .. %3ddB", - entry.lna_short, - entry.lna, - entry.mixer, - entry.pga, - 1 + i, - entry.lna_short_dB, - entry.lna_dB, - entry.mixer_dB, - entry.pga_dB, - entry.sum_dB); - - if (i == original_index) - strcat(s, " original"); - - fprintf(file, "%s\n", s); - } - #else - { - //BK4819_WriteRegister(BK4819_REG_13, ((uint16_t)gains.lna_short << 8) | ((uint16_t)gains.lna << 5) | ((uint16_t)gains.mixer << 3) | ((uint16_t)gains.pga << 0)); - - uint16_t reg_val; - int16_t sum_dB; - - reg_val = ((uint16_t)orig_lna_short << 8) | ((uint16_t)orig_lna << 5) | ((uint16_t)orig_mixer << 3) | ((uint16_t)orig_pga << 0); - sum_dB = lna_short_dB[orig_lna_short] + lna_dB[orig_lna] + mixer_dB[orig_mixer] + pga_dB[orig_pga]; - fprintf(file, "\t\t{0x%04X, %-3d}, // 0 .. %u %u %u %u .. 0dB -14dB 0dB -3dB .. -17dB original\n\n", - reg_val, - sum_dB, - orig_lna_short, - orig_lna, - orig_mixer, - orig_pga); - - for (unsigned int i = 0; i < gain_table.size(); i++) - { - char s[1024]; - - const t_gain_table entry = gain_table[i]; - - reg_val = ((uint16_t)entry.lna_short << 8) | ((uint16_t)entry.lna << 5) | ((uint16_t)entry.mixer << 3) | ((uint16_t)entry.pga << 0); - sum_dB = lna_short_dB[entry.lna_short] + lna_dB[entry.lna] + mixer_dB[entry.mixer] + pga_dB[entry.pga]; - - sprintf(s, "\t\t{0x%04X, %-3d}, // %3u .. %u %u %u %u .. %3ddB %3ddB %2ddB %3ddB .. %3ddB", - reg_val, - sum_dB, - - 1 + i, - - entry.lna_short, - entry.lna, - entry.mixer, - entry.pga, - - entry.lna_short_dB, - entry.lna_dB, - entry.mixer_dB, - entry.pga_dB, - - entry.sum_dB); - - if (i == original_index) - strcat(s, " original"); - - fprintf(file, "%s\n", s); - } - } - #endif - - fprintf(file, "\t};\n\n"); - - fprintf(file, "\tstatic const unsigned int original_index = %u;\n", 1 + original_index); - - fclose(file); -} - // ************************************************************************ // "rotate_font()" has nothing to do with this program at all, I just needed // to write a bit of code to rotate some fonts I've drawn @@ -464,8 +138,6 @@ void rotate_font(const char *filename1, const char *filename2) #pragma argsused int main(int argc, char* argv[]) { - create_gain_table("gain_table.c"); - rotate_font("uv-k5_small.bin", "uv-k5_small.c"); rotate_font("uv-k5_small_bold.bin", "uv-k5_small_bold.c");