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..012d28dd 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Anyway, have fun. # Donations -Special thanks to Jean-Cyrille F6IWW (3 times), Fabrice 14RC123, David F4BPP, Olivier 14RC206, Frédéric F4ESO, Stéphane F5LGW (2 times), Jorge Ornelas (4 times), Laurent F4AXK, Christophe Morel, Clayton W0LED, Pierre Antoine F6FWB, Jean-Claude 14FRS3306, Thierry F4GVO, Eric F1NOU, PricelessToolkit, Ady M6NYJ, Tom McGovern (4 times), Joseph Roth, Pierre-Yves Colin, Frank DJ7FG, Marcel Testaz, Brian Frobisher, Yannick F4JFO, Paolo Bussola, Dirk DL8DF, Levente Szőke (2 times), Bernard-Michel Herrera, Jérôme Saintespes, Paul Davies, RS (3 times), Johan F4WAT, Robert Wörle, Rafael Sundorf, Paul Harker, Peter Fintl, Pascal F4ICR (2 times), Mike DL2MF (3 times), Eric KI1C / F4WFS (3 times), Phil G0ELM, Jérôme Lambert, Eliot Vedel, Alfonso EA7KDF, Jean-François F1EVM, Robert DC1RDB (2 times), Ian KE2CHJ, Daryl VK3AWA, Roberto Brunelli, Robert Boardman, Stephen Oliver, Nicolas F4INE, William Bruno, Daniel OK2VLK, Tayler Chew, Peter DL7RFP, Philippe Kopp, Rune LA6YMA, Jeremy Luna, Steef Wagenaar (2 times), Zhuo BG7SGA, Jamie M0JLB, Antoine LIBERT, Vince K0DKR, Julia DF7JA, Ken 2E0UMK, Victor TI2SYS, Tobi DG9LAY, Deaglan K4DFQ, Catherine PALMER, Brian WA6JFK, Stéphane Hintzy, Roger F1HCN, Marcin Kusaj, Flavio Cottarelli, Bob N1MLZ, Carlos EA1IJ, Brian M7YLF, Giuseppe IT9LLH and 邓 月 for their [donations](https://www.paypal.com/paypalme/F4HWN). That’s so kind of them. Thanks so much 🙏🏻 +Special thanks to Jean-Cyrille F6IWW (3 times), Fabrice 14RC123, David F4BPP, Olivier 14RC206, Frédéric F4ESO, Stéphane F5LGW (2 times), Jorge Ornelas (4 times), Laurent F4AXK, Christophe Morel, Clayton W0LED, Pierre Antoine F6FWB, Jean-Claude 14FRS3306, Thierry F4GVO, Eric F1NOU, PricelessToolkit, Ady M6NYJ, Tom McGovern (4 times), Joseph Roth, Pierre-Yves Colin, Frank DJ7FG, Marcel Testaz, Brian Frobisher, Yannick F4JFO, Paolo Bussola, Dirk DL8DF, Levente Szőke (2 times), Bernard-Michel Herrera, Jérôme Saintespes, Paul Davies, RS (3 times), Johan F4WAT, Robert Wörle, Rafael Sundorf, Paul Harker, Peter Fintl, Pascal F4ICR (2 times), Mike DL2MF (3 times), Eric KI1C / F4WFS (3 times), Phil G0ELM, Jérôme Lambert, Eliot Vedel, Alfonso EA7KDF, Jean-François F1EVM, Robert DC1RDB (2 times), Ian KE2CHJ, Daryl VK3AWA, Roberto Brunelli, Robert Boardman, Stephen Oliver, Nicolas F4INE, William Bruno, Daniel OK2VLK, Tayler Chew, Peter DL7RFP, Philippe Kopp, Rune LA6YMA, Jeremy Luna, Steef Wagenaar (2 times), Zhuo BG7SGA, Jamie M0JLB, Antoine LIBERT, Vince K0DKR, Julia DF7JA, Ken 2E0UMK, Victor TI2SYS, Tobi DG9LAY, Deaglan K4DFQ, Catherine PALMER, Brian WA6JFK, Stéphane Hintzy, Roger F1HCN, Marcin Kusaj, Flavio Cottarelli, Bob N1MLZ, Carlos EA1IJ, Brian M7YLF, Giuseppe IT9LLH, 邓 月 and Jon M1JRH for their [donations](https://www.paypal.com/paypalme/F4HWN). That’s so kind of them. Thanks so much 🙏🏻 ## Table of Contents @@ -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/archive/apps/v6.0.0/Beacon.app b/archive/apps/v6.0.0/Beacon.app new file mode 100644 index 00000000..8b65fe00 Binary files /dev/null and b/archive/apps/v6.0.0/Beacon.app differ diff --git a/archive/apps/v6.0.0/Beam.app b/archive/apps/v6.0.0/Beam.app new file mode 100644 index 00000000..39e39410 Binary files /dev/null and b/archive/apps/v6.0.0/Beam.app differ diff --git a/archive/apps/v6.0.0/Breakout.app b/archive/apps/v6.0.0/Breakout.app new file mode 100644 index 00000000..8fa79657 Binary files /dev/null and b/archive/apps/v6.0.0/Breakout.app differ diff --git a/archive/apps/v6.0.0/BroadcastFM.app b/archive/apps/v6.0.0/BroadcastFM.app new file mode 100644 index 00000000..529aad24 Binary files /dev/null and b/archive/apps/v6.0.0/BroadcastFM.app differ diff --git a/archive/apps/v6.0.0/Cube3D.app b/archive/apps/v6.0.0/Cube3D.app new file mode 100644 index 00000000..f007a85b Binary files /dev/null and b/archive/apps/v6.0.0/Cube3D.app differ diff --git a/archive/apps/v6.0.0/FoxHunt.app b/archive/apps/v6.0.0/FoxHunt.app new file mode 100644 index 00000000..4ece1f61 Binary files /dev/null and b/archive/apps/v6.0.0/FoxHunt.app differ diff --git a/archive/apps/v6.0.0/Plasma.app b/archive/apps/v6.0.0/Plasma.app new file mode 100644 index 00000000..c0d4ffd0 Binary files /dev/null and b/archive/apps/v6.0.0/Plasma.app differ diff --git a/archive/apps/v6.0.0/Tetris.app b/archive/apps/v6.0.0/Tetris.app new file mode 100644 index 00000000..341fa221 Binary files /dev/null and b/archive/apps/v6.0.0/Tetris.app differ diff --git a/archive/apps/v6.0.0/TripleVFO.app b/archive/apps/v6.0.0/TripleVFO.app new file mode 100644 index 00000000..017c7139 Binary files /dev/null and b/archive/apps/v6.0.0/TripleVFO.app differ diff --git a/archive/f4hwn.fieldops.v6.0.0.bin b/archive/f4hwn.fieldops.v6.0.0.bin new file mode 100755 index 00000000..ddbcef14 Binary files /dev/null and b/archive/f4hwn.fieldops.v6.0.0.bin differ diff --git a/archive/f4hwn.fusion.v6.0.0.bin b/archive/f4hwn.fusion.v6.0.0.bin new file mode 100755 index 00000000..ab08c9ad Binary files /dev/null and b/archive/f4hwn.fusion.v6.0.0.bin differ diff --git a/archive/f4hwn.labs.v6.0.0.bin b/archive/f4hwn.labs.v6.0.0.bin new file mode 100755 index 00000000..a117881d Binary files /dev/null and b/archive/f4hwn.labs.v6.0.0.bin differ diff --git a/archive/f4hwn.transfer.v6.0.0.bin b/archive/f4hwn.transfer.v6.0.0.bin new file mode 100755 index 00000000..b7b61bdc Binary files /dev/null and b/archive/f4hwn.transfer.v6.0.0.bin differ 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");