mirror of
https://github.com/armel/uv-k1-k5v3-firmware-custom.git
synced 2026-10-02 03:15:37 +00:00
128 files changed
+10760
-2502
No files matched your search
@@ -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
|
||||
@@ -17,3 +17,11 @@ __pycache__/
|
||||
|
||||
.DS_Store
|
||||
AGENTS.md
|
||||
|
||||
# Overlay-app build artifacts (generated by build.sh / compile-app.sh)
|
||||
App/apps/*/*.elf
|
||||
App/apps/*/*.bin
|
||||
App/apps/*/*.map
|
||||
App/apps/*/*.app
|
||||
App/apps/*/*_app_blob.h
|
||||
build/apps/
|
||||
+56
-13
@@ -95,8 +95,7 @@ if(ENABLE_AIRCOPY OR ENABLE_UART OR ENABLE_USB)
|
||||
endif()
|
||||
|
||||
enable_feature(ENABLE_UART
|
||||
driver/uart.c
|
||||
app/uart.c
|
||||
driver/uart.c
|
||||
# driver/aes.c
|
||||
)
|
||||
|
||||
@@ -118,11 +117,17 @@ endif()
|
||||
|
||||
# ---- STOCK QUANSHENG FEATURES ----
|
||||
|
||||
# ENABLE_FMRADIO keeps the BK1080 driver and the FM data/helpers shared with the
|
||||
# overlay. ENABLE_FMRADIO_EMBEDDED additionally selects the legacy resident
|
||||
# state machine and its integration with the main scheduler.
|
||||
enable_feature(ENABLE_FMRADIO
|
||||
driver/bk1080.c
|
||||
app/fm.c
|
||||
ui/fmradio.c
|
||||
driver/bk1080.c
|
||||
app/fm.c
|
||||
)
|
||||
if(ENABLE_FMRADIO AND NOT ENABLE_FEAT_F4HWN_OVERLAY_APPS)
|
||||
target_compile_definitions(App INTERFACE ENABLE_FMRADIO_EMBEDDED)
|
||||
target_sources(App INTERFACE ui/fmradio.c)
|
||||
endif()
|
||||
enable_feature(ENABLE_AIRCOPY
|
||||
app/aircopy.c
|
||||
ui/aircopy.c
|
||||
@@ -132,7 +137,6 @@ enable_feature(ENABLE_VOICE
|
||||
driver/voice.c
|
||||
)
|
||||
enable_feature(ENABLE_VOX)
|
||||
enable_feature(ENABLE_ALARM)
|
||||
enable_feature(ENABLE_TX1750)
|
||||
enable_feature(ENABLE_PWRON_PASSWORD
|
||||
ui/lock.c
|
||||
@@ -152,7 +156,6 @@ enable_feature(ENABLE_SMALL_BOLD)
|
||||
enable_feature(ENABLE_CUSTOM_MENU_LAYOUT)
|
||||
enable_feature(ENABLE_KEEP_MEM_NAME)
|
||||
enable_feature(ENABLE_WIDE_RX)
|
||||
enable_feature(ENABLE_TX_WHEN_AM)
|
||||
enable_feature(ENABLE_F_CAL_MENU)
|
||||
enable_feature(ENABLE_CTCSS_TAIL_PHASE_SHIFT)
|
||||
enable_feature(ENABLE_BOOT_BEEPS)
|
||||
@@ -167,7 +170,6 @@ enable_feature(ENABLE_FEAT_F4HWN_AUDIO_SCOPE)
|
||||
enable_feature(ENABLE_COPY_CHAN_TO_VFO)
|
||||
enable_feature(ENABLE_REDUCE_LOW_MID_TX_POWER)
|
||||
enable_feature(ENABLE_BYP_RAW_DEMODULATORS)
|
||||
enable_feature(ENABLE_BLMIN_TMP_OFF)
|
||||
enable_feature(ENABLE_SCAN_RANGES)
|
||||
|
||||
# ---- CONTRIB MODS ----
|
||||
@@ -177,10 +179,36 @@ enable_feature(ENABLE_EXTRA_UART_CMD)
|
||||
|
||||
# ---- F4HWN MODS ----
|
||||
|
||||
if(NOT ENABLE_UART AND NOT ENABLE_USB)
|
||||
if(ENABLE_FEAT_F4HWN_K5VIEWER OR ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER)
|
||||
message(STATUS "Disabling K5Viewer because both ENABLE_UART and ENABLE_USB are OFF.")
|
||||
endif()
|
||||
set(ENABLE_FEAT_F4HWN_K5VIEWER OFF)
|
||||
set(ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER OFF)
|
||||
endif()
|
||||
|
||||
enable_feature(ENABLE_FEAT_F4HWN)
|
||||
enable_feature(ENABLE_FEAT_F4HWN_GAME
|
||||
app/breakout.c
|
||||
# GAME keeps the F+7 hook; the resident breakout.c is compiled only when the
|
||||
# overlay does NOT provide Breakout as an app (that is the flash we recover).
|
||||
if(ENABLE_FEAT_F4HWN_GAME)
|
||||
target_compile_definitions(App INTERFACE ENABLE_FEAT_F4HWN_GAME)
|
||||
if(NOT ENABLE_FEAT_F4HWN_OVERLAY_APPS)
|
||||
target_sources(App INTERFACE app/breakout.c)
|
||||
endif()
|
||||
endif()
|
||||
enable_feature(ENABLE_FEAT_F4HWN_OVERLAY_APPS
|
||||
apps/app_overlay.c
|
||||
apps/app_menu.c
|
||||
)
|
||||
enable_feature(ENABLE_FEAT_F4HWN_OVERLAY_TRIVFO)
|
||||
enable_feature(ENABLE_FEAT_F4HWN_OVERLAY_BEAM)
|
||||
if(ENABLE_FEAT_F4HWN_OVERLAY_APPS AND NOT ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY)
|
||||
message(FATAL_ERROR "ENABLE_FEAT_F4HWN_OVERLAY_APPS requires ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY (the 4 KiB overlay workspace).")
|
||||
endif()
|
||||
if((ENABLE_FEAT_F4HWN_OVERLAY_TRIVFO OR ENABLE_FEAT_F4HWN_OVERLAY_BEAM) AND
|
||||
NOT ENABLE_FEAT_F4HWN_OVERLAY_APPS)
|
||||
message(FATAL_ERROR "Overlay Triple VFO/Beam services require ENABLE_FEAT_F4HWN_OVERLAY_APPS.")
|
||||
endif()
|
||||
enable_feature(ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
k5viewer.c
|
||||
)
|
||||
@@ -211,9 +239,11 @@ enable_feature(ENABLE_FEAT_F4HWN_BEAM
|
||||
enable_feature(ENABLE_FEAT_F4HWN_RXTX_LOG
|
||||
app/rxtx_log.c
|
||||
)
|
||||
enable_feature(ENABLE_FEAT_F4HWN_FOXHUNT
|
||||
app/foxhunt.c
|
||||
)
|
||||
enable_feature(ENABLE_FEAT_F4HWN_FOXHUNT)
|
||||
enable_feature(ENABLE_FEAT_F4HWN_BEACON)
|
||||
if(ENABLE_FEAT_F4HWN_FOXHUNT OR ENABLE_FEAT_F4HWN_BEACON)
|
||||
target_sources(App INTERFACE app/foxhunt.c)
|
||||
endif()
|
||||
enable_feature(ENABLE_FEAT_F4HWN_ACTION_PICKER)
|
||||
|
||||
if(ENABLE_FEAT_F4HWN_ACTION_PICKER AND NOT ENABLE_FEAT_F4HWN)
|
||||
@@ -230,6 +260,19 @@ endif()
|
||||
if(ENABLE_FEAT_F4HWN_BEAM AND NOT ENABLE_AIRCOPY)
|
||||
message(FATAL_ERROR "ENABLE_FEAT_F4HWN_BEAM requires ENABLE_AIRCOPY (it reuses g_FSK_Buffer, AIRCOPY_Obfuscate and the FSK packet plumbing).")
|
||||
endif()
|
||||
enable_feature(ENABLE_FEAT_F4HWN_MULTIBOOT
|
||||
driver/mb_flash.c
|
||||
ui/multiboot.c
|
||||
)
|
||||
if(ENABLE_FEAT_F4HWN_MULTIBOOT AND NOT ENABLE_FEAT_F4HWN)
|
||||
message(FATAL_ERROR "ENABLE_FEAT_F4HWN_MULTIBOOT requires ENABLE_FEAT_F4HWN (the SetCfg menu, SysInfo badge and multiboot UI all live under ENABLE_FEAT_F4HWN).")
|
||||
endif()
|
||||
enable_feature(ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY)
|
||||
|
||||
if(ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY AND NOT ENABLE_FEAT_F4HWN_MULTIBOOT)
|
||||
message(FATAL_ERROR "ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY requires ENABLE_FEAT_F4HWN_MULTIBOOT.")
|
||||
endif()
|
||||
|
||||
enable_feature(ENABLE_FEAT_F4HWN_QRCODE)
|
||||
enable_feature(ENABLE_FEAT_F4HWN_LOGO)
|
||||
enable_feature(ENABLE_FEAT_F4HWN_LOGO_SAV)
|
||||
|
||||
-397
@@ -1,397 +0,0 @@
|
||||
|
||||
/* Copyright 2023 OneOfEleven
|
||||
* https://github.com/DualTachyon
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// code to 'try' and reduce the AM demodulator saturation problem
|
||||
//
|
||||
// that is until someone works out how to properly configure the BK chip !
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "am_fix.h"
|
||||
#include "app/main.h"
|
||||
#include "board.h"
|
||||
#include "driver/bk4819.h"
|
||||
#include "external/printf/printf.h"
|
||||
#include "frequencies.h"
|
||||
#include "functions.h"
|
||||
#include "misc.h"
|
||||
#include "settings.h"
|
||||
#ifdef ENABLE_AGC_SHOW_DATA
|
||||
#include "ui/main.h"
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_AM_FIX
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uint16_t reg_val;
|
||||
int8_t gain_dB;
|
||||
} __attribute__((packed)) t_gain_table;
|
||||
|
||||
// REG_10 AGC gain table
|
||||
//
|
||||
// <15:10> ???
|
||||
//
|
||||
// <9:8> = LNA Gain Short
|
||||
// 3 = 0dB < original value
|
||||
// 2 = -19dB // was -11
|
||||
// 1 = -24dB // was -16
|
||||
// 0 = -28dB // was -19
|
||||
//
|
||||
// <7:5> = LNA Gain
|
||||
// 7 = 0dB
|
||||
// 6 = -2dB
|
||||
// 5 = -4dB < original value
|
||||
// 4 = -6dB
|
||||
// 3 = -9dB
|
||||
// 2 = -14dB
|
||||
// 1 = -19dB
|
||||
// 0 = -24dB
|
||||
//
|
||||
// <4:3> = MIXER Gain
|
||||
// 3 = 0dB < original value
|
||||
// 2 = -3dB
|
||||
// 1 = -6dB
|
||||
// 0 = -8dB
|
||||
//
|
||||
// <2:0> = PGA Gain
|
||||
// 7 = 0dB
|
||||
// 6 = -3dB < original value
|
||||
// 5 = -6dB
|
||||
// 4 = -9dB
|
||||
// 3 = -15dB
|
||||
// 2 = -21dB
|
||||
// 1 = -27dB
|
||||
// 0 = -33dB
|
||||
|
||||
// front end register dB values
|
||||
//
|
||||
// these values need to be accurate for the code to properly/reliably switch
|
||||
// between table entries when adjusting the front end registers.
|
||||
//
|
||||
// these 4 tables need a measuring/calibration update
|
||||
//
|
||||
//// static const int16_t lna_short_dB[] = { -19, -16, -11, 0}; // was (but wrong)
|
||||
// static const int16_t lna_short_dB[] = { (-28), (-24), (-19), 0}; // corrected'ish
|
||||
// static const int16_t lna_dB[] = { (-24), (-19), (-14), ( -9), (-6), (-4), (-2), 0};
|
||||
// static const int16_t mixer_dB[] = { ( -8), ( -6), ( -3), 0};
|
||||
// static const int16_t pga_dB[] = { (-33), (-27), (-21), (-15), (-9), (-6), (-3), 0};
|
||||
|
||||
// lookup table is hugely easier than writing code to do the same
|
||||
//
|
||||
|
||||
#define LOOKUP_TABLE 1
|
||||
|
||||
#if LOOKUP_TABLE
|
||||
static const t_gain_table gain_table[] =
|
||||
{
|
||||
{0x03BE, -7}, // 0 .. 3 5 3 6 .. 0dB -4dB 0dB -3dB .. -7dB original
|
||||
|
||||
{0x0000,-93}, // 1 .. 0 0 0 0 .. -28dB -24dB -8dB -33dB .. -93dB
|
||||
{0x0008,-91}, // 2 .. 0 0 1 0 .. -28dB -24dB -6dB -33dB .. -91dB
|
||||
{0x0010,-88}, // 3 .. 0 0 2 0 .. -28dB -24dB -3dB -33dB .. -88dB
|
||||
{0x0001,-87}, // 4 .. 0 0 0 1 .. -28dB -24dB -8dB -27dB .. -87dB
|
||||
{0x0009,-85}, // 5 .. 0 0 1 1 .. -28dB -24dB -6dB -27dB .. -85dB
|
||||
{0x0011,-82}, // 6 .. 0 0 2 1 .. -28dB -24dB -3dB -27dB .. -82dB
|
||||
{0x0002,-81}, // 7 .. 0 0 0 2 .. -28dB -24dB -8dB -21dB .. -81dB
|
||||
{0x000A,-79}, // 8 .. 0 0 1 2 .. -28dB -24dB -6dB -21dB .. -79dB
|
||||
{0x0012,-76}, // 9 .. 0 0 2 2 .. -28dB -24dB -3dB -21dB .. -76dB
|
||||
{0x0003,-75}, // 10 .. 0 0 0 3 .. -28dB -24dB -8dB -15dB .. -75dB
|
||||
{0x000B,-73}, // 11 .. 0 0 1 3 .. -28dB -24dB -6dB -15dB .. -73dB
|
||||
{0x0013,-70}, // 12 .. 0 0 2 3 .. -28dB -24dB -3dB -15dB .. -70dB
|
||||
{0x0004,-69}, // 13 .. 0 0 0 4 .. -28dB -24dB -8dB -9dB .. -69dB
|
||||
{0x000C,-67}, // 14 .. 0 0 1 4 .. -28dB -24dB -6dB -9dB .. -67dB
|
||||
{0x000D,-64}, // 15 .. 0 0 1 5 .. -28dB -24dB -6dB -6dB .. -64dB
|
||||
{0x001C,-61}, // 16 .. 0 0 3 4 .. -28dB -24dB 0dB - 9dB .. -61dB
|
||||
{0x001D,-58}, // 17 .. 0 0 3 5 .. -28dB -24dB 0dB -6dB .. -58dB
|
||||
{0x001E,-55}, // 18 .. 0 0 3 6 .. -28dB -24dB 0dB -3dB .. -55dB
|
||||
{0x001F,-52}, // 19 .. 0 0 3 7 .. -28dB -24dB 0dB 0dB .. -52dB
|
||||
{0x003E,-50}, // 20 .. 0 1 3 6 .. -28dB -19dB 0dB -3dB .. -50dB
|
||||
{0x003F,-47}, // 21 .. 0 1 3 7 .. -28dB -19dB 0dB 0dB .. -47dB
|
||||
{0x005E,-45}, // 22 .. 0 2 3 6 .. -28dB -14dB 0dB -3dB .. -45dB
|
||||
{0x005F,-42}, // 23 .. 0 2 3 7 .. -28dB -14dB 0dB 0dB .. -42dB
|
||||
{0x007E,-40}, // 24 .. 0 3 3 6 .. -28dB -9dB 0dB -3dB .. -40dB
|
||||
{0x007F,-37}, // 25 .. 0 3 3 7 .. -28dB -9dB 0dB 0dB .. -37dB
|
||||
{0x009F,-34}, // 26 .. 0 4 3 7 .. -28dB -6dB 0dB 0dB .. -34dB
|
||||
{0x00BF,-32}, // 27 .. 0 5 3 7 .. -28dB -4dB 0dB 0dB .. -32dB
|
||||
{0x00DF,-30}, // 28 .. 0 6 3 7 .. -28dB -2dB 0dB 0dB .. -30dB
|
||||
{0x00FF,-28}, // 29 .. 0 7 3 7 .. -28dB 0dB 0dB 0dB .. -28dB
|
||||
{0x01DF,-26}, // 30 .. 1 6 3 7 .. -24dB -2dB 0dB 0dB .. -26dB
|
||||
{0x01FF,-24}, // 31 .. 1 7 3 7 .. -24dB 0dB 0dB 0dB .. -24dB
|
||||
{0x02BF,-23}, // 32 .. 2 5 3 7 .. -19dB -4dB 0dB 0dB .. -23dB
|
||||
{0x02DF,-21}, // 33 .. 2 6 3 7 .. -19dB -2dB 0dB -0dB .. -21dB
|
||||
{0x02FF,-19}, // 34 .. 2 7 3 7 .. -19dB 0dB 0dB 0dB .. -19dB
|
||||
{0x035E,-17}, // 35 .. 3 2 3 6 .. 0dB -14dB 0dB -3dB .. -17dB
|
||||
{0x035F,-14}, // 36 .. 3 2 3 7 .. 0dB -14dB 0dB 0dB .. -14dB
|
||||
{0x037E,-12}, // 37 .. 3 3 3 6 .. 0dB -9dB 0dB -3dB .. -12dB
|
||||
{0x037F,-9}, // 38 .. 3 3 3 7 .. 0dB -9dB 0dB 0dB .. -9dB
|
||||
{0x038F,-6}, // 39 .. 3 4 3 7 .. 0dB - 6dB 0dB 0dB .. -6dB
|
||||
{0x03BF,-4}, // 40 .. 3 5 3 7 .. 0dB -4dB 0dB 0dB .. -4dB
|
||||
{0x03DF,-2}, // 41 .. 3 6 3 7 .. 0dB - 2dB 0dB 0dB .. -2dB
|
||||
{0x03FF,0} // 42 .. 3 7 3 7 .. 0dB 0dB 0dB 0dB .. 0dB
|
||||
};
|
||||
|
||||
const uint8_t gain_table_size = ARRAY_SIZE(gain_table);
|
||||
#else
|
||||
|
||||
t_gain_table gain_table[100] = {{0x03BE, -7}}; //original
|
||||
uint8_t gain_table_size = 0;
|
||||
|
||||
void CreateTable()
|
||||
{
|
||||
typedef union {
|
||||
struct {
|
||||
uint8_t pgaIdx:3;
|
||||
uint8_t mixerIdx:2;
|
||||
uint8_t lnaIdx:3;
|
||||
uint8_t lnaSIdx:2;
|
||||
};
|
||||
uint16_t __raw;
|
||||
} GainData;
|
||||
|
||||
static const int8_t lna_short_dB[] = {-28, -24, -19, 0}; // corrected'ish
|
||||
static const int8_t lna_dB[] = {-24, -19, -14, -9, -6, -4, -2, 0};
|
||||
static const int8_t mixer_dB[] = { -8, -6, -3, 0};
|
||||
static const int8_t pga_dB[] = {-33, -27, -21, -15, -9, -6, -3, 0};
|
||||
|
||||
unsigned i;
|
||||
for (uint8_t lnaSIdx = 0; lnaSIdx < ARRAY_SIZE(lna_short_dB); lnaSIdx++) {
|
||||
for (uint8_t lnaIdx = 0; lnaIdx < ARRAY_SIZE(lna_dB); lnaIdx++) {
|
||||
for (uint8_t mixerIdx = 0; mixerIdx < ARRAY_SIZE(mixer_dB); mixerIdx++) {
|
||||
for (uint8_t pgaIdx = 0; pgaIdx < ARRAY_SIZE(pga_dB); pgaIdx++) {
|
||||
int16_t db = lna_short_dB[lnaSIdx] + lna_dB[lnaIdx] + mixer_dB[mixerIdx] + pga_dB[pgaIdx];
|
||||
GainData gainData = {{
|
||||
pgaIdx,
|
||||
mixerIdx,
|
||||
lnaIdx,
|
||||
lnaSIdx,
|
||||
}};
|
||||
|
||||
for (i = 1; i < ARRAY_SIZE(gain_table); i++) {
|
||||
t_gain_table * gain = &gain_table[i];
|
||||
if (db == gain->gain_dB)
|
||||
break;
|
||||
if (db > gain->gain_dB)
|
||||
continue;
|
||||
if (db < gain->gain_dB) {
|
||||
if(gain->gain_dB)
|
||||
memmove(gain + 1, gain, 100 - i);
|
||||
gain->gain_dB = db;
|
||||
gain->reg_val = gainData.__raw;
|
||||
break;
|
||||
}
|
||||
gain->gain_dB = db;
|
||||
gain->reg_val = gainData.__raw;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gain_table_size = i+1;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
#ifdef ENABLE_AM_FIX_SHOW_DATA
|
||||
// display update rate
|
||||
static const unsigned int display_update_rate = 250 / 10; // max 250ms display update rate
|
||||
unsigned int counter = 0;
|
||||
#endif
|
||||
|
||||
unsigned int gain_table_index[2] = {0, 0};
|
||||
// used simply to detect a changed gain setting
|
||||
unsigned int gain_table_index_prev[2] = {0, 0};
|
||||
// holds the previous RSSI level .. we do an average of old + new RSSI reading
|
||||
int16_t prev_rssi[2] = {0, 0};
|
||||
// to help reduce gain hunting, peak hold count down tick
|
||||
unsigned int hold_counter[2] = {0, 0};
|
||||
// -89dBm, any higher and the AM demodulator starts to saturate/clip/distort
|
||||
const int16_t desired_rssi = (-89 + 160) * 2;
|
||||
|
||||
int8_t currentGainDiff;
|
||||
bool enabled = true;
|
||||
|
||||
void AM_fix_init(void)
|
||||
{ // called at boot-up
|
||||
for (int i = 0; i < 2; i++) {
|
||||
gain_table_index[i] = 0; // re-start with original QS setting
|
||||
}
|
||||
#if !LOOKUP_TABLE
|
||||
CreateTable();
|
||||
#endif
|
||||
}
|
||||
|
||||
void AM_fix_reset(const unsigned vfo)
|
||||
{ // reset the AM fixer upper
|
||||
if (vfo > 1)
|
||||
return;
|
||||
|
||||
#ifdef ENABLE_AM_FIX_SHOW_DATA
|
||||
counter = 0;
|
||||
#endif
|
||||
|
||||
prev_rssi[vfo] = 0;
|
||||
hold_counter[vfo] = 0;
|
||||
gain_table_index_prev[vfo] = 0;
|
||||
}
|
||||
|
||||
// adjust the RX gain to try and prevent the AM demodulator from
|
||||
// saturating/overloading/clipping (distorted AM audio)
|
||||
//
|
||||
// we're actually doing the BK4819's job for it here, but as the chip
|
||||
// won't/don't do it for itself, we're left to bodging it ourself by
|
||||
// playing with the RF front end gain setting
|
||||
//
|
||||
void AM_fix_10ms(const unsigned vfo)
|
||||
{
|
||||
if(!gSetting_AM_fix || !enabled || vfo > 1 )
|
||||
return;
|
||||
|
||||
if (gCurrentFunction != FUNCTION_FOREGROUND && !FUNCTION_IsRx()) {
|
||||
#ifdef ENABLE_AM_FIX_SHOW_DATA
|
||||
counter = display_update_rate; // queue up a display update as soon as we switch to RX mode
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_AM_FIX_SHOW_DATA
|
||||
if (counter > 0) {
|
||||
if (++counter >= display_update_rate) { // trigger a display update
|
||||
counter = 0;
|
||||
gUpdateDisplay = true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
static uint32_t lastFreq[2];
|
||||
if(gEeprom.VfoInfo[vfo].pRX->Frequency != lastFreq[vfo]) {
|
||||
lastFreq[vfo] = gEeprom.VfoInfo[vfo].pRX->Frequency;
|
||||
AM_fix_reset(vfo);
|
||||
}
|
||||
|
||||
int16_t rssi;
|
||||
{ // sample the current RSSI level
|
||||
// average it with the previous rssi (a bit of noise/spike immunity)
|
||||
const int16_t new_rssi = BK4819_GetRSSI();
|
||||
rssi = (prev_rssi[vfo] > 0) ? (prev_rssi[vfo] + new_rssi) / 2 : new_rssi;
|
||||
prev_rssi[vfo] = new_rssi;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_AM_FIX_SHOW_DATA
|
||||
{
|
||||
static int16_t lastRssi;
|
||||
|
||||
if (lastRssi != rssi) { // rssi changed
|
||||
lastRssi = rssi;
|
||||
|
||||
if (counter == 0) {
|
||||
counter = 1;
|
||||
gUpdateDisplay = true; // trigger a display update
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// automatically adjust the RF RX gain
|
||||
|
||||
// update the gain hold counter
|
||||
if (hold_counter[vfo] > 0)
|
||||
hold_counter[vfo]--;
|
||||
|
||||
// dB difference between actual and desired RSSI level
|
||||
int16_t diff_dB = (rssi - desired_rssi) / 2;
|
||||
|
||||
if (diff_dB > 0) { // decrease gain
|
||||
unsigned int index = gain_table_index[vfo]; // current position we're at
|
||||
|
||||
if (diff_dB >= 10) { // jump immediately to a new gain setting
|
||||
// this greatly speeds up initial gain reduction (but reduces noise/spike immunity)
|
||||
|
||||
const int16_t desired_gain_dB = (int16_t)gain_table[index].gain_dB - diff_dB + 8; // get no closer than 8dB (bit of noise/spike immunity)
|
||||
|
||||
// scan the table to see what index to jump straight too
|
||||
while (index > 1)
|
||||
if (gain_table[--index].gain_dB <= desired_gain_dB)
|
||||
break;
|
||||
}
|
||||
else
|
||||
{ // incrementally reduce the gain .. taking it slow improves noise/spike immunity
|
||||
if (index > 1)
|
||||
index--; // slow step-by-step gain reduction
|
||||
}
|
||||
|
||||
index = MAX(1u, index);
|
||||
|
||||
if (gain_table_index[vfo] != index)
|
||||
{
|
||||
gain_table_index[vfo] = index;
|
||||
hold_counter[vfo] = 30; // 300ms hold
|
||||
}
|
||||
}
|
||||
|
||||
if (diff_dB >= -6) // 6dB hysterisis (help reduce gain hunting)
|
||||
hold_counter[vfo] = 30; // 300ms hold
|
||||
|
||||
if (hold_counter[vfo] == 0)
|
||||
{ // hold has been released, we're free to increase gain
|
||||
const unsigned int index = gain_table_index[vfo] + 1; // move up to next gain index
|
||||
gain_table_index[vfo] = MIN(index, gain_table_size - 1u);
|
||||
}
|
||||
|
||||
|
||||
{ // apply the new settings to the front end registers
|
||||
const unsigned int index = gain_table_index[vfo];
|
||||
|
||||
// remember the new table index
|
||||
gain_table_index_prev[vfo] = index;
|
||||
currentGainDiff = gain_table[0].gain_dB - gain_table[index].gain_dB;
|
||||
BK4819_WriteRegister(BK4819_REG_13, gain_table[index].reg_val);
|
||||
#ifdef ENABLE_AGC_SHOW_DATA
|
||||
UI_MAIN_PrintAGC(true);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef ENABLE_AM_FIX_SHOW_DATA
|
||||
if (counter == 0) {
|
||||
counter = 1;
|
||||
gUpdateDisplay = true;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef ENABLE_AM_FIX_SHOW_DATA
|
||||
void AM_fix_print_data(const unsigned vfo, char *s) {
|
||||
if (s != NULL && vfo < ARRAY_SIZE(gain_table_index)) {
|
||||
const unsigned int index = gain_table_index[vfo];
|
||||
sprintf(s, "%2u %4ddB %3u", index, gain_table[index].gain_dB, prev_rssi[vfo]);
|
||||
counter = 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
int8_t AM_fix_get_gain_diff()
|
||||
{
|
||||
return currentGainDiff;
|
||||
}
|
||||
|
||||
void AM_fix_enable(bool on)
|
||||
{
|
||||
enabled = on;
|
||||
}
|
||||
#endif
|
||||
+119
-92
@@ -25,12 +25,15 @@
|
||||
#ifdef ENABLE_FLASHLIGHT
|
||||
#include "app/flashlight.h"
|
||||
#endif
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
#include "app/fm.h"
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_APPS
|
||||
#include "apps/app_overlay.h"
|
||||
#endif
|
||||
#include "app/scanner.h"
|
||||
#include "audio.h"
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
#include "driver/bk1080.h"
|
||||
#endif
|
||||
#include "driver/bk4819.h"
|
||||
@@ -48,26 +51,28 @@
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
|
||||
#include "app/rxtx_log.h"
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_FOXHUNT
|
||||
#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
|
||||
#include "app/foxhunt.h"
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_ACTION_PICKER
|
||||
#include "ui/menu.h"
|
||||
#endif
|
||||
|
||||
#if defined(ENABLE_FMRADIO)
|
||||
#if defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) && !defined(ENABLE_FEAT_F4HWN_BEAM)
|
||||
static void ACTION_Beam(void);
|
||||
#endif
|
||||
|
||||
#if defined(ENABLE_FMRADIO_EMBEDDED)
|
||||
static void ACTION_Scan_FM(bool bRestart);
|
||||
#endif
|
||||
|
||||
#if defined(ENABLE_ALARM) || defined(ENABLE_TX1750)
|
||||
static void ACTION_AlarmOr1750(bool b1750);
|
||||
inline static void ACTION_Alarm() { ACTION_AlarmOr1750(false); }
|
||||
inline static void ACTION_1750() { ACTION_AlarmOr1750(true); };
|
||||
#ifdef ENABLE_TX1750
|
||||
static void ACTION_1750(void);
|
||||
#endif
|
||||
|
||||
inline static void ACTION_ScanRestart() { ACTION_Scan(true); };
|
||||
|
||||
void (*const action_opt_table[])(void) = {
|
||||
void (*const action_opt_table[ACTION_OPT_LEN])(void) = {
|
||||
[ACTION_OPT_NONE] = &FUNCTION_NOP,
|
||||
[ACTION_OPT_POWER] = &ACTION_Power,
|
||||
[ACTION_OPT_MONITOR] = &ACTION_Monitor,
|
||||
@@ -79,38 +84,18 @@ void (*const action_opt_table[])(void) = {
|
||||
|
||||
#ifdef ENABLE_FLASHLIGHT
|
||||
[ACTION_OPT_FLASHLIGHT] = &ACTION_FlashLight,
|
||||
#else
|
||||
[ACTION_OPT_FLASHLIGHT] = &FUNCTION_NOP,
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_VOX
|
||||
[ACTION_OPT_VOX] = &ACTION_Vox,
|
||||
#else
|
||||
[ACTION_OPT_VOX] = &FUNCTION_NOP,
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
[ACTION_OPT_FM] = &ACTION_FM,
|
||||
#else
|
||||
[ACTION_OPT_FM] = &FUNCTION_NOP,
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_ALARM
|
||||
[ACTION_OPT_ALARM] = &ACTION_Alarm,
|
||||
#else
|
||||
[ACTION_OPT_ALARM] = &FUNCTION_NOP,
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_TX1750
|
||||
[ACTION_OPT_1750] = &ACTION_1750,
|
||||
#else
|
||||
[ACTION_OPT_1750] = &FUNCTION_NOP,
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_BLMIN_TMP_OFF
|
||||
[ACTION_OPT_BLMIN_TMP_OFF] = &ACTION_BlminTmpOff,
|
||||
#else
|
||||
[ACTION_OPT_BLMIN_TMP_OFF] = &FUNCTION_NOP,
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN
|
||||
@@ -118,7 +103,6 @@ void (*const action_opt_table[])(void) = {
|
||||
[ACTION_OPT_MAINONLY] = &ACTION_MainOnly,
|
||||
[ACTION_OPT_PTT] = &ACTION_Ptt,
|
||||
[ACTION_OPT_WN] = &ACTION_Wn,
|
||||
[ACTION_OPT_BACKLIGHT] = &ACTION_BackLight,
|
||||
//#if !defined(ENABLE_SPECTRUM) || !defined(ENABLE_FMRADIO)
|
||||
[ACTION_OPT_MUTE] = &ACTION_Mute,
|
||||
//#else
|
||||
@@ -126,29 +110,65 @@ void (*const action_opt_table[])(void) = {
|
||||
//#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_AUDIO
|
||||
[ACTION_OPT_RXA] = &ACTION_RxA,
|
||||
#else
|
||||
[ACTION_OPT_RXA] = &FUNCTION_NOP,
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
|
||||
[ACTION_OPT_POWER_HIGH] = &ACTION_Power_High,
|
||||
[ACTION_OPT_REMOVE_OFFSET] = &ACTION_Remove_Offset,
|
||||
#endif
|
||||
#else
|
||||
[ACTION_OPT_RXMODE] = &FUNCTION_NOP,
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_BEAM
|
||||
#if defined(ENABLE_FEAT_F4HWN_BEAM) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
|
||||
[ACTION_OPT_BEAM] = &ACTION_Beam,
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
|
||||
[ACTION_OPT_RXTX_LOG] = &ACTION_RxTxLog,
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_FOXHUNT
|
||||
#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
|
||||
[ACTION_OPT_FOXHUNT] = &ACTION_FoxHunt,
|
||||
#endif
|
||||
#if defined(ENABLE_FEAT_F4HWN_BEACON) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
|
||||
[ACTION_OPT_BEACON] = &ACTION_Beacon,
|
||||
#endif
|
||||
};
|
||||
|
||||
static_assert(ARRAY_SIZE(action_opt_table) == ACTION_OPT_LEN);
|
||||
static_assert(ACTION_OPT_RXTX_LOG == 18);
|
||||
static_assert(ACTION_OPT_BEAM == 19);
|
||||
static_assert(ACTION_OPT_POWER_HIGH == 20);
|
||||
static_assert(ACTION_OPT_REMOVE_OFFSET == 21);
|
||||
static_assert(ACTION_OPT_FOXHUNT == 22);
|
||||
static_assert(ACTION_OPT_BEACON == 23);
|
||||
|
||||
bool ACTION_IsAvailable(uint8_t action)
|
||||
{
|
||||
if (action >= ACTION_OPT_LEN || action_opt_table[action] == NULL)
|
||||
return false;
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_APPS
|
||||
switch (action) {
|
||||
#ifdef ENABLE_FMRADIO
|
||||
case ACTION_OPT_FM:
|
||||
return (APP_OverlayShortcutMask() & APP_SHORTCUT_FM) != 0;
|
||||
#endif
|
||||
#ifndef ENABLE_FEAT_F4HWN_FOXHUNT
|
||||
case ACTION_OPT_FOXHUNT:
|
||||
return (APP_OverlayShortcutMask() & APP_SHORTCUT_FOXHUNT) != 0;
|
||||
#endif
|
||||
#ifndef ENABLE_FEAT_F4HWN_BEACON
|
||||
case ACTION_OPT_BEACON:
|
||||
return (APP_OverlayShortcutMask() & APP_SHORTCUT_BEACON) != 0;
|
||||
#endif
|
||||
#ifndef ENABLE_FEAT_F4HWN_BEAM
|
||||
case ACTION_OPT_BEAM:
|
||||
return (APP_OverlayShortcutMask() & APP_SHORTCUT_BEAM) != 0;
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ACTION_Power(void)
|
||||
{
|
||||
@@ -195,7 +215,7 @@ void ACTION_Monitor(void)
|
||||
|
||||
RADIO_SetupRegisters(true);
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (gFmRadioMode) {
|
||||
FM_Start();
|
||||
gRequestDisplayScreen = DISPLAY_FM;
|
||||
@@ -209,7 +229,7 @@ void ACTION_Scan(bool bRestart)
|
||||
{
|
||||
(void)bRestart;
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (gFmRadioMode) {
|
||||
ACTION_Scan_FM(bRestart);
|
||||
return;
|
||||
@@ -302,7 +322,7 @@ void ACTION_SwitchDemodul(void)
|
||||
}
|
||||
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
inline static bool ACTION_IsBlockedInFM(uint8_t action)
|
||||
{
|
||||
switch (action) {
|
||||
@@ -326,11 +346,14 @@ inline static bool ACTION_IsBlockedInFM(uint8_t action)
|
||||
case ACTION_OPT_REMOVE_OFFSET:
|
||||
#endif
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_BEAM
|
||||
#if defined(ENABLE_FEAT_F4HWN_BEAM) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
|
||||
case ACTION_OPT_BEAM:
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_FOXHUNT
|
||||
#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
|
||||
case ACTION_OPT_FOXHUNT:
|
||||
#endif
|
||||
#if defined(ENABLE_FEAT_F4HWN_BEACON) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
|
||||
case ACTION_OPT_BEACON:
|
||||
#endif
|
||||
return true;
|
||||
|
||||
@@ -340,15 +363,14 @@ inline static bool ACTION_IsBlockedInFM(uint8_t action)
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_ACTION_PICKER
|
||||
static void ACTION_Execute(uint8_t action)
|
||||
{
|
||||
if (action >= ACTION_OPT_LEN || action_opt_table[action] == NULL) {
|
||||
if (!ACTION_IsAvailable(action)) {
|
||||
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (gFmRadioMode && ACTION_IsBlockedInFM(action)) {
|
||||
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
|
||||
return;
|
||||
@@ -359,6 +381,7 @@ static void ACTION_Execute(uint8_t action)
|
||||
action_opt_table[action]();
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_ACTION_PICKER
|
||||
uint8_t gActionPickerKey;
|
||||
uint8_t gActionPickerSelection[2] = {1, 1};
|
||||
uint8_t gActionPickerTimeout_500ms;
|
||||
@@ -477,26 +500,52 @@ void ACTION_Handle(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
|
||||
}
|
||||
|
||||
// held or released after short press
|
||||
#ifdef ENABLE_FEAT_F4HWN_ACTION_PICKER
|
||||
ACTION_Execute(func);
|
||||
#else
|
||||
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
if (gFmRadioMode && ACTION_IsBlockedInFM(func)) {
|
||||
}
|
||||
|
||||
#if defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) && !defined(ENABLE_FEAT_F4HWN_BEAM)
|
||||
static void ACTION_Beam(void)
|
||||
{
|
||||
if (APP_LaunchOverlayShortcut(APP_SHORTCUT_BEAM) != APP_OK)
|
||||
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
action_opt_table[func]();
|
||||
#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
|
||||
void ACTION_FoxHunt(void)
|
||||
{
|
||||
#ifdef ENABLE_FEAT_F4HWN_FOXHUNT
|
||||
APP_RunFoxHunt();
|
||||
GUI_SelectNextDisplay(DISPLAY_MAIN);
|
||||
#else
|
||||
if (APP_LaunchOverlayShortcut(APP_SHORTCUT_FOXHUNT) != APP_OK)
|
||||
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(ENABLE_FEAT_F4HWN_BEACON) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
|
||||
void ACTION_Beacon(void)
|
||||
{
|
||||
#ifdef ENABLE_FEAT_F4HWN_BEACON
|
||||
APP_RunBeacon();
|
||||
GUI_SelectNextDisplay(DISPLAY_MAIN);
|
||||
#else
|
||||
if (APP_LaunchOverlayShortcut(APP_SHORTCUT_BEACON) != APP_OK)
|
||||
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
void ACTION_FM(void)
|
||||
{
|
||||
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_APPS
|
||||
if (APP_LaunchOverlayShortcut(APP_SHORTCUT_FM) != APP_OK)
|
||||
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
|
||||
return;
|
||||
#else
|
||||
if (gCurrentFunction != FUNCTION_TRANSMIT && gCurrentFunction != FUNCTION_MONITOR)
|
||||
{
|
||||
gInputBoxIndex = 0;
|
||||
@@ -512,6 +561,14 @@ void ACTION_FM(void)
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not start broadcast FM while a VFO reception is already active.
|
||||
// Keeping this check after the block above ensures EXIT can still
|
||||
// turn FM off if the UI ever reaches DISPLAY_MAIN with FM mode active.
|
||||
if (FUNCTION_IsRx()) {
|
||||
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
|
||||
return;
|
||||
}
|
||||
|
||||
gMonitor = false;
|
||||
|
||||
if (gScanStateDir != SCAN_OFF) {
|
||||
@@ -527,8 +584,10 @@ void ACTION_FM(void)
|
||||
|
||||
gRequestDisplayScreen = DISPLAY_FM;
|
||||
}
|
||||
#endif /* !ENABLE_FEAT_F4HWN_OVERLAY_APPS */
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
static void ACTION_Scan_FM(bool bRestart)
|
||||
{
|
||||
if (FUNCTION_IsRx())
|
||||
@@ -568,40 +627,24 @@ static void ACTION_Scan_FM(bool bRestart)
|
||||
#endif
|
||||
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
#if defined(ENABLE_ALARM) || defined(ENABLE_TX1750)
|
||||
static void ACTION_AlarmOr1750(const bool b1750)
|
||||
#ifdef ENABLE_TX1750
|
||||
static void ACTION_1750(void)
|
||||
{
|
||||
|
||||
if(gEeprom.KEY_LOCK && (gSetting_set_lck & SET_LCK_PTT))
|
||||
return;
|
||||
|
||||
#if defined(ENABLE_ALARM)
|
||||
const AlarmState_t alarm_mode = (gEeprom.ALARM_MODE == ALARM_MODE_TONE) ? ALARM_STATE_TXALARM : ALARM_STATE_SITE_ALARM;
|
||||
gAlarmRunningCounter = 0;
|
||||
#endif
|
||||
|
||||
#if defined(ENABLE_ALARM) && defined(ENABLE_TX1750)
|
||||
gAlarmState = b1750 ? ALARM_STATE_TX1750 : alarm_mode;
|
||||
#elif defined(ENABLE_ALARM)
|
||||
gAlarmState = alarm_mode;
|
||||
#else
|
||||
gAlarmState = ALARM_STATE_TX1750;
|
||||
#endif
|
||||
|
||||
(void)b1750;
|
||||
gTx1750Active = true;
|
||||
gInputBoxIndex = 0;
|
||||
|
||||
gFlagPrepareTX = gAlarmState != ALARM_STATE_OFF;
|
||||
gFlagPrepareTX = true;
|
||||
|
||||
if (gScreenToDisplay != DISPLAY_MENU) // 1of11 .. don't close the menu
|
||||
gRequestDisplayScreen = DISPLAY_MAIN;
|
||||
}
|
||||
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_VOX
|
||||
@@ -618,18 +661,6 @@ void ACTION_Vox(void)
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_BLMIN_TMP_OFF
|
||||
void ACTION_BlminTmpOff(void)
|
||||
{
|
||||
if(++gEeprom.BACKLIGHT_MIN_STAT == BLMIN_STAT_UNKNOWN) {
|
||||
gEeprom.BACKLIGHT_MIN_STAT = BLMIN_STAT_ON;
|
||||
BACKLIGHT_SetBrightness(gEeprom.BACKLIGHT_MIN);
|
||||
} else {
|
||||
BACKLIGHT_SetBrightness(0);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN
|
||||
void ACTION_Update(void)
|
||||
{
|
||||
@@ -714,11 +745,7 @@ void ACTION_Wn(void)
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_AM_FIX
|
||||
BK4819_SetFilterBandwidth(bw, true);
|
||||
#else
|
||||
BK4819_SetFilterBandwidth(bw, false);
|
||||
#endif
|
||||
BK4819_SetFilterBandwidth(bw, false);
|
||||
}
|
||||
|
||||
void ACTION_BackLight(void)
|
||||
@@ -760,7 +787,7 @@ void ACTION_Mute(void)
|
||||
gMute = !gMute;
|
||||
|
||||
// Update the registers
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
BK1080_WriteRegister(BK1080_REG_05_SYSTEM_CONFIGURATION2, gMute ? 0x0A10 : 0x0A1F);
|
||||
#endif
|
||||
gEeprom.VOLUME_GAIN = gMute ? 0 : gEeprom.VOLUME_GAIN_BACKUP;
|
||||
|
||||
+4
-4
@@ -17,6 +17,9 @@
|
||||
#ifndef APP_ACTION_H
|
||||
#define APP_ACTION_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "driver/keyboard.h"
|
||||
|
||||
void ACTION_Power(void);
|
||||
@@ -31,10 +34,6 @@ void ACTION_Scan(bool bRestart);
|
||||
#endif
|
||||
void ACTION_SwitchDemodul(void);
|
||||
|
||||
#ifdef ENABLE_BLMIN_TMP_OFF
|
||||
void ACTION_BlminTmpOff(void);
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN
|
||||
void ACTION_RxMode(void);
|
||||
void ACTION_MainOnly(void);
|
||||
@@ -62,6 +61,7 @@ extern uint8_t gActionPickerSelection[2];
|
||||
extern uint8_t gActionPickerTimeout_500ms;
|
||||
bool ACTION_PickerProcessKey(KEY_Code_t key, bool isPressed, bool isHeld);
|
||||
#endif
|
||||
bool ACTION_IsAvailable(uint8_t action);
|
||||
void ACTION_Handle(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld);
|
||||
|
||||
#endif
|
||||
+254
-201
@@ -21,6 +21,7 @@
|
||||
#include "driver/bk4819.h"
|
||||
#include "driver/crc.h"
|
||||
#include "driver/eeprom.h"
|
||||
#include "driver/system.h"
|
||||
#include "frequencies.h"
|
||||
#include "misc.h"
|
||||
#include "radio.h"
|
||||
@@ -29,6 +30,7 @@
|
||||
#include "ui/ui.h"
|
||||
#include "settings.h"
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
#include "k5viewer.h"
|
||||
@@ -40,141 +42,104 @@ AIRCOPY_State_t gAircopyState;
|
||||
uint16_t gAirCopyBlockNumber;
|
||||
uint16_t gErrorsDuringAirCopy;
|
||||
bool gAirCopyIsSendMode;
|
||||
bool gAircopyAll;
|
||||
|
||||
uint16_t g_FSK_Buffer[36];
|
||||
|
||||
// ============================================================================
|
||||
// Transfer Maps Definition
|
||||
// ============================================================================
|
||||
// Stop-and-wait protocol. Every frame keeps the original 64-byte payload:
|
||||
// DATA is acknowledged only after storage, ACK confirms that offset, and
|
||||
// RESEND requests the same offset immediately instead of waiting for timeout.
|
||||
#define AIRCOPY_PACKET_DATA 0xABCDu
|
||||
#define AIRCOPY_PACKET_ACK 0xABCEu
|
||||
#define AIRCOPY_PACKET_RESEND 0xABCFu
|
||||
#define AIRCOPY_PACKET_END 0xDCBAu
|
||||
#define AIRCOPY_ACK_TIMEOUT_10MS 400u
|
||||
#define AIRCOPY_RX_TIMEOUT_10MS 2000u
|
||||
#define AIRCOPY_RX_LINGER_10MS 500u
|
||||
#define AIRCOPY_MAX_RETRIES 3u
|
||||
|
||||
#define AIRCOPY_BANK_SEGMENTS(bank) \
|
||||
{ \
|
||||
{ 0x0000 + (bank)*0x0800, 0x0000 + (bank)*0x0800 + 0x0800, AIRCOPY_WRITE_STRUCT }, \
|
||||
{ 0x4000 + (bank)*0x0800, 0x4000 + (bank)*0x0800 + 0x0800, AIRCOPY_WRITE_STRUCT }, \
|
||||
{ 0x8000 + (bank)*0x0100, 0x8000 + (bank)*0x0100 + 0x0100, AIRCOPY_WRITE_BYTES }, \
|
||||
}
|
||||
static uint16_t AircopyCountdown;
|
||||
static uint8_t AircopyRetries;
|
||||
|
||||
#define AIRCOPY_STD_MAP(seg_array) \
|
||||
{ \
|
||||
.segments = seg_array, \
|
||||
.num_segments = 3, \
|
||||
.total_blocks = 68 \
|
||||
}
|
||||
|
||||
// total_blocks = 68 (blocs of 64 bytes) because (16 bytes + 16 bytes + 2 bytes) * 128 = 4352 / 64 = 68
|
||||
|
||||
#define DECLARE_AIRCOPY_BANK(n) \
|
||||
static const AIRCOPY_Segment_t AIRCOPY_Segments_Bank##n[] = \
|
||||
AIRCOPY_BANK_SEGMENTS(n); \
|
||||
\
|
||||
static const AIRCOPY_TransferMap_t AIRCOPY_Map_Bank##n = \
|
||||
AIRCOPY_STD_MAP(AIRCOPY_Segments_Bank##n);
|
||||
|
||||
DECLARE_AIRCOPY_BANK(0)
|
||||
DECLARE_AIRCOPY_BANK(1)
|
||||
#if AIRCOPY_NUM_BANKS >= 4 // if 512 MR CHANNEL
|
||||
DECLARE_AIRCOPY_BANK(2)
|
||||
DECLARE_AIRCOPY_BANK(3)
|
||||
#endif
|
||||
#if AIRCOPY_NUM_BANKS >= 6 // if 758 MR CHANNEL
|
||||
DECLARE_AIRCOPY_BANK(4)
|
||||
DECLARE_AIRCOPY_BANK(5)
|
||||
#endif
|
||||
#if AIRCOPY_NUM_BANKS >= 8 // if 1024 MR CHANNEL
|
||||
DECLARE_AIRCOPY_BANK(6)
|
||||
DECLARE_AIRCOPY_BANK(7)
|
||||
#endif
|
||||
|
||||
// For settings only
|
||||
|
||||
static const AIRCOPY_Segment_t AIRCOPY_Segments_Settings[] = {
|
||||
{ 0xA000, 0xA170, AIRCOPY_WRITE_BYTES },
|
||||
{ 0x880E, 0x886E, AIRCOPY_WRITE_BYTES },
|
||||
{ 0x9000, 0x90E5, AIRCOPY_WRITE_BYTES }, // VFO area (full 14 VFOs 0x9000..0x90E0) +
|
||||
// Fox Hunt settings tail 0x90E0..0x90E5
|
||||
};
|
||||
|
||||
// total_blocks = ceil(0x170/64) + ceil(0x60/64) + ceil(0xE5/64) = 6 + 2 + 4 = 12
|
||||
static const AIRCOPY_TransferMap_t AIRCOPY_Map_Settings = {
|
||||
.segments = AIRCOPY_Segments_Settings,
|
||||
.num_segments = 3,
|
||||
.total_blocks = 12
|
||||
};
|
||||
|
||||
// Finally
|
||||
|
||||
static const AIRCOPY_TransferMap_t *AIRCOPY_AvailableMaps[] = {
|
||||
&AIRCOPY_Map_Bank0,
|
||||
&AIRCOPY_Map_Bank1,
|
||||
#if AIRCOPY_NUM_BANKS >= 4 // if 512 MR CHANNEL
|
||||
&AIRCOPY_Map_Bank2,
|
||||
&AIRCOPY_Map_Bank3,
|
||||
#endif
|
||||
#if AIRCOPY_NUM_BANKS >= 6 // if 758 MR CHANNEL
|
||||
&AIRCOPY_Map_Bank4,
|
||||
&AIRCOPY_Map_Bank5,
|
||||
#endif
|
||||
#if AIRCOPY_NUM_BANKS >= 8 // if 1024 MR CHANNEL
|
||||
&AIRCOPY_Map_Bank6,
|
||||
&AIRCOPY_Map_Bank7,
|
||||
#endif
|
||||
&AIRCOPY_Map_Settings,
|
||||
};
|
||||
|
||||
#define AIRCOPY_NUM_MAPS (sizeof(AIRCOPY_AvailableMaps) / sizeof(AIRCOPY_AvailableMaps[0]))
|
||||
#define AIRCOPY_BANK_BLOCKS 68u
|
||||
#define AIRCOPY_SETTINGS_BLOCKS 12u
|
||||
#define AIRCOPY_ALL_BLOCKS (AIRCOPY_NUM_BANKS * AIRCOPY_BANK_BLOCKS + AIRCOPY_SETTINGS_BLOCKS)
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
const AIRCOPY_TransferMap_t* AIRCOPY_GetCurrentMap(void)
|
||||
uint16_t AIRCOPY_GetTotalBlocks(void)
|
||||
{
|
||||
if (gAircopyCurrentMapIndex >= AIRCOPY_NUM_MAPS) {
|
||||
gAircopyCurrentMapIndex = 0;
|
||||
if (gAircopyAll)
|
||||
return AIRCOPY_ALL_BLOCKS; // banks + settings, one continuous run
|
||||
return gAircopyCurrentMapIndex == AIRCOPY_NUM_BANKS
|
||||
? AIRCOPY_SETTINGS_BLOCKS
|
||||
: AIRCOPY_BANK_BLOCKS;
|
||||
}
|
||||
|
||||
// Resolve the map that a (possibly global, in All mode) block index lands in.
|
||||
// On return, *block is rewritten to the block index within that map.
|
||||
static uint8_t AIRCOPY_ResolveMap(uint16_t *block)
|
||||
{
|
||||
if (!gAircopyAll)
|
||||
return gAircopyCurrentMapIndex;
|
||||
|
||||
uint8_t map = 0;
|
||||
while (map < AIRCOPY_NUM_BANKS && *block >= AIRCOPY_BANK_BLOCKS)
|
||||
{
|
||||
*block -= AIRCOPY_BANK_BLOCKS;
|
||||
map++;
|
||||
}
|
||||
return AIRCOPY_AvailableMaps[gAircopyCurrentMapIndex];
|
||||
return map; // AIRCOPY_NUM_BANKS once the banks are exhausted (settings map)
|
||||
}
|
||||
|
||||
// Map index of the block currently in progress, for the All-mode slice label.
|
||||
uint8_t AIRCOPY_CurrentSliceMap(void)
|
||||
{
|
||||
uint16_t block = gAirCopyBlockNumber;
|
||||
return AIRCOPY_ResolveMap(&block);
|
||||
}
|
||||
|
||||
static uint16_t AIRCOPY_GetBlockOffset(uint16_t block)
|
||||
{
|
||||
const uint8_t map = AIRCOPY_ResolveMap(&block);
|
||||
|
||||
if (map == AIRCOPY_NUM_BANKS)
|
||||
{
|
||||
// Settings: 6 blocks at 0xA000, 2 at 0x880E and 4 at 0x9000.
|
||||
if (block < 6u)
|
||||
return 0xA000u + block * AIRCOPY_BLOCK_SIZE;
|
||||
if (block < 8u)
|
||||
return 0x880Eu + (block - 6u) * AIRCOPY_BLOCK_SIZE;
|
||||
return 0x9000u + (block - 8u) * AIRCOPY_BLOCK_SIZE;
|
||||
}
|
||||
|
||||
// A bank contains 32 frequency, 32 name and 4 attribute blocks.
|
||||
const uint16_t channelOffset = map * 0x0800u;
|
||||
if (block < 32u)
|
||||
return channelOffset + block * AIRCOPY_BLOCK_SIZE;
|
||||
if (block < 64u)
|
||||
return 0x4000u + channelOffset + (block - 32u) * AIRCOPY_BLOCK_SIZE;
|
||||
return 0x8000u + map * 0x0100u
|
||||
+ (block - 64u) * AIRCOPY_BLOCK_SIZE;
|
||||
}
|
||||
|
||||
static void AIRCOPY_clear()
|
||||
{
|
||||
for (uint8_t i = 0; i < 15; i++)
|
||||
{
|
||||
crc[i] = 0;
|
||||
}
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
K5VIEWER_Update(true);
|
||||
#endif
|
||||
}
|
||||
|
||||
static inline const AIRCOPY_Segment_t *AIRCOPY_FindSegmentForOffset(uint16_t off)
|
||||
static void AIRCOPY_Finish(AIRCOPY_State_t state)
|
||||
{
|
||||
const AIRCOPY_TransferMap_t *map = AIRCOPY_GetCurrentMap();
|
||||
|
||||
for (uint16_t i = 0; i < map->num_segments; i++)
|
||||
{
|
||||
const AIRCOPY_Segment_t *seg = &map->segments[i];
|
||||
|
||||
if (off >= seg->start_offset && off < seg->end_offset)
|
||||
return seg;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static inline void AIRCOPY_CheckComplete(uint16_t *num)
|
||||
{
|
||||
*num = *num + 1;
|
||||
|
||||
const AIRCOPY_TransferMap_t *map = AIRCOPY_GetCurrentMap();
|
||||
uint16_t done = gAirCopyBlockNumber + gErrorsDuringAirCopy;
|
||||
|
||||
if (done >= map->total_blocks)
|
||||
{
|
||||
gAircopyState = AIRCOPY_COMPLETE;
|
||||
AircopyCountdown = 0;
|
||||
gAircopyState = state;
|
||||
gUpdateDisplay = true;
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
K5VIEWER_Update(false);
|
||||
K5VIEWER_Update(false);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void AIRCOPY_Obfuscate(unsigned int count)
|
||||
@@ -184,70 +149,97 @@ void AIRCOPY_Obfuscate(unsigned int count)
|
||||
}
|
||||
}
|
||||
|
||||
static void AIRCOPY_TransmitBuffer(void)
|
||||
{
|
||||
// Both sides need time to leave TX and re-arm FSK RX before the reply.
|
||||
SYSTEM_DelayMs(50);
|
||||
RADIO_SetTxParameters();
|
||||
BK4819_SendFSKData(g_FSK_Buffer);
|
||||
BK4819_SetupPowerAmplifier(0, 0);
|
||||
BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, false);
|
||||
}
|
||||
|
||||
static void AIRCOPY_FinalizeAndSend(void)
|
||||
{
|
||||
g_FSK_Buffer[34] = CRC_Calculate(&g_FSK_Buffer[0],
|
||||
4 + AIRCOPY_BLOCK_SIZE);
|
||||
g_FSK_Buffer[35] = AIRCOPY_PACKET_END;
|
||||
AIRCOPY_Obfuscate(34);
|
||||
AIRCOPY_TransmitBuffer();
|
||||
gFSKWriteIndex = 0;
|
||||
BK4819_PrepareFSKReceive();
|
||||
}
|
||||
|
||||
static void AIRCOPY_SendControl(uint16_t type, uint16_t offset)
|
||||
{
|
||||
g_FSK_Buffer[0] = type;
|
||||
g_FSK_Buffer[1] = offset;
|
||||
memset(&g_FSK_Buffer[2], 0, AIRCOPY_BLOCK_SIZE);
|
||||
AIRCOPY_FinalizeAndSend();
|
||||
}
|
||||
|
||||
static void AIRCOPY_RequestResend(void)
|
||||
{
|
||||
gErrorsDuringAirCopy++;
|
||||
gUpdateDisplay = true;
|
||||
AircopyCountdown = AIRCOPY_RX_TIMEOUT_10MS;
|
||||
AIRCOPY_SendControl(AIRCOPY_PACKET_RESEND,
|
||||
AIRCOPY_GetBlockOffset(gAirCopyBlockNumber));
|
||||
}
|
||||
|
||||
static bool AIRCOPY_Retry(void)
|
||||
{
|
||||
if (AircopyRetries >= AIRCOPY_MAX_RETRIES)
|
||||
{
|
||||
AIRCOPY_Finish(AIRCOPY_FAILED);
|
||||
return false;
|
||||
}
|
||||
|
||||
AircopyRetries++;
|
||||
gErrorsDuringAirCopy++;
|
||||
gUpdateDisplay = true;
|
||||
AircopyCountdown = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Send/Receive Functions
|
||||
// ============================================================================
|
||||
|
||||
bool AIRCOPY_SendMessage(void)
|
||||
{
|
||||
static uint8_t gAircopySendCountdown = 1;
|
||||
static uint16_t CurrentOffset = 0;
|
||||
static uint16_t CurrentSegmentIndex = 0;
|
||||
|
||||
if (gAircopyState != AIRCOPY_TRANSFER) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (--gAircopySendCountdown) {
|
||||
if (!gAirCopyIsSendMode)
|
||||
{
|
||||
if (AircopyCountdown != 0 && --AircopyCountdown == 0)
|
||||
{
|
||||
AIRCOPY_Finish(gAirCopyBlockNumber >= AIRCOPY_GetTotalBlocks()
|
||||
? AIRCOPY_COMPLETE
|
||||
: AIRCOPY_FAILED);
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
const AIRCOPY_TransferMap_t *map = AIRCOPY_GetCurrentMap();
|
||||
|
||||
// Initialize on first call
|
||||
if (gAirCopyBlockNumber == 0) {
|
||||
CurrentSegmentIndex = 0;
|
||||
CurrentOffset = map->segments[0].start_offset;
|
||||
}
|
||||
|
||||
// Advance to next segment if current is done
|
||||
while (CurrentSegmentIndex < map->num_segments &&
|
||||
CurrentOffset >= map->segments[CurrentSegmentIndex].end_offset)
|
||||
if (AircopyCountdown != 0)
|
||||
{
|
||||
CurrentSegmentIndex++;
|
||||
if (CurrentSegmentIndex < map->num_segments) {
|
||||
CurrentOffset = map->segments[CurrentSegmentIndex].start_offset;
|
||||
}
|
||||
if (--AircopyCountdown != 0)
|
||||
return 1;
|
||||
if (!AIRCOPY_Retry())
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check if transfer is complete
|
||||
if (CurrentSegmentIndex >= map->num_segments) {
|
||||
gAircopyState = AIRCOPY_COMPLETE;
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
K5VIEWER_Update(false);
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
const uint16_t currentOffset = AIRCOPY_GetBlockOffset(gAirCopyBlockNumber);
|
||||
g_FSK_Buffer[0] = AIRCOPY_PACKET_DATA;
|
||||
g_FSK_Buffer[1] = currentOffset;
|
||||
EEPROM_ReadBuffer(currentOffset, &g_FSK_Buffer[2], AIRCOPY_BLOCK_SIZE);
|
||||
AIRCOPY_FinalizeAndSend();
|
||||
AircopyCountdown = AIRCOPY_ACK_TIMEOUT_10MS;
|
||||
|
||||
// Send data from current offset
|
||||
g_FSK_Buffer[1] = CurrentOffset;
|
||||
EEPROM_ReadBuffer(CurrentOffset, &g_FSK_Buffer[2], 64);
|
||||
|
||||
g_FSK_Buffer[34] = CRC_Calculate(&g_FSK_Buffer[1], 2 + 64);
|
||||
|
||||
AIRCOPY_Obfuscate(34);
|
||||
|
||||
RADIO_SetTxParameters();
|
||||
|
||||
BK4819_SendFSKData(g_FSK_Buffer);
|
||||
BK4819_SetupPowerAmplifier(0, 0);
|
||||
BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, false);
|
||||
|
||||
CurrentOffset += 64;
|
||||
gAirCopyBlockNumber++;
|
||||
gAircopySendCountdown = 30;
|
||||
|
||||
return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
void AIRCOPY_StorePacket(void)
|
||||
@@ -257,53 +249,112 @@ void AIRCOPY_StorePacket(void)
|
||||
}
|
||||
|
||||
gFSKWriteIndex = 0;
|
||||
gUpdateDisplay = true;
|
||||
uint16_t Status = BK4819_ReadRegister(BK4819_REG_0B);
|
||||
BK4819_PrepareFSKReceive();
|
||||
const uint16_t status = BK4819_ReadRegister(BK4819_REG_0B);
|
||||
const uint16_t type = g_FSK_Buffer[0];
|
||||
bool valid = (status & 0x0010u) == 0 &&
|
||||
(type == AIRCOPY_PACKET_DATA ||
|
||||
type == AIRCOPY_PACKET_ACK ||
|
||||
type == AIRCOPY_PACKET_RESEND) &&
|
||||
g_FSK_Buffer[35] == AIRCOPY_PACKET_END;
|
||||
|
||||
if ((Status & 0x0010U) != 0 || g_FSK_Buffer[0] != 0xABCD || g_FSK_Buffer[35] != 0xDCBA) {
|
||||
BK4819_ResetFSK(); // <- important
|
||||
BK4819_PrepareFSKReceive(); // <- re-arm proprement
|
||||
|
||||
AIRCOPY_CheckComplete(&gErrorsDuringAirCopy);
|
||||
return;
|
||||
}
|
||||
|
||||
AIRCOPY_Obfuscate(34);
|
||||
|
||||
uint16_t Crc = CRC_Calculate(&g_FSK_Buffer[1], 2 + 64);
|
||||
if (g_FSK_Buffer[34] != Crc) {
|
||||
AIRCOPY_CheckComplete(&gErrorsDuringAirCopy);
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t Offset = g_FSK_Buffer[1];
|
||||
|
||||
const AIRCOPY_Segment_t *seg = AIRCOPY_FindSegmentForOffset(Offset);
|
||||
|
||||
if (seg == NULL) {
|
||||
AIRCOPY_CheckComplete(&gErrorsDuringAirCopy);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t *pData = (const uint8_t *)&g_FSK_Buffer[2];
|
||||
|
||||
for (unsigned int i = 0; i < 8; i++)
|
||||
if (valid)
|
||||
{
|
||||
EEPROM_WriteBuffer(Offset + (i * 8), pData + (i * 8));
|
||||
AIRCOPY_Obfuscate(34);
|
||||
valid = g_FSK_Buffer[34] ==
|
||||
CRC_Calculate(&g_FSK_Buffer[0], 4 + AIRCOPY_BLOCK_SIZE);
|
||||
}
|
||||
|
||||
AIRCOPY_CheckComplete(&gAirCopyBlockNumber);
|
||||
if (gAirCopyIsSendMode)
|
||||
{
|
||||
if (!valid)
|
||||
{
|
||||
BK4819_PrepareFSKReceive();
|
||||
return;
|
||||
}
|
||||
|
||||
const uint16_t offset = g_FSK_Buffer[1];
|
||||
const uint16_t currentOffset = AIRCOPY_GetBlockOffset(gAirCopyBlockNumber);
|
||||
if (type == AIRCOPY_PACKET_ACK && offset == currentOffset)
|
||||
{
|
||||
AircopyCountdown = 0;
|
||||
AircopyRetries = 0;
|
||||
gAirCopyBlockNumber++;
|
||||
gUpdateDisplay = true;
|
||||
if (gAirCopyBlockNumber >= AIRCOPY_GetTotalBlocks())
|
||||
AIRCOPY_Finish(AIRCOPY_COMPLETE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == AIRCOPY_PACKET_RESEND && offset == currentOffset)
|
||||
{
|
||||
(void)AIRCOPY_Retry();
|
||||
return;
|
||||
}
|
||||
|
||||
BK4819_PrepareFSKReceive();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!valid)
|
||||
{
|
||||
if (type == AIRCOPY_PACKET_DATA)
|
||||
AIRCOPY_RequestResend();
|
||||
else
|
||||
BK4819_PrepareFSKReceive();
|
||||
return;
|
||||
}
|
||||
|
||||
if (type != AIRCOPY_PACKET_DATA)
|
||||
{
|
||||
BK4819_PrepareFSKReceive();
|
||||
return;
|
||||
}
|
||||
|
||||
const uint16_t offset = g_FSK_Buffer[1];
|
||||
if (gAirCopyBlockNumber != 0u &&
|
||||
offset == AIRCOPY_GetBlockOffset(gAirCopyBlockNumber - 1u))
|
||||
{
|
||||
AircopyCountdown = gAirCopyBlockNumber >= AIRCOPY_GetTotalBlocks()
|
||||
? AIRCOPY_RX_LINGER_10MS
|
||||
: AIRCOPY_RX_TIMEOUT_10MS;
|
||||
AIRCOPY_SendControl(AIRCOPY_PACKET_ACK, offset);
|
||||
return;
|
||||
}
|
||||
|
||||
if (offset != AIRCOPY_GetBlockOffset(gAirCopyBlockNumber))
|
||||
{
|
||||
AIRCOPY_RequestResend();
|
||||
return;
|
||||
}
|
||||
|
||||
EEPROM_WriteBuffer(offset, &g_FSK_Buffer[2], AIRCOPY_BLOCK_SIZE);
|
||||
// All pending RX errors concerned this stop-and-wait block.
|
||||
gErrorsDuringAirCopy = 0;
|
||||
gAirCopyBlockNumber++;
|
||||
gUpdateDisplay = true;
|
||||
|
||||
AircopyCountdown = gAirCopyBlockNumber < AIRCOPY_GetTotalBlocks()
|
||||
? AIRCOPY_RX_TIMEOUT_10MS
|
||||
: AIRCOPY_RX_LINGER_10MS;
|
||||
|
||||
AIRCOPY_SendControl(AIRCOPY_PACKET_ACK, offset);
|
||||
}
|
||||
|
||||
static void AIRCOPY_InitTransfer(bool isSendMode)
|
||||
{
|
||||
gAircopyStep = 1;
|
||||
if (gAircopyCurrentMapIndex > AIRCOPY_ALL_INDEX)
|
||||
gAircopyCurrentMapIndex = 0;
|
||||
gAircopyAll = (gAircopyCurrentMapIndex == AIRCOPY_ALL_INDEX);
|
||||
|
||||
gFSKWriteIndex = 0;
|
||||
gAirCopyBlockNumber = 0;
|
||||
gErrorsDuringAirCopy = 0;
|
||||
gInputBoxIndex = 0;
|
||||
gAirCopyIsSendMode = isSendMode;
|
||||
|
||||
AircopyCountdown = isSendMode ? 0 : AIRCOPY_RX_TIMEOUT_10MS;
|
||||
AircopyRetries = 0;
|
||||
|
||||
AIRCOPY_clear();
|
||||
|
||||
gAircopyState = AIRCOPY_TRANSFER;
|
||||
@@ -357,8 +408,6 @@ static void AIRCOPY_Key_EXIT()
|
||||
{
|
||||
if (gInputBoxIndex == 0) {
|
||||
AIRCOPY_InitTransfer(0); // Mode: Receive
|
||||
gErrorsDuringAirCopy = lErrorsDuringAirCopy = 0;
|
||||
|
||||
BK4819_PrepareFSKReceive();
|
||||
|
||||
} else {
|
||||
@@ -369,10 +418,6 @@ static void AIRCOPY_Key_EXIT()
|
||||
static void AIRCOPY_Key_MENU()
|
||||
{
|
||||
AIRCOPY_InitTransfer(1); // Mode: Send
|
||||
|
||||
g_FSK_Buffer[0] = 0xABCD;
|
||||
g_FSK_Buffer[1] = 0;
|
||||
g_FSK_Buffer[35] = 0xDCBA;
|
||||
}
|
||||
|
||||
static void AIRCOPY_Key_UP_DOWN(int8_t Direction)
|
||||
@@ -384,10 +429,10 @@ static void AIRCOPY_Key_UP_DOWN(int8_t Direction)
|
||||
switch(Direction)
|
||||
{
|
||||
case 1:
|
||||
gAircopyCurrentMapIndex = (gAircopyCurrentMapIndex + 1) % AIRCOPY_NUM_MAPS;
|
||||
gAircopyCurrentMapIndex = (gAircopyCurrentMapIndex + 1) % (AIRCOPY_NUM_MAPS + 1u);
|
||||
break;
|
||||
case -1:
|
||||
gAircopyCurrentMapIndex = (gAircopyCurrentMapIndex + AIRCOPY_NUM_MAPS - 1) % AIRCOPY_NUM_MAPS;
|
||||
gAircopyCurrentMapIndex = (gAircopyCurrentMapIndex + AIRCOPY_NUM_MAPS) % (AIRCOPY_NUM_MAPS + 1u);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -398,6 +443,14 @@ void AIRCOPY_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
|
||||
return;
|
||||
}
|
||||
|
||||
if (gAircopyState == AIRCOPY_COMPLETE || gAircopyState == AIRCOPY_FAILED)
|
||||
{
|
||||
gAircopyState = AIRCOPY_READY;
|
||||
gUpdateDisplay = true;
|
||||
gRequestDisplayScreen = DISPLAY_AIRCOPY;
|
||||
return;
|
||||
}
|
||||
|
||||
if (Key != KEY_PTT) {
|
||||
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
|
||||
}
|
||||
@@ -426,4 +479,4 @@ void AIRCOPY_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
|
||||
gRequestDisplayScreen = DISPLAY_AIRCOPY;
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
+7
-48
@@ -28,53 +28,10 @@
|
||||
#define AIRCOPY_BLOCK_SIZE 0x0040u // 64 bytes per AirCopy block
|
||||
#define AIRCOPY_CHANNELS_PER_BANK 128
|
||||
#define AIRCOPY_NUM_BANKS MR_CHANNELS_MAX / AIRCOPY_CHANNELS_PER_BANK
|
||||
#define AIRCOPY_CHANNEL_SIZE 16 // bytes per channel (freq/name)
|
||||
#define AIRCOPY_BANK_SIZE_BYTES 0x1080u // 0x800 (Freq) + 0x800 (Name) + 0x80 (Attr)
|
||||
#define AIRCOPY_NUM_MAPS (AIRCOPY_NUM_BANKS + 1u) // banks + one settings map
|
||||
#define AIRCOPY_ALL_INDEX AIRCOPY_NUM_MAPS // selection sentinel: send/receive everything
|
||||
#define AIRCOPY_BAR_WIDTH 120 // Visible width of the progress gauge
|
||||
|
||||
// ============================================================================
|
||||
// Segment write mode
|
||||
// ============================================================================
|
||||
|
||||
/*
|
||||
* Defines how a segment must be written to EEPROM.
|
||||
*
|
||||
* - STRUCT: structured data (frequencies, names)
|
||||
* - BYTES : raw byte stream (attributes, settings, etc.)
|
||||
*/
|
||||
typedef enum {
|
||||
AIRCOPY_WRITE_STRUCT = 0,
|
||||
AIRCOPY_WRITE_BYTES = 1,
|
||||
} AIRCOPY_WriteMode_t;
|
||||
|
||||
// ============================================================================
|
||||
// Transfer segment structure
|
||||
// ============================================================================
|
||||
|
||||
/*
|
||||
* Describes a contiguous EEPROM region involved in AirCopy.
|
||||
* The write_mode defines how the RX side must write the data.
|
||||
*/
|
||||
typedef struct {
|
||||
uint16_t start_offset;
|
||||
uint16_t end_offset;
|
||||
AIRCOPY_WriteMode_t write_mode;
|
||||
} AIRCOPY_Segment_t;
|
||||
|
||||
// ============================================================================
|
||||
// Transfer map structure
|
||||
// ============================================================================
|
||||
|
||||
/*
|
||||
* A transfer map is a collection of segments describing
|
||||
* one complete AirCopy operation (bank, settings, etc.).
|
||||
*/
|
||||
typedef struct {
|
||||
const AIRCOPY_Segment_t *segments;
|
||||
uint16_t num_segments;
|
||||
uint16_t total_blocks;
|
||||
} AIRCOPY_TransferMap_t;
|
||||
|
||||
// ============================================================================
|
||||
// AirCopy state
|
||||
// ============================================================================
|
||||
@@ -82,7 +39,8 @@ typedef struct {
|
||||
typedef enum {
|
||||
AIRCOPY_READY = 0,
|
||||
AIRCOPY_TRANSFER,
|
||||
AIRCOPY_COMPLETE
|
||||
AIRCOPY_COMPLETE,
|
||||
AIRCOPY_FAILED
|
||||
} AIRCOPY_State_t;
|
||||
|
||||
// ============================================================================
|
||||
@@ -93,6 +51,7 @@ extern AIRCOPY_State_t gAircopyState;
|
||||
extern uint16_t gAirCopyBlockNumber;
|
||||
extern uint16_t gErrorsDuringAirCopy;
|
||||
extern bool gAirCopyIsSendMode;
|
||||
extern bool gAircopyAll; // All mode: banks + settings in one pass
|
||||
|
||||
extern uint16_t g_FSK_Buffer[36];
|
||||
|
||||
@@ -103,8 +62,8 @@ extern uint16_t g_FSK_Buffer[36];
|
||||
bool AIRCOPY_SendMessage(void);
|
||||
void AIRCOPY_StorePacket(void);
|
||||
void AIRCOPY_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld);
|
||||
|
||||
const AIRCOPY_TransferMap_t* AIRCOPY_GetCurrentMap(void);
|
||||
uint16_t AIRCOPY_GetTotalBlocks(void);
|
||||
uint8_t AIRCOPY_CurrentSliceMap(void); // map index of the block in progress (All slice label)
|
||||
|
||||
// XOR-obfuscate `count` words of g_FSK_Buffer starting at index 1.
|
||||
// Self-inverse: applying twice restores the original buffer.
|
||||
|
||||
+153
-134
@@ -18,7 +18,6 @@
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "am_fix.h"
|
||||
#include "app/action.h"
|
||||
|
||||
#ifdef ENABLE_AIRCOPY
|
||||
@@ -33,7 +32,7 @@
|
||||
#ifdef ENABLE_FLASHLIGHT
|
||||
#include "app/flashlight.h"
|
||||
#endif
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
#include "app/fm.h"
|
||||
#endif
|
||||
#include "app/generic.h"
|
||||
@@ -54,7 +53,7 @@
|
||||
// #include "bsp/dp32g030/pwmplus.h"
|
||||
#endif
|
||||
#include "driver/backlight.h"
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
#include "driver/bk1080.h"
|
||||
#endif
|
||||
#include "driver/bk4819.h"
|
||||
@@ -112,8 +111,12 @@ void (*const ProcessKeysFunctions[])(KEY_Code_t Key, bool bKeyPressed, bool bKey
|
||||
[DISPLAY_MENU] = &MENU_ProcessKeys,
|
||||
[DISPLAY_SCANNER] = &SCANNER_ProcessKeys,
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
[DISPLAY_FM] = &FM_ProcessKeys,
|
||||
#elif defined(ENABLE_FMRADIO)
|
||||
/* The modal FM overlay handles its own keys. Keep the enum slot populated
|
||||
for configurations where DISPLAY_FM is the final display entry. */
|
||||
[DISPLAY_FM] = &MAIN_ProcessKeys,
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_AIRCOPY
|
||||
@@ -235,7 +238,7 @@ static void ScreenSaverUpdateViewer(void)
|
||||
#endif
|
||||
}
|
||||
|
||||
static bool ScreenSaverCanDisplay(void)
|
||||
static bool ScreenSaverCanDisplay(bool modal)
|
||||
{
|
||||
if (gSetting_set_sav == SET_SAV_OFF ||
|
||||
gEeprom.BACKLIGHT_TIME == 0 ||
|
||||
@@ -247,7 +250,7 @@ static bool ScreenSaverCanDisplay(void)
|
||||
gCurrentFunction == FUNCTION_TRANSMIT ||
|
||||
FUNCTION_IsRx() ||
|
||||
gPttIsPressed
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
|| (gFM_ScanState != FM_SCAN_OFF && !gFM_FoundFrequency)
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_BEAM
|
||||
@@ -258,10 +261,10 @@ static bool ScreenSaverCanDisplay(void)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (gScreenToDisplay == DISPLAY_MAIN)
|
||||
if (modal || gScreenToDisplay == DISPLAY_MAIN)
|
||||
return true;
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (gScreenToDisplay == DISPLAY_FM)
|
||||
return true;
|
||||
#endif
|
||||
@@ -269,9 +272,9 @@ static bool ScreenSaverCanDisplay(void)
|
||||
return false;
|
||||
}
|
||||
|
||||
static void ScreenSaverTryDisplay(void)
|
||||
static void ScreenSaverTryDisplay(bool modal)
|
||||
{
|
||||
if (!ScreenSaverCanDisplay())
|
||||
if (!ScreenSaverCanDisplay(modal))
|
||||
return;
|
||||
|
||||
if (gSetting_set_sav == SET_SAV_LOGO)
|
||||
@@ -296,6 +299,28 @@ static void ScreenSaverExit(void)
|
||||
gUpdateStatus = true;
|
||||
}
|
||||
}
|
||||
|
||||
static bool ScreenSaverAnimate(void)
|
||||
{
|
||||
if (!gScreenSaverDisplayed)
|
||||
return false;
|
||||
|
||||
if (gSetting_set_sav == SET_SAV_MATRIX) {
|
||||
if (++gScreenSaverTick >= 8u) {
|
||||
gScreenSaverTick = 0;
|
||||
ScreenSaverRenderMatrix(false);
|
||||
return true;
|
||||
}
|
||||
} else if (gSetting_set_sav == SET_SAV_LOGO_PLUS) {
|
||||
if (++gScreenSaverTick >= 16u) {
|
||||
gScreenSaverTick = 0;
|
||||
ScreenSaverRenderLogoPlus(false);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
bool APP_IsScreenSaverDisplayed(void)
|
||||
@@ -307,12 +332,53 @@ bool APP_IsScreenSaverDisplayed(void)
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Modal foreground loops (resident tools and overlay apps) bypass APP_Update()
|
||||
* and therefore also bypass the normal 10 ms fade and 500 ms BLTime service.
|
||||
* Keep that service resident so every overlay app gets identical timing without
|
||||
* extending the app ABI. Only selected modal screens opt into the saver; the
|
||||
* others still fade from BLMax to BLMin when BLTime expires. */
|
||||
void APP_ModalBacklightTick(bool allowScreenSaver)
|
||||
{
|
||||
if (gNextTimeslice) {
|
||||
gNextTimeslice = false;
|
||||
BACKLIGHT_Update();
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_LOGO_SAV
|
||||
if (ScreenSaverAnimate())
|
||||
ScreenSaverUpdateViewer();
|
||||
#endif
|
||||
}
|
||||
|
||||
if (!gNextTimeslice_500ms)
|
||||
return;
|
||||
gNextTimeslice_500ms = false;
|
||||
|
||||
if (gBacklightCountdown_500ms > 0 &&
|
||||
gEeprom.BACKLIGHT_TIME < 61 &&
|
||||
--gBacklightCountdown_500ms == 0)
|
||||
BACKLIGHT_TurnOff();
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_LOGO_SAV
|
||||
if (allowScreenSaver && gBacklightCountdown_500ms == 0)
|
||||
ScreenSaverTryDisplay(true);
|
||||
#else
|
||||
(void)allowScreenSaver;
|
||||
#endif
|
||||
}
|
||||
|
||||
void APP_ModalScreenSaverExit(void)
|
||||
{
|
||||
#ifdef ENABLE_FEAT_F4HWN_LOGO_SAV
|
||||
ScreenSaverExit();
|
||||
#endif
|
||||
}
|
||||
|
||||
static void CheckForIncoming(void)
|
||||
{
|
||||
if (!g_SquelchLost)
|
||||
return; // squelch is closed
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
// FM scan in progress: ignore main-channel RX so scanning is not interrupted.
|
||||
// Normal FM listening (FM_SCAN_OFF) still yields to channel signals as before.
|
||||
if (gFmRadioMode && gFM_ScanState != FM_SCAN_OFF)
|
||||
@@ -451,7 +517,7 @@ static void HandleIncoming(void)
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
// Defensive: do not leave FM scan for a main-channel signal.
|
||||
if (gFmRadioMode && gFM_ScanState != FM_SCAN_OFF)
|
||||
return;
|
||||
@@ -722,7 +788,7 @@ void APP_StartListening(FUNCTION_Type_t function)
|
||||
return;
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (gFmRadioMode)
|
||||
BK1080_Init0();
|
||||
#endif
|
||||
@@ -791,7 +857,7 @@ void APP_StartListening(FUNCTION_Type_t function)
|
||||
RXTX_LOG_BeginRx(gRxVfo, function);
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (function == FUNCTION_MONITOR || gFmRadioMode)
|
||||
#else
|
||||
if (function == FUNCTION_MONITOR)
|
||||
@@ -1008,36 +1074,44 @@ static void CheckRadioInterrupts(void)
|
||||
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, false);
|
||||
}
|
||||
|
||||
#ifdef ENABLE_AIRCOPY
|
||||
if (interrupts.fskFifoAlmostFull &&
|
||||
gScreenToDisplay == DISPLAY_AIRCOPY &&
|
||||
gAircopyState == AIRCOPY_TRANSFER &&
|
||||
gAirCopyIsSendMode == 0)
|
||||
#if defined(ENABLE_AIRCOPY) || defined(ENABLE_FEAT_F4HWN_BEAM)
|
||||
if (interrupts.fskFifoAlmostFull || interrupts.fskRxFinied)
|
||||
{
|
||||
for (unsigned int i = 0; i < 4; i++) {
|
||||
g_FSK_Buffer[gFSKWriteIndex++] = BK4819_ReadRegister(BK4819_REG_5F);
|
||||
}
|
||||
|
||||
AIRCOPY_StorePacket();
|
||||
}
|
||||
#endif
|
||||
uint8_t fskTarget = 0;
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_BEAM
|
||||
if ((interrupts.fskFifoAlmostFull || interrupts.fskRxFinied) &&
|
||||
gBeamActive &&
|
||||
gBeamMode == BEAM_MODE_RX &&
|
||||
(gBeamStatus == BEAM_STATUS_RX_WAIT || gBeamStatus == BEAM_STATUS_ERROR))
|
||||
{
|
||||
const unsigned int wordsToRead = interrupts.fskRxFinied ? (36 - gFSKWriteIndex) : 4;
|
||||
for (unsigned int i = 0; i < wordsToRead; i++) {
|
||||
const uint16_t word = BK4819_ReadRegister(BK4819_REG_5F);
|
||||
if (gFSKWriteIndex < 36)
|
||||
g_FSK_Buffer[gFSKWriteIndex++] = word;
|
||||
}
|
||||
if (gBeamActive &&
|
||||
gBeamMode == BEAM_MODE_RX &&
|
||||
(gBeamStatus == BEAM_STATUS_RX_WAIT || gBeamStatus == BEAM_STATUS_ERROR))
|
||||
fskTarget = 2;
|
||||
#endif
|
||||
#ifdef ENABLE_AIRCOPY
|
||||
// Aircopy wins if stale state ever makes both receivers eligible.
|
||||
if (gScreenToDisplay == DISPLAY_AIRCOPY &&
|
||||
gAircopyState == AIRCOPY_TRANSFER)
|
||||
fskTarget = 1;
|
||||
#endif
|
||||
|
||||
gBeamRxWordCount = gFSKWriteIndex;
|
||||
gUpdateDisplay = true;
|
||||
BEAM_StorePacket();
|
||||
if (fskTarget != 0)
|
||||
{
|
||||
const unsigned int wordsToRead = interrupts.fskRxFinied
|
||||
? (gFSKWriteIndex < 36 ? 36u - gFSKWriteIndex : 0u)
|
||||
: 4u;
|
||||
for (unsigned int i = 0; i < wordsToRead; i++) {
|
||||
const uint16_t word = BK4819_ReadRegister(BK4819_REG_5F);
|
||||
if (gFSKWriteIndex < 36)
|
||||
g_FSK_Buffer[gFSKWriteIndex++] = word;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_AIRCOPY
|
||||
if (fskTarget == 1)
|
||||
AIRCOPY_StorePacket();
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_BEAM
|
||||
if (fskTarget == 2)
|
||||
BEAM_StorePacket();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -1089,7 +1163,7 @@ static void HandleVox(void)
|
||||
gVoxPauseCountdown = 0;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (gFmRadioMode)
|
||||
return;
|
||||
#endif
|
||||
@@ -1251,7 +1325,7 @@ void APP_Update(void)
|
||||
if (gCurrentFunction != FUNCTION_TRANSMIT)
|
||||
HandleFunction();
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
// if (gFmRadioCountdown_500ms > 0)
|
||||
if (gFmRadioMode && gFmRadioCountdown_500ms > 0) // 1of11
|
||||
return;
|
||||
@@ -1296,7 +1370,7 @@ void APP_Update(void)
|
||||
#ifdef ENABLE_VOICE
|
||||
&& gVoiceWriteIndex == 0
|
||||
#endif
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
&& !gFmRadioMode
|
||||
#endif
|
||||
#ifdef ENABLE_DTMF_CALLING
|
||||
@@ -1315,7 +1389,7 @@ void APP_Update(void)
|
||||
gScheduleDualWatch = false;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (gScheduleFM && gFM_ScanState != FM_SCAN_OFF && !FUNCTION_IsRx()) {
|
||||
// switch to FM radio mode
|
||||
FM_Play();
|
||||
@@ -1335,7 +1409,7 @@ void APP_Update(void)
|
||||
|| gScanStateDir != SCAN_OFF
|
||||
|| gCssBackgroundScan
|
||||
|| gScreenToDisplay != DISPLAY_MAIN
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
|| gFmRadioMode
|
||||
#endif
|
||||
#ifdef ENABLE_DTMF_CALLING
|
||||
@@ -1447,7 +1521,7 @@ void CheckKeys(void)
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_AIRCOPY
|
||||
if (gScreenToDisplay == DISPLAY_AIRCOPY && gAircopyState != AIRCOPY_READY){
|
||||
if (gScreenToDisplay == DISPLAY_AIRCOPY && gAircopyState == AIRCOPY_TRANSFER){
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
@@ -1605,12 +1679,6 @@ void APP_TimeSlice10ms(void)
|
||||
|
||||
gFlashLightBlinkCounter++;
|
||||
|
||||
#ifdef ENABLE_AM_FIX
|
||||
if (gRxVfo->Modulation == MODULATION_AM) {
|
||||
AM_fix_10ms(gEeprom.RX_VFO);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_UART
|
||||
if (UART_IsCommandAvailable(UART_PORT_UART)) {
|
||||
// SCHEDULER_Disable();
|
||||
@@ -1622,6 +1690,8 @@ void APP_TimeSlice10ms(void)
|
||||
if (gReducedService)
|
||||
return;
|
||||
|
||||
UI_MAIN_TimeSlice10ms(); // scan-list name hold countdown (10 ms resolution)
|
||||
|
||||
if (gCurrentFunction != FUNCTION_POWER_SAVE || !gRxIdleMode)
|
||||
CheckRadioInterrupts();
|
||||
#ifdef ENABLE_FEAT_F4HWN_ACTION_PICKER
|
||||
@@ -1654,7 +1724,9 @@ void APP_TimeSlice10ms(void)
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_LOGO_SAV
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
bool screenSaverRendered = false;
|
||||
#endif
|
||||
|
||||
if (gScreenSaverDisplayed) {
|
||||
if (gUpdateDisplayCurrent) {
|
||||
@@ -1664,18 +1736,10 @@ void APP_TimeSlice10ms(void)
|
||||
gUpdateStatus = false;
|
||||
}
|
||||
|
||||
if (gSetting_set_sav == SET_SAV_MATRIX) {
|
||||
if (++gScreenSaverTick >= 8u) {
|
||||
gScreenSaverTick = 0;
|
||||
ScreenSaverRenderMatrix(false);
|
||||
screenSaverRendered = true;
|
||||
}
|
||||
} else if (gSetting_set_sav == SET_SAV_LOGO_PLUS) {
|
||||
if (++gScreenSaverTick >= 16u) {
|
||||
gScreenSaverTick = 0;
|
||||
ScreenSaverRenderLogoPlus(false);
|
||||
screenSaverRendered = true;
|
||||
}
|
||||
if (ScreenSaverAnimate()) {
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
screenSaverRendered = true;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1702,7 +1766,7 @@ void APP_TimeSlice10ms(void)
|
||||
|
||||
// Skipping authentic device checks
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (gFmRadioMode && gFmRadioCountdown_500ms > 0) // 1of11
|
||||
return;
|
||||
#endif
|
||||
@@ -1722,52 +1786,6 @@ void APP_TimeSlice10ms(void)
|
||||
#endif
|
||||
|
||||
if (gCurrentFunction == FUNCTION_TRANSMIT) {
|
||||
#ifdef ENABLE_ALARM
|
||||
if (gAlarmState == ALARM_STATE_TXALARM || gAlarmState == ALARM_STATE_SITE_ALARM) {
|
||||
uint16_t Tone;
|
||||
|
||||
gAlarmRunningCounter++;
|
||||
gAlarmToneCounter++;
|
||||
|
||||
Tone = 500 + (gAlarmToneCounter * 25);
|
||||
if (Tone > 1500) {
|
||||
Tone = 500;
|
||||
gAlarmToneCounter = 0;
|
||||
}
|
||||
|
||||
BK4819_SetScrambleFrequencyControlWord(Tone);
|
||||
|
||||
if (gEeprom.ALARM_MODE == ALARM_MODE_TONE && gAlarmRunningCounter == 512) {
|
||||
gAlarmRunningCounter = 0;
|
||||
|
||||
if (gAlarmState == ALARM_STATE_TXALARM) {
|
||||
gAlarmState = ALARM_STATE_SITE_ALARM;
|
||||
|
||||
RADIO_SendCssTail();
|
||||
BK4819_SetupPowerAmplifier(0, 0);
|
||||
BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, false);
|
||||
BK4819_Enable_AfDac_DiscMode_TxDsp();
|
||||
BK4819_ToggleGpioOut(BK4819_GPIO5_PIN1_RED, false);
|
||||
|
||||
GUI_DisplayScreen();
|
||||
}
|
||||
else {
|
||||
gAlarmState = ALARM_STATE_TXALARM;
|
||||
|
||||
GUI_DisplayScreen();
|
||||
|
||||
BK4819_ToggleGpioOut(BK4819_GPIO5_PIN1_RED, true);
|
||||
RADIO_SetTxParameters();
|
||||
BK4819_TransmitTone(true, 500);
|
||||
SYSTEM_DelayMs(2);
|
||||
AUDIO_AudioPathOn();
|
||||
|
||||
gEnableSpeaker = true;
|
||||
gAlarmToneCounter = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// repeater tail tone elimination
|
||||
if (gRTTECountdown_10ms > 0) {
|
||||
if (--gRTTECountdown_10ms == 0) {
|
||||
@@ -1780,7 +1798,7 @@ void APP_TimeSlice10ms(void)
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (gFmRadioMode && gFM_RestoreCountdown_10ms > 0) {
|
||||
if (--gFM_RestoreCountdown_10ms == 0) {
|
||||
FM_Start(); // switch back to FM radio mode
|
||||
@@ -1797,7 +1815,7 @@ void APP_TimeSlice10ms(void)
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_AIRCOPY
|
||||
if (gScreenToDisplay == DISPLAY_AIRCOPY && gAircopyState == AIRCOPY_TRANSFER && gAirCopyIsSendMode == 1) {
|
||||
if (gScreenToDisplay == DISPLAY_AIRCOPY && gAircopyState == AIRCOPY_TRANSFER) {
|
||||
if (!AIRCOPY_SendMessage()) {
|
||||
GUI_DisplayScreen();
|
||||
}
|
||||
@@ -1868,7 +1886,7 @@ void APP_TimeSlice500ms(void)
|
||||
{
|
||||
|
||||
if (IS_MR_CHANNEL(gTxVfo->CHANNEL_SAVE) && (gInputBoxIndex > 0 && gInputBoxIndex < 4)
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
&& (!gFmRadioMode)
|
||||
#endif
|
||||
)
|
||||
@@ -1915,7 +1933,7 @@ void APP_TimeSlice500ms(void)
|
||||
|
||||
// Skipped authentic device check
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (gFmRadioCountdown_500ms > 0)
|
||||
{
|
||||
gFmRadioCountdown_500ms--;
|
||||
@@ -1934,7 +1952,7 @@ void APP_TimeSlice500ms(void)
|
||||
) {
|
||||
BACKLIGHT_TurnOff();
|
||||
#ifdef ENABLE_FEAT_F4HWN_LOGO_SAV
|
||||
ScreenSaverTryDisplay();
|
||||
ScreenSaverTryDisplay(false);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -2024,10 +2042,14 @@ void APP_TimeSlice500ms(void)
|
||||
|
||||
if ((gBatteryCheckCounter & 1) == 0)
|
||||
{
|
||||
#if defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) || defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON)
|
||||
BATTERY_Sample(true);
|
||||
#else
|
||||
BOARD_ADC_GetBatteryInfo(&gBatteryVoltages[gBatteryVoltageIndex++], &gBatteryCurrent);
|
||||
if (gBatteryVoltageIndex > 3)
|
||||
gBatteryVoltageIndex = 0;
|
||||
BATTERY_GetReadings(true);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2043,7 +2065,7 @@ void APP_TimeSlice500ms(void)
|
||||
}
|
||||
|
||||
if (!gCssBackgroundScan && gScanStateDir == SCAN_OFF && !SCANNER_IsScanning()
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
&& (gFM_ScanState == FM_SCAN_OFF || gAskToSave)
|
||||
#endif
|
||||
#ifdef ENABLE_AIRCOPY
|
||||
@@ -2098,7 +2120,7 @@ void APP_TimeSlice500ms(void)
|
||||
|
||||
GUI_DisplayType_t disp = DISPLAY_INVALID;
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (gFmRadioMode && ! FUNCTION_IsRx()) {
|
||||
disp = DISPLAY_FM;
|
||||
}
|
||||
@@ -2120,7 +2142,7 @@ void APP_TimeSlice500ms(void)
|
||||
|
||||
if (!gPttIsPressed && gVFOStateResumeCountdown_500ms > 0 && --gVFOStateResumeCountdown_500ms == 0) {
|
||||
RADIO_SetVfoState(VFO_STATE_NORMAL);
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (gFmRadioMode && !FUNCTION_IsRx()) {
|
||||
// switch back to FM radio mode
|
||||
FM_Start();
|
||||
@@ -2136,7 +2158,7 @@ void APP_TimeSlice500ms(void)
|
||||
!gAskToSave &&
|
||||
!gCssBackgroundScan)
|
||||
{
|
||||
ScreenSaverTryDisplay();
|
||||
ScreenSaverTryDisplay(false);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -2174,17 +2196,15 @@ void APP_TimeSlice500ms(void)
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(ENABLE_ALARM) || defined(ENABLE_TX1750)
|
||||
static void ALARM_Off(void)
|
||||
#ifdef ENABLE_TX1750
|
||||
static void TX1750_Off(void)
|
||||
{
|
||||
AUDIO_AudioPathOff();
|
||||
gEnableSpeaker = false;
|
||||
|
||||
if (gAlarmState == ALARM_STATE_TXALARM || gAlarmState == ALARM_STATE_TX1750) {
|
||||
RADIO_SendEndOfTransmission();
|
||||
}
|
||||
RADIO_SendEndOfTransmission();
|
||||
|
||||
gAlarmState = ALARM_STATE_OFF;
|
||||
gTx1750Active = false;
|
||||
|
||||
#ifdef ENABLE_VOX
|
||||
gVoxResumeCountdown = 80;
|
||||
@@ -2275,7 +2295,7 @@ static void ProcessKey(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
|
||||
flagSaveSettings = false;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (gFlagSaveFM) {
|
||||
SETTINGS_SaveFM();
|
||||
gFlagSaveFM = false;
|
||||
@@ -2448,8 +2468,8 @@ static void ProcessKey(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
|
||||
}
|
||||
|
||||
if (gCurrentFunction == FUNCTION_TRANSMIT) {
|
||||
#if defined(ENABLE_ALARM) || defined(ENABLE_TX1750)
|
||||
if (gAlarmState == ALARM_STATE_OFF)
|
||||
#ifdef ENABLE_TX1750
|
||||
if (!gTx1750Active)
|
||||
#endif
|
||||
{
|
||||
char Code;
|
||||
@@ -2501,10 +2521,9 @@ static void ProcessKey(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
|
||||
BK4819_PlayDTMFEx(gEeprom.DTMF_SIDE_TONE, Code);
|
||||
}
|
||||
}
|
||||
#if defined(ENABLE_ALARM) || defined(ENABLE_TX1750)
|
||||
// else if ((!bKeyHeld && bKeyPressed) || (gAlarmState == ALARM_STATE_TX1750 && bKeyHeld && !bKeyPressed)) {
|
||||
else if ((bKeyHeld != bKeyPressed) && (gAlarmState == ALARM_STATE_TX1750 || bKeyPressed)) {
|
||||
ALARM_Off();
|
||||
#ifdef ENABLE_TX1750
|
||||
else if (bKeyHeld != bKeyPressed) {
|
||||
TX1750_Off();
|
||||
|
||||
if (gEeprom.REPEATER_TAIL_TONE_ELIMINATION == 0)
|
||||
FUNCTION_Select(FUNCTION_FOREGROUND);
|
||||
@@ -2570,7 +2589,7 @@ Skip:
|
||||
gUpdateStatus = true;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (gRequestSaveFM) {
|
||||
gRequestSaveFM = false;
|
||||
if (!bKeyHeld)
|
||||
|
||||
@@ -32,5 +32,7 @@ void APP_Update(void);
|
||||
void APP_TimeSlice10ms(void);
|
||||
void APP_TimeSlice500ms(void);
|
||||
bool APP_IsScreenSaverDisplayed(void);
|
||||
void APP_ModalBacklightTick(bool allowScreenSaver);
|
||||
void APP_ModalScreenSaverExit(void);
|
||||
|
||||
#endif
|
||||
+3
-14
@@ -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();
|
||||
}
|
||||
|
||||
@@ -42,7 +42,6 @@ typedef enum {
|
||||
extern BEAM_Mode_t gBeamMode;
|
||||
extern BEAM_Status_t gBeamStatus;
|
||||
extern uint16_t gBeamCopiedChannel;
|
||||
extern uint8_t gBeamRxWordCount;
|
||||
extern bool gBeamActive;
|
||||
|
||||
void ACTION_Beam(void);
|
||||
|
||||
+26
-32
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -553,11 +553,7 @@ static void ScanFastApplyChannelShape(ModulationMode_t modulation)
|
||||
}
|
||||
else
|
||||
{
|
||||
#ifdef ENABLE_AM_FIX
|
||||
BK4819_SetFilterBandwidth(BK4819_FILTER_BW_WIDE, true);
|
||||
#else
|
||||
BK4819_SetFilterBandwidth(BK4819_FILTER_BW_WIDE, false);
|
||||
#endif
|
||||
}
|
||||
|
||||
if (modulationChanged)
|
||||
|
||||
+2
-2
@@ -18,7 +18,7 @@
|
||||
#include <stdio.h> // NULL
|
||||
|
||||
#include "app/chFrScanner.h"
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
#include "app/fm.h"
|
||||
#endif
|
||||
#include "app/scanner.h"
|
||||
@@ -279,7 +279,7 @@ void DTMF_HandleRequest(void)
|
||||
|
||||
gDTMF_ReplyState = DTMF_REPLY_AB;
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (gFmRadioMode)
|
||||
{
|
||||
FM_TurnOff();
|
||||
|
||||
+6
-1
@@ -33,6 +33,7 @@
|
||||
#include "ui/ui.h"
|
||||
|
||||
uint16_t gFM_Channels[FM_CHANNELS_MAX];
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
bool gFmRadioMode;
|
||||
uint8_t gFmRadioCountdown_500ms;
|
||||
volatile uint16_t gFmPlayCountdown_10ms;
|
||||
@@ -52,8 +53,8 @@ const uint8_t BUTTON_EVENT_HELD = BUTTON_STATE_PRESSED | BUTTON_STATE_HELD;
|
||||
const uint8_t BUTTON_EVENT_SHORT = 0;
|
||||
const uint8_t BUTTON_EVENT_LONG = BUTTON_STATE_HELD;
|
||||
|
||||
|
||||
static void Key_FUNC(KEY_Code_t Key, uint8_t state);
|
||||
#endif
|
||||
|
||||
bool FM_CheckValidChannel(uint8_t Channel)
|
||||
{
|
||||
@@ -94,6 +95,7 @@ int FM_ConfigureChannelState(void)
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
void FM_SetFrequency(void)
|
||||
{
|
||||
BK1080_SetFrequency(gEeprom.FM_FrequencyPlaying, gEeprom.FM_Band/*, gEeprom.FM_Space*/);
|
||||
@@ -196,6 +198,7 @@ void FM_PlayAndUpdate(void)
|
||||
BACKLIGHT_TurnOn();
|
||||
FM_AudioPathOn();
|
||||
}
|
||||
#endif
|
||||
|
||||
int FM_CheckFrequencyLock(uint16_t Frequency, uint16_t LowerLimit)
|
||||
{
|
||||
@@ -240,6 +243,7 @@ int FM_CheckFrequencyLock(uint16_t Frequency, uint16_t LowerLimit)
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
static void Key_DIGITS(KEY_Code_t Key, uint8_t state)
|
||||
{
|
||||
enum { STATE_FREQ_MODE, STATE_MR_MODE, STATE_SAVE };
|
||||
@@ -662,5 +666,6 @@ void FM_Start(void)
|
||||
SETTINGS_WriteCurrentState();
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
+13
-8
@@ -19,9 +19,12 @@
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
|
||||
#include "driver/keyboard.h"
|
||||
#include "misc.h"
|
||||
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
#include "driver/keyboard.h"
|
||||
#endif
|
||||
|
||||
#define FM_CHANNEL_UP 0x01
|
||||
#define FM_CHANNEL_DOWN 0xFF
|
||||
|
||||
@@ -30,32 +33,34 @@ enum {
|
||||
};
|
||||
|
||||
extern uint16_t gFM_Channels[FM_CHANNELS_MAX];
|
||||
|
||||
bool FM_CheckValidChannel(uint8_t Channel);
|
||||
// returns first valid channel starting at Channel
|
||||
uint8_t FM_FindNextChannel(uint8_t Channel, uint8_t Direction);
|
||||
int FM_ConfigureChannelState(void);
|
||||
int FM_CheckFrequencyLock(uint16_t Frequency, uint16_t LowerLimit);
|
||||
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
extern bool gFmRadioMode;
|
||||
extern uint8_t gFmRadioCountdown_500ms;
|
||||
extern volatile uint16_t gFmPlayCountdown_10ms;
|
||||
extern volatile int8_t gFM_ScanState;
|
||||
extern bool gFM_AutoScan;
|
||||
extern uint8_t gFM_ChannelPosition;
|
||||
// Doubts about whether this should be signed or not
|
||||
extern uint16_t gFM_FrequencyDeviation;
|
||||
extern bool gFM_FoundFrequency;
|
||||
extern uint16_t gFM_RestoreCountdown_10ms;
|
||||
|
||||
bool FM_CheckValidChannel(uint8_t Channel);
|
||||
// returns first valid channel starting at Channel
|
||||
uint8_t FM_FindNextChannel(uint8_t Channel, uint8_t Direction);
|
||||
int FM_ConfigureChannelState(void);
|
||||
void FM_TurnOff(void);
|
||||
void FM_EraseChannels(void);
|
||||
|
||||
void FM_Tune(uint16_t Frequency, int8_t Step, bool bFlag);
|
||||
void FM_PlayAndUpdate(void);
|
||||
int FM_CheckFrequencyLock(uint16_t Frequency, uint16_t LowerLimit);
|
||||
|
||||
void FM_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld);
|
||||
|
||||
void FM_Play(void);
|
||||
void FM_Start(void);
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
+176
-177
@@ -16,7 +16,7 @@
|
||||
|
||||
#include "app/foxhunt.h"
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_FOXHUNT
|
||||
#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON)
|
||||
|
||||
#if defined(ENABLE_UART) || defined(ENABLE_USB)
|
||||
#include "app/uart.h"
|
||||
@@ -26,6 +26,9 @@
|
||||
#include "k5viewer.h"
|
||||
#endif
|
||||
|
||||
#include "settings.h"
|
||||
#include "ui/status.h"
|
||||
|
||||
// Signal window mapped onto the RSSI bar, in dBm.
|
||||
// Roughly S0 (empty) to S9 + 40 dB (full), IARU VHF/UHF scale.
|
||||
#define FOXHUNT_DBM_FLOOR (-141)
|
||||
@@ -84,11 +87,15 @@
|
||||
#define FOXHUNT_AUDIO_BEEP 1
|
||||
#define FOXHUNT_AUDIO_STATION 2
|
||||
|
||||
// --- Beacon (fox) sub-mode ---------------------------------------------------
|
||||
// --- Beacon (fox) app --------------------------------------------------------
|
||||
// Turns the radio into the hidden transmitter: each cycle keys up on the TX VFO and
|
||||
// repeats the CW fox identifier (MOE..MO5, or "<call> MOE") in Morse for the TX window,
|
||||
// then stays silent for the (adjustable) idle gap. The carrier stays up during the
|
||||
// window; only the tone modulation is keyed on/off (EnterTxMute/ExitTxMute) = MCW on FM.
|
||||
// then stays silent for the (adjustable) idle gap. Two keying modes (KEY_4):
|
||||
// TONE (default): carrier stays up the whole window, only the tone is keyed on/off
|
||||
// (EnterTxMute/ExitTxMute) = MCW on FM (F2A).
|
||||
// CARR: the carrier (PA_ENABLE) is keyed together with the tone, so between
|
||||
// elements the carrier itself is gone (carrier interruption, the ARDF
|
||||
// field pattern) — harder to home in on, at the cost of some key clicks.
|
||||
#define FOXHUNT_BEACON_TONE_HZ 1000 // CW tone pitch (Hz)
|
||||
#define FOXHUNT_MORSE_UNIT_MS 100 // one Morse time unit (~12 WPM, ARDF pace)
|
||||
#define FOXHUNT_BEACON_IDLE_DEF 30 // default silence between IDs (s)
|
||||
@@ -102,7 +109,6 @@
|
||||
#define FOXHUNT_BEACON_TX_MIN 5 // shortest TX window (s)
|
||||
#define FOXHUNT_BEACON_TX_MAX 60 // longest TX window (s)
|
||||
#define FOXHUNT_BEACON_TX_STEP 5 // TX adjust step (s)
|
||||
#define FOXHUNT_CALLSIGN_ADDR 0x00A0C8u // boot message line 1 in SPI flash
|
||||
#define FOXHUNT_CALLSIGN_MAX 12 // maximum boot-message characters used by the beacon
|
||||
|
||||
// Fox identifier (3 key). MOE..MO5 are the five standard IARU ARDF foxes ("MO" + 1..5
|
||||
@@ -199,8 +205,7 @@ static const uint8_t FOXHUNT_MORSE_DIGIT[10] = {
|
||||
// Trailing character of each fox id: MOE / MOI / MOS / MOH / MO5 (index = FOXHUNT_FOX_*).
|
||||
static const char FOXHUNT_FOX_TAIL[5] = {'E', 'I', 'S', 'H', '5'};
|
||||
|
||||
// Beacon sub-mode state.
|
||||
static bool foxBeacon; // false = hunt (RX), true = beacon (TX)
|
||||
// Beacon state.
|
||||
static bool beaconPhaseTx; // true = transmit this tick, false = idle gap
|
||||
static uint8_t beaconIdle; // configured silence between IDs (s)
|
||||
static uint8_t beaconIdleLeft; // seconds left in the current silence
|
||||
@@ -211,6 +216,7 @@ static uint8_t beaconTx; // TX window length (s): the ID repeats for th
|
||||
static uint16_t beaconTxMsLeft; // ms left in the current TX window (drained as it plays)
|
||||
static uint8_t beaconTxSecShown; // whole-second value last painted on the TX line
|
||||
static uint8_t foxFox; // selected fox identifier (FOXHUNT_FOX_*)
|
||||
static bool beaconCarrierKeyed; // false = TONE (F2A, keyed tone); true = CARR (keyed carrier)
|
||||
static char foxCall[FOXHUNT_CALLSIGN_MAX + 1]; // sanitised callsign for the CALL id
|
||||
|
||||
static void FOXHUNT_EnterHunt(void);
|
||||
@@ -428,9 +434,9 @@ static void FOXHUNT_Tag(const char *s, uint8_t x, uint8_t line)
|
||||
static void FOXHUNT_DrawFKey(void)
|
||||
{
|
||||
if (foxLocked)
|
||||
memcpy(gStatusLine + 69, gFontKeyLock, sizeof(gFontKeyLock));
|
||||
memcpy(gStatusLine + 70, gFontKeyLock, sizeof(gFontKeyLock));
|
||||
else if (gWasFKeyPressed)
|
||||
memcpy(gStatusLine + 69, gFontF, sizeof(gFontF));
|
||||
memcpy(gStatusLine + 70, gFontF, sizeof(gFontF));
|
||||
}
|
||||
|
||||
// Draw a string right-aligned in the small (7 px) font: its right edge lands at rightX.
|
||||
@@ -439,24 +445,6 @@ static void FOXHUNT_DrawRightSmall(const char *s, uint8_t rightX, uint8_t line)
|
||||
UI_PrintStringSmallNormal(s, (uint8_t)(rightX - strlen(s) * 7), 0, line);
|
||||
}
|
||||
|
||||
// Battery icon plus the optional voltage/percentage text, top-right of the status
|
||||
// line — shared by the hunt and beacon screens.
|
||||
static void FOXHUNT_DrawStatusBattery(void)
|
||||
{
|
||||
unsigned int bx = LCD_WIDTH - sizeof(BITMAP_BatteryLevel1);
|
||||
UI_DrawBattery(gStatusLine + bx, gBatteryDisplayLevel, gLowBatteryBlink);
|
||||
if (gSetting_battery_text != 0) {
|
||||
if (gSetting_battery_text == 1) { // voltage
|
||||
const uint16_t v = MIN(gBatteryVoltageAverage, 999);
|
||||
sprintf(str, "%u.%02u", v / 100, v % 100);
|
||||
} else { // percentage
|
||||
sprintf(str, "%02u%%", BATTERY_VoltsToPercent(gBatteryVoltageAverage));
|
||||
}
|
||||
bx -= 7 * strlen(str);
|
||||
UI_PrintStringSmallBufferNormal(str, gStatusLine + bx);
|
||||
}
|
||||
}
|
||||
|
||||
// Right-aligned frequency on the bottom line (line 6), shared by the hunt and
|
||||
// beacon screens.
|
||||
static void FOXHUNT_DrawFreqBR(uint32_t freq)
|
||||
@@ -472,7 +460,7 @@ static void FOXHUNT_BeaconChrome(void)
|
||||
UI_DisplayClear();
|
||||
UI_StatusClear();
|
||||
GUI_DisplaySmallestInverse("BEACON", 2, 0, true, true, 26);
|
||||
FOXHUNT_DrawStatusBattery();
|
||||
UI_DrawStatusBattery(gStatusLine, str);
|
||||
FOXHUNT_DrawFKey();
|
||||
FOXHUNT_DrawFreqBR(gTxVfo->pTX->Frequency);
|
||||
}
|
||||
@@ -490,7 +478,7 @@ static void FOXHUNT_Draw(void)
|
||||
GUI_DisplaySmallestInverse("FOX HUNT", 2, 0, true, true, 34);
|
||||
|
||||
// Battery (icon + optional percentage/voltage) top-right, as on the main screens.
|
||||
FOXHUNT_DrawStatusBattery();
|
||||
UI_DrawStatusBattery(gStatusLine, str);
|
||||
FOXHUNT_DrawFKey();
|
||||
|
||||
// Gauge-mode icon (2 key), between the label and the audio icon: ascending
|
||||
@@ -571,7 +559,7 @@ static void FOXHUNT_Draw(void)
|
||||
// the burst end, fox at the next repeat), so cycling any of them is safe at any time.
|
||||
|
||||
// Wrap a 0..count-1 index one step forward (dir > 0) or backward, both ways round.
|
||||
static uint8_t FOXHUNT_WrapStep(uint8_t v, uint8_t count, int8_t dir)
|
||||
static __attribute__((noinline)) uint8_t FOXHUNT_WrapStep(uint8_t v, uint8_t count, int8_t dir)
|
||||
{
|
||||
return (uint8_t)((v + (dir > 0 ? 1u : (unsigned)(count - 1u))) % count);
|
||||
}
|
||||
@@ -624,6 +612,16 @@ static void FOXHUNT_AttCycle(int8_t dir)
|
||||
FOXHUNT_RebaseMeasurements();
|
||||
}
|
||||
|
||||
// Convert the two navigation keys into the same semantic value direction used
|
||||
// by the resident menus and by the overlay FoxHunt app:
|
||||
// UV-K5 UP/DOWN -> +1/-1
|
||||
// UV-K1 LEFT/RIGHT -> -1/+1
|
||||
static int8_t FOXHUNT_NavDirection(KEY_Code_t key)
|
||||
{
|
||||
int8_t direction = (key == KEY_UP) ? 1 : -1;
|
||||
return gEeprom.SET_NAV ? direction : -direction;
|
||||
}
|
||||
|
||||
// Fox identifier: MOE -> MOI -> MOS -> MOH -> MO5 -> MO -> CALL (F reverses).
|
||||
static void FOXHUNT_FoxCycle(int8_t dir)
|
||||
{
|
||||
@@ -645,14 +643,15 @@ static void FOXHUNT_IdleCycle(int8_t dir)
|
||||
}
|
||||
|
||||
// Apply a beacon number key in the given direction; returns true when it changed a
|
||||
// setting so the caller can refresh. Keys follow the on-screen layout: 1 = TX (top-left),
|
||||
// 2 = IDLE (below it), 3 = FOX (right). Shared by both beacon phases.
|
||||
// setting so the caller can refresh. Keys: 1 = TX, 2 = IDLE, 3 = FOX, 4 = keying mode
|
||||
// (TONE/CARR, a plain toggle so dir is moot). Shared by both beacon phases.
|
||||
static bool FOXHUNT_BeaconKey(KEY_Code_t key, int8_t dir)
|
||||
{
|
||||
switch (key) {
|
||||
case KEY_1: FOXHUNT_TxCycle(dir); return true;
|
||||
case KEY_2: FOXHUNT_IdleCycle(dir); return true;
|
||||
case KEY_3: FOXHUNT_FoxCycle(dir); return true;
|
||||
case KEY_4: beaconCarrierKeyed = !beaconCarrierKeyed; return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
@@ -689,10 +688,7 @@ static void FOXHUNT_IdleHousekeeping(void)
|
||||
// hunt loop, and the beacon idle gap, never a burst), so the reading is not
|
||||
// pulled down by TX load — keeping the status icon live and letting the
|
||||
// beacon's battery gate react to a pack draining under a long run.
|
||||
BOARD_ADC_GetBatteryInfo(&gBatteryVoltages[gBatteryVoltageIndex++], &gBatteryCurrent);
|
||||
if (gBatteryVoltageIndex > 3)
|
||||
gBatteryVoltageIndex = 0;
|
||||
BATTERY_GetReadings(false);
|
||||
BATTERY_Sample(false);
|
||||
|
||||
// Persist any changed setting within ~0.5 s, so it survives a power-off (not
|
||||
// just a clean EXIT). No-op when nothing changed.
|
||||
@@ -750,8 +746,8 @@ static void FOXHUNT_HandleKeys(void)
|
||||
// released only by the long-press F handled above. Keeping ATT reachable is the whole
|
||||
// point: on the final approach the sensitivity still has to be pulled down by hand.
|
||||
if (foxLocked) {
|
||||
if (kbd.current == KEY_UP) FOXHUNT_AttCycle(+1); // more attenuation
|
||||
if (kbd.current == KEY_DOWN) FOXHUNT_AttCycle(-1); // less attenuation
|
||||
if (kbd.current == KEY_UP || kbd.current == KEY_DOWN)
|
||||
FOXHUNT_AttCycle(FOXHUNT_NavDirection(kbd.current));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -781,12 +777,9 @@ static void FOXHUNT_HandleKeys(void)
|
||||
FOXHUNT_AttCycle(dir);
|
||||
break;
|
||||
case KEY_UP:
|
||||
// Attenuation up one step (also the locked-mode control).
|
||||
FOXHUNT_AttCycle(+1);
|
||||
break;
|
||||
case KEY_DOWN:
|
||||
// Attenuation down one step.
|
||||
FOXHUNT_AttCycle(-1);
|
||||
// Follow SetNav: UP/DOWN on K5, LEFT/RIGHT on K1.
|
||||
FOXHUNT_AttCycle(FOXHUNT_NavDirection(kbd.current));
|
||||
break;
|
||||
case KEY_MENU:
|
||||
// Reset the peak / min hold and the trend reference (before each body scan).
|
||||
@@ -794,11 +787,6 @@ static void FOXHUNT_HandleKeys(void)
|
||||
minDbm = curDbm;
|
||||
trendRef = curDbm;
|
||||
break;
|
||||
case KEY_SIDE1:
|
||||
case KEY_SIDE2:
|
||||
// Same shortcut that opened Fox Hunt now toggles to the beacon.
|
||||
FOXHUNT_EnterBeacon();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -806,22 +794,18 @@ static void FOXHUNT_HandleKeys(void)
|
||||
gWasFKeyPressed = false; // any non-F key consumes (or cancels) the reverse arm
|
||||
}
|
||||
|
||||
// (Re)enter the hunt (RX) sub-mode: fixed front-end gain, attenuator and audio mode
|
||||
// re-applied, peak/trend reset. Called at start-up and when leaving the beacon. The
|
||||
// audio mode is a hunt setting, so it is (re)applied here, not forced off — it must
|
||||
// survive a trip through the beacon.
|
||||
// Enter Fox Hunt with fixed front-end gain, the persisted attenuator/audio mode,
|
||||
// and freshly rebased level tracking.
|
||||
static void FOXHUNT_EnterHunt(void)
|
||||
{
|
||||
foxBeacon = false;
|
||||
|
||||
BK4819_SetAGC(false);
|
||||
FOXHUNT_ApplyAtt();
|
||||
FOXHUNT_SetAudio(); // (re)apply the current audio mode: off / beep / station
|
||||
|
||||
// Let the RSSI settle to the gain just applied, then reset the peak/min hold, trend
|
||||
// reference and signal history onto a fresh reading (also re-primes the sparkline on
|
||||
// return from the beacon). Same settle as FOXHUNT_AttCycle: without it a restored
|
||||
// BYP/BYP+ step at start-up or on beacon return would rebase onto the old-gain reading.
|
||||
// reference and signal history onto a fresh reading. Same settle as
|
||||
// FOXHUNT_AttCycle: without it a restored
|
||||
// BYP/BYP+ step at start-up would rebase onto the old-gain reading.
|
||||
// SetAudio already waits when audio is on, but not in the default audio-off case.
|
||||
SYSTEM_DelayMs(FOXHUNT_ATT_SETTLE_MS);
|
||||
FOXHUNT_RebaseMeasurements();
|
||||
@@ -841,18 +825,15 @@ static VfoState_t FOXHUNT_TxState(void)
|
||||
return VFO_STATE_BAT_LOW;
|
||||
if (gBatteryDisplayLevel > 6)
|
||||
return VFO_STATE_VOLTAGE_HIGH;
|
||||
#ifndef ENABLE_TX_WHEN_AM
|
||||
if (gTxVfo->Modulation != MODULATION_FM)
|
||||
return VFO_STATE_TX_DISABLE;
|
||||
#endif
|
||||
return VFO_STATE_NORMAL;
|
||||
}
|
||||
|
||||
// Refuse feedback shown when a burst is barred: reuse the beacon screen layout and
|
||||
// the radio's own state label (VfoStateStr, e.g. "TX DISABLE" / "BAT LOW" / "VOLT
|
||||
// HIGH"), same font as the main screen, for a beat; the caller then falls back to
|
||||
// the hunt. The RX front-end and audio path are left untouched so the hunt keeps
|
||||
// reading cleanly.
|
||||
// HIGH"), same font as the main screen, for a beat; the caller then waits through
|
||||
// a fresh idle gap before checking again.
|
||||
static void FOXHUNT_TxDeniedNotice(VfoState_t state)
|
||||
{
|
||||
// Frame (clear + BEACON tag + battery + barred TX frequency).
|
||||
@@ -869,12 +850,10 @@ static void FOXHUNT_TxDeniedNotice(VfoState_t state)
|
||||
FOXHUNT_TickDelay();
|
||||
}
|
||||
|
||||
// Enter the beacon (fox) sub-mode. The TX gates are enforced per burst in
|
||||
// FOXHUNT_BeaconTick (which runs immediately), so entry just arms the state.
|
||||
// Enter the Beacon app. TX gates are enforced before each burst.
|
||||
static void FOXHUNT_EnterBeacon(void)
|
||||
{
|
||||
foxBeacon = true;
|
||||
AUDIO_AudioPathOff(); // no RX audio while beaconing; foxAudioMode kept for the hunt
|
||||
AUDIO_AudioPathOff();
|
||||
gCurrentVfo = gTxVfo; // the VFO RADIO_SetTxParameters keys up
|
||||
beaconPhaseTx = true;
|
||||
}
|
||||
@@ -893,8 +872,8 @@ static void FOXHUNT_DrawTxSeconds(void)
|
||||
}
|
||||
|
||||
// Sleep in short slices while watching the keypad, so a burst stays interactive and the
|
||||
// TX-window countdown (beaconTxMsLeft) drains in real time. EXIT leaves Fox Hunt and a
|
||||
// side key aborts back to the hunt (both return true). The 1/2/3 keys edit the beacon
|
||||
// TX-window countdown (beaconTxMsLeft) drains in real time. EXIT leaves Beacon. The
|
||||
// 1/2/3 keys edit the beacon
|
||||
// settings live, exactly as in the idle phase: the TX window (1) and idle gap (2) take
|
||||
// effect at the upcoming window / gap, and a FOX change (3) at the next repeat (the
|
||||
// message is rebuilt there, never under the running loop).
|
||||
@@ -940,17 +919,12 @@ static bool FOXHUNT_TxDelay(uint16_t ms)
|
||||
continue;
|
||||
|
||||
if (kbd.current == KEY_EXIT) {
|
||||
foxRunning = false; // abort the burst and leave Fox Hunt
|
||||
return true;
|
||||
}
|
||||
if (kbd.current == KEY_SIDE1 || kbd.current == KEY_SIDE2) {
|
||||
foxBeacon = false; // abort the burst and switch back to hunt
|
||||
foxRunning = false; // abort the burst and leave Beacon
|
||||
return true;
|
||||
}
|
||||
if (kbd.current == KEY_MENU) {
|
||||
// Cut the transmission short but stay in Beacon: aborting the burst without
|
||||
// clearing foxBeacon/foxRunning drops the loop straight into a fresh idle
|
||||
// gap of the configured IDLE length.
|
||||
// Cut the transmission short but stay in Beacon: the loop drops straight
|
||||
// into a fresh idle gap of the configured IDLE length.
|
||||
return true;
|
||||
}
|
||||
if (kbd.current == KEY_F) { // arm / disarm the reverse step
|
||||
@@ -978,8 +952,28 @@ static uint8_t FOXHUNT_MorseByte(char c)
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Send one character as modulated CW: key the running TX tone on per element,
|
||||
// muting the modulation (carrier stays up) between them. Returns true if aborted.
|
||||
// Beacon keying primitive. Both modes key the tone (Enter/ExitTxMute); CARR additionally
|
||||
// gates the PA in lockstep, so between elements the carrier itself is gone — not just the
|
||||
// tone. Muting the tone in CARR too is deliberate: gating PA_ENABLE does not perfectly
|
||||
// kill the carrier, and a tone left riding would bleed through that residual carrier as a
|
||||
// near-continuous note. The PLL stays locked, so the carrier gate is only a GPIO toggle.
|
||||
static void FOXHUNT_KeyOn(void)
|
||||
{
|
||||
BK4819_ExitTxMute();
|
||||
// Assert the carrier in both modes: a no-op in TONE (already up), but it re-arms the PA
|
||||
// if the mode was switched from CARR mid-window while the carrier happened to be down.
|
||||
BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, true);
|
||||
}
|
||||
static void FOXHUNT_KeyOff(void)
|
||||
{
|
||||
BK4819_EnterTxMute();
|
||||
if (beaconCarrierKeyed)
|
||||
BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, false);
|
||||
}
|
||||
|
||||
// Send one character as Morse: key each element on (FOXHUNT_KeyOn) for its dit/dah
|
||||
// duration, off (FOXHUNT_KeyOff) in the gaps. What "key" means depends on the beacon
|
||||
// mode (tone vs carrier), see above. Returns true if aborted.
|
||||
static bool FOXHUNT_MorseChar(char c, uint8_t visibleChars)
|
||||
{
|
||||
uint8_t code = FOXHUNT_MorseByte(c);
|
||||
@@ -996,9 +990,9 @@ static bool FOXHUNT_MorseChar(char c, uint8_t visibleChars)
|
||||
for (bit >>= 1; bit; bit >>= 1) { // then walk the elements, MSB first
|
||||
const uint16_t on = (code & bit) ? (FOXHUNT_MORSE_UNIT_MS * 3) // dah
|
||||
: FOXHUNT_MORSE_UNIT_MS; // dit
|
||||
BK4819_ExitTxMute();
|
||||
if (FOXHUNT_TxDelay(on)) { BK4819_EnterTxMute(); return true; }
|
||||
BK4819_EnterTxMute();
|
||||
FOXHUNT_KeyOn();
|
||||
if (FOXHUNT_TxDelay(on)) { FOXHUNT_KeyOff(); return true; }
|
||||
FOXHUNT_KeyOff();
|
||||
if (FOXHUNT_TxDelay(FOXHUNT_MORSE_UNIT_MS)) return true; // intra gap
|
||||
}
|
||||
|
||||
@@ -1017,11 +1011,14 @@ static bool FOXHUNT_MorseChar(char c, uint8_t visibleChars)
|
||||
// keys up for a fixed slot rather than sending a single one-shot.
|
||||
static void FOXHUNT_BeaconTransmit(void)
|
||||
{
|
||||
RADIO_SetTxParameters(); // key up: carrier + PA
|
||||
RADIO_SetTxParameters(); // key up: carrier + PA + PLL lock
|
||||
|
||||
// Prime the tone generator, then start silent before the first element.
|
||||
// Prime the tone generator, then open the window silent before the first element:
|
||||
// tone muted in both modes, and in CARR the carrier dropped too (PLL still locked).
|
||||
BK4819_TransmitTone(false, FOXHUNT_BEACON_TONE_HZ);
|
||||
BK4819_EnterTxMute();
|
||||
if (beaconCarrierKeyed)
|
||||
BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, false);
|
||||
|
||||
beaconTxMsLeft = (uint16_t)beaconTx * 1000u; // drained inside FOXHUNT_TxDelay
|
||||
|
||||
@@ -1065,8 +1062,8 @@ static void FOXHUNT_BeaconTransmit(void)
|
||||
stop = FOXHUNT_TxDelay(FOXHUNT_MORSE_UNIT_MS * 7);
|
||||
}
|
||||
|
||||
// Key down: mute, drop the PA and restore RX for the silent gap. If the burst
|
||||
// was interrupted, FOXHUNT_TxDelay already set the target mode (quit or hunt).
|
||||
// Key down: mute, drop the PA and restore RX for the silent gap. If EXIT
|
||||
// interrupted the burst, FOXHUNT_TxDelay has already stopped the app.
|
||||
BK4819_EnterTxMute();
|
||||
BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, false);
|
||||
RADIO_SetupRegisters(true);
|
||||
@@ -1183,16 +1180,19 @@ static void FOXHUNT_BeaconDraw(bool txNow, uint8_t idleLeft)
|
||||
UI_PrintString(str, 0, 127, 3, 10);
|
||||
}
|
||||
|
||||
// Bottom-left column: 1 = TX window over 2 = IDLE gap. Bottom-right: 3 = FOX id over
|
||||
// the freq (drawn by FOXHUNT_BeaconChrome). All three are inverse tags; each has its
|
||||
// own number key, editable in either phase, so there is no cursor to show.
|
||||
// Settings tags: TX (line 5, left) over IDLE (line 6, left). On line 5, right of TX:
|
||||
// FOX shifted left with the keying mode (TONE/CARR) right-aligned beside it, so the pair
|
||||
// spans roughly the width of the TX frequency drawn just below on line 6
|
||||
// (FOXHUNT_BeaconChrome). Keys: 1 TX, 2 IDLE, 3 FOX, 4 mode; editable in either phase.
|
||||
sprintf(str, "TX %us", beaconTx);
|
||||
FOXHUNT_Tag(str, 4, 5);
|
||||
sprintf(str, "IDLE %us", beaconIdle);
|
||||
FOXHUNT_Tag(str, 4, 6);
|
||||
|
||||
FOXHUNT_FoxLabel(str);
|
||||
FOXHUNT_Tag(str, (uint8_t)(125 - strlen(str) * 4), 5);
|
||||
FOXHUNT_Tag(str, 66, 5);
|
||||
|
||||
const char *mode = beaconCarrierKeyed ? "CARR" : "TONE";
|
||||
FOXHUNT_Tag(mode, (uint8_t)(125 - strlen(mode) * 4), 5);
|
||||
}
|
||||
|
||||
static void FOXHUNT_BeaconKeys(void)
|
||||
@@ -1222,11 +1222,7 @@ static void FOXHUNT_BeaconKeys(void)
|
||||
|
||||
switch (kbd.current) {
|
||||
case KEY_EXIT:
|
||||
foxRunning = false; // leave Fox Hunt entirely
|
||||
break;
|
||||
case KEY_SIDE1:
|
||||
case KEY_SIDE2:
|
||||
foxBeacon = false; // toggle back to the hunt
|
||||
foxRunning = false;
|
||||
break;
|
||||
case KEY_MENU:
|
||||
// Restart a full idle interval now (the TX-phase M drops here too).
|
||||
@@ -1251,28 +1247,26 @@ static void FOXHUNT_BeaconTick(void)
|
||||
// LOCK / TX LOCK and modulation are fixed for the session, but the battery
|
||||
// is re-sampled over the idle gaps (FOXHUNT_IdleHousekeeping), so a beacon
|
||||
// left running stops keying up once the pack falls low or over-voltage
|
||||
// instead of transmitting blind. Refuse, notify, and drop back to hunt.
|
||||
// instead of transmitting blind. Refuse, notify, then retry after an idle gap.
|
||||
VfoState_t state = FOXHUNT_TxState();
|
||||
if (state != VFO_STATE_NORMAL) {
|
||||
FOXHUNT_TxDeniedNotice(state);
|
||||
foxBeacon = false;
|
||||
beaconPhaseTx = false;
|
||||
FOXHUNT_EnterHunt();
|
||||
beaconIdleLeft = beaconIdle;
|
||||
beaconIdleTick = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// The burst repeats the ID for the whole TX window, redrawing the screen
|
||||
// itself each repeat (remaining-window countdown); it may clear foxBeacon
|
||||
// (side-key abort) or foxRunning (EXIT).
|
||||
// itself each repeat (remaining-window countdown); EXIT may clear
|
||||
// foxRunning and abort it.
|
||||
FOXHUNT_BeaconTransmit();
|
||||
|
||||
beaconPhaseTx = false;
|
||||
beaconIdleLeft = beaconIdle;
|
||||
beaconIdleTick = 0;
|
||||
|
||||
if (foxRunning && !foxBeacon) // side-key switched to hunt
|
||||
FOXHUNT_EnterHunt();
|
||||
return; // EXIT (quit) falls through
|
||||
return;
|
||||
}
|
||||
|
||||
// Idle (silent) phase: responsive, editable countdown.
|
||||
@@ -1281,12 +1275,8 @@ static void FOXHUNT_BeaconTick(void)
|
||||
#endif
|
||||
FOXHUNT_BeaconKeys();
|
||||
|
||||
if (!foxRunning) // EXIT: leave Fox Hunt
|
||||
if (!foxRunning)
|
||||
return;
|
||||
if (!foxBeacon) { // side-key: back to the hunt
|
||||
FOXHUNT_EnterHunt();
|
||||
return;
|
||||
}
|
||||
|
||||
FOXHUNT_BeaconDraw(false, beaconIdleLeft);
|
||||
FOXHUNT_BlitScreen();
|
||||
@@ -1312,14 +1302,14 @@ static void FOXHUNT_BeaconTick(void)
|
||||
// (eeprom_compat.c) so aircopy clones them with the VFOs. A factory reset clears it.
|
||||
#define FOXHUNT_CFG_ADDR 0x0090E0u
|
||||
#define FOXHUNT_CFG_MAGIC 0xF4u // tells a written config from erased flash (0xFF)
|
||||
#define FOXHUNT_CFG_LEN 7 // magic + att + graph + audio + idle + fox + tx
|
||||
#define FOXHUNT_CFG_LEN 8 // magic + att + graph + audio + idle + fox + tx + carrier
|
||||
|
||||
// RAM mirror of the bytes last written to flash, so FOXHUNT_SaveConfig only touches
|
||||
// the flash when a value actually changed.
|
||||
static uint8_t foxCfgSaved[FOXHUNT_CFG_LEN];
|
||||
|
||||
// Pack the live settings into the on-flash layout.
|
||||
static void FOXHUNT_ConfigPack(uint8_t out[FOXHUNT_CFG_LEN])
|
||||
static __attribute__((noinline)) void FOXHUNT_ConfigPack(uint8_t out[FOXHUNT_CFG_LEN])
|
||||
{
|
||||
out[0] = FOXHUNT_CFG_MAGIC;
|
||||
out[1] = attStep;
|
||||
@@ -1328,6 +1318,7 @@ static void FOXHUNT_ConfigPack(uint8_t out[FOXHUNT_CFG_LEN])
|
||||
out[4] = beaconIdle;
|
||||
out[5] = foxFox;
|
||||
out[6] = beaconTx;
|
||||
out[7] = beaconCarrierKeyed;
|
||||
}
|
||||
|
||||
// Restore persisted settings; erased/invalid flash leaves the defaults in place.
|
||||
@@ -1347,6 +1338,7 @@ static void FOXHUNT_LoadConfig(void)
|
||||
if (cfg[6] >= FOXHUNT_BEACON_TX_MIN && cfg[6] <= FOXHUNT_BEACON_TX_MAX
|
||||
&& (cfg[6] % FOXHUNT_BEACON_TX_STEP) == 0)
|
||||
beaconTx = cfg[6];
|
||||
if (cfg[7] <= 1) beaconCarrierKeyed = cfg[7]; // erased (0xFF) legacy config -> keep default
|
||||
}
|
||||
|
||||
FOXHUNT_ConfigPack(foxCfgSaved); // mirror the loaded (or default) state
|
||||
@@ -1354,7 +1346,7 @@ static void FOXHUNT_LoadConfig(void)
|
||||
|
||||
// Write the settings to flash, but only when they changed since the last write.
|
||||
// Called on the 500 ms tick (so any change persists within ~0.5 s and survives a
|
||||
// power-off, not just a clean EXIT) and once more on leaving Fox Hunt. The RAM
|
||||
// power-off, not just a clean EXIT) and once more on leaving either app. The RAM
|
||||
// compare avoids re-reading the 4 KB sector on every quiet tick.
|
||||
static void FOXHUNT_SaveConfig(void)
|
||||
{
|
||||
@@ -1368,83 +1360,91 @@ static void FOXHUNT_SaveConfig(void)
|
||||
memcpy(foxCfgSaved, cfg, sizeof(cfg));
|
||||
}
|
||||
|
||||
void APP_RunFoxHunt(void)
|
||||
static void FOXHUNT_Begin(void)
|
||||
{
|
||||
// Finish any pending backlight fade, then start with the screen on and a full
|
||||
// BLTime window (BACKLIGHT_TurnOn also re-arms the sleep countdown).
|
||||
BACKLIGHT_UpdateTickless();
|
||||
BACKLIGHT_TurnOn();
|
||||
|
||||
// Hunt on the user-selected VFO: dual-watch / cross-band may have left the
|
||||
// radio listening on the other VFO, so force RX on the selected one and retune.
|
||||
// Both apps operate on the user-selected VFO. Dual watch may have left the
|
||||
// receiver on the other one, so establish a deterministic starting point.
|
||||
gEeprom.RX_VFO = gEeprom.TX_VFO;
|
||||
gRxVfo = gTxVfo;
|
||||
gCurrentVfo = gTxVfo;
|
||||
RADIO_SetupRegisters(true);
|
||||
|
||||
// Beacon defaults (LoadConfig may override fox / tx / idle below).
|
||||
beaconIdle = FOXHUNT_BEACON_IDLE_DEF;
|
||||
beaconTx = FOXHUNT_BEACON_TX_DEF;
|
||||
foxFox = FOXHUNT_FOX_CALL; // identified beacon by default (legal on ham)
|
||||
beaconPhaseTx = false;
|
||||
|
||||
// Read the callsign from the boot message (line 1), sanitised to what Morse can
|
||||
// send; empty/erased leaves foxCall empty (the CALL id then falls back to bare MOE).
|
||||
{
|
||||
char call[FOXHUNT_CALLSIGN_MAX + 1];
|
||||
uint8_t n = 0;
|
||||
PY25Q16_ReadBuffer(FOXHUNT_CALLSIGN_ADDR, call, FOXHUNT_CALLSIGN_MAX);
|
||||
call[FOXHUNT_CALLSIGN_MAX] = '\0';
|
||||
for (uint8_t i = 0; i < FOXHUNT_CALLSIGN_MAX; i++) {
|
||||
char c = call[i];
|
||||
if (c == '\0' || (uint8_t)c == 0xFF)
|
||||
break;
|
||||
if (c >= 'a' && c <= 'z')
|
||||
c -= 32;
|
||||
if ((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '/')
|
||||
foxCall[n++] = c;
|
||||
}
|
||||
foxCall[n] = '\0';
|
||||
}
|
||||
|
||||
// Start in the hunt (RX) sub-mode, S-meter staircase gauge by default, then restore
|
||||
// any persisted settings (attenuator, gauge, audio, beacon idle / fox / tx).
|
||||
// Initialise the full configuration before loading it. The carrier-keying byte was
|
||||
// appended (index 7): an older seven-byte config reads 0xFF there and LoadConfig's
|
||||
// `<= 1` guard rejects it, so it stays at the default below — no migration needed.
|
||||
attStep = 0;
|
||||
foxGraphMode = FOXHUNT_GRAPH_BAR;
|
||||
foxAudioMode = FOXHUNT_AUDIO_OFF;
|
||||
FOXHUNT_LoadConfig(); // may override att / gauge / audio / idle / fox / tx
|
||||
FOXHUNT_EnterHunt(); // applies the restored attStep and audio mode
|
||||
// beaconMsg is (re)built by FOXHUNT_BeaconTransmit before every burst, and nothing
|
||||
// reads it before the first burst, so no need to assemble it here.
|
||||
beaconIdle = FOXHUNT_BEACON_IDLE_DEF;
|
||||
beaconTx = FOXHUNT_BEACON_TX_DEF;
|
||||
foxFox = FOXHUNT_FOX_CALL;
|
||||
beaconCarrierKeyed = false; // TONE (F2A) by default
|
||||
beaconPhaseTx = false;
|
||||
FOXHUNT_LoadConfig();
|
||||
|
||||
// Prime the key state with whatever is held right now, so the side key that
|
||||
// launched Fox Hunt (still down on a long-press assignment) is not taken for
|
||||
// a fresh press and does not immediately toggle to the beacon.
|
||||
// Ignore the key that launched the modal app until it has been released.
|
||||
kbd.prev = kbd.current = KEYBOARD_GetKey();
|
||||
gWasFKeyPressed = false; // start with the reverse-step arm cleared
|
||||
|
||||
// Always enter unlocked: the keypad lock is a transient safety toggle, not a
|
||||
// persisted setting, so a previous session must not leave the pad locked.
|
||||
gWasFKeyPressed = false;
|
||||
foxLocked = false;
|
||||
fHoldMs = 0;
|
||||
fLongDone = false;
|
||||
|
||||
foxRunning = true;
|
||||
}
|
||||
|
||||
static void FOXHUNT_LoadCallsign(void)
|
||||
{
|
||||
char call[FOXHUNT_CALLSIGN_MAX + 1];
|
||||
uint8_t n = 0;
|
||||
|
||||
// Read the callsign from the boot message (line 1), sanitised to what Morse can
|
||||
// send; empty/erased leaves foxCall empty (the CALL id then falls back to bare MOE).
|
||||
PY25Q16_ReadBuffer(SETTINGS_BOOT_MESSAGE_LINE1_ADDR, call, FOXHUNT_CALLSIGN_MAX);
|
||||
call[FOXHUNT_CALLSIGN_MAX] = '\0';
|
||||
for (uint8_t i = 0; i < FOXHUNT_CALLSIGN_MAX; i++) {
|
||||
char c = call[i];
|
||||
if (c == '\0' || (uint8_t)c == 0xFF)
|
||||
break;
|
||||
if (c >= 'a' && c <= 'z')
|
||||
c -= 32;
|
||||
if ((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '/')
|
||||
foxCall[n++] = c;
|
||||
}
|
||||
foxCall[n] = '\0';
|
||||
}
|
||||
|
||||
static void FOXHUNT_End(void)
|
||||
{
|
||||
FOXHUNT_SaveConfig();
|
||||
gWasFKeyPressed = false;
|
||||
AUDIO_AudioPathOff();
|
||||
BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, false);
|
||||
RADIO_SelectVfos();
|
||||
RADIO_SetupRegisters(true);
|
||||
}
|
||||
|
||||
/* Both entry points stay in this translation unit so their small modal services
|
||||
* can be shared when both features are enabled. The action table references only
|
||||
* the enabled entry point(s); --gc-sections then drops the complete unused RX or
|
||||
* Beacon call graph, including its private constants and bitmaps. */
|
||||
void APP_RunFoxHunt(void)
|
||||
{
|
||||
FOXHUNT_Begin();
|
||||
|
||||
FOXHUNT_EnterHunt();
|
||||
while (foxRunning) {
|
||||
#if defined(ENABLE_UART) || defined(ENABLE_USB)
|
||||
UART_ServiceCommands();
|
||||
#endif
|
||||
if (foxBeacon) {
|
||||
FOXHUNT_BeaconTick();
|
||||
continue;
|
||||
}
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
// Keep the K5Viewer link alive and pick up any remote key.
|
||||
K5VIEWER_ParseInput();
|
||||
#endif
|
||||
FOXHUNT_HandleKeys();
|
||||
if (foxBeacon) // just switched to beacon: skip the RX work this tick
|
||||
continue;
|
||||
if (!foxRunning)
|
||||
break;
|
||||
|
||||
curDbm = FOXHUNT_ReadDbm();
|
||||
if (curDbm > peakDbm)
|
||||
@@ -1482,24 +1482,23 @@ void APP_RunFoxHunt(void)
|
||||
FOXHUNT_TickDelay(); // tick delay + smooth backlight fade
|
||||
}
|
||||
|
||||
// Persist the session's settings on the way out (EXIT clears foxRunning) so
|
||||
// they survive a later power cycle.
|
||||
FOXHUNT_SaveConfig();
|
||||
|
||||
gWasFKeyPressed = false; // don't leak a pending reverse-step arm to the main screen
|
||||
|
||||
// Mute any pending tone, drop the PA (safety), then restore the normal VFO
|
||||
// selection and RX config.
|
||||
AUDIO_AudioPathOff();
|
||||
BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, false);
|
||||
RADIO_SelectVfos();
|
||||
RADIO_SetupRegisters(true);
|
||||
FOXHUNT_End();
|
||||
}
|
||||
|
||||
void ACTION_FoxHunt(void)
|
||||
void APP_RunBeacon(void)
|
||||
{
|
||||
APP_RunFoxHunt();
|
||||
GUI_SelectNextDisplay(DISPLAY_MAIN);
|
||||
FOXHUNT_Begin();
|
||||
FOXHUNT_LoadCallsign();
|
||||
FOXHUNT_EnterBeacon();
|
||||
|
||||
while (foxRunning) {
|
||||
#if defined(ENABLE_UART) || defined(ENABLE_USB)
|
||||
UART_ServiceCommands();
|
||||
#endif
|
||||
FOXHUNT_BeaconTick();
|
||||
}
|
||||
|
||||
FOXHUNT_End();
|
||||
}
|
||||
|
||||
#endif // ENABLE_FEAT_F4HWN_FOXHUNT
|
||||
#endif // ENABLE_FEAT_F4HWN_FOXHUNT || ENABLE_FEAT_F4HWN_BEACON
|
||||
+12
-3
@@ -17,7 +17,7 @@
|
||||
#ifndef APP_FOXHUNT_H
|
||||
#define APP_FOXHUNT_H
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_FOXHUNT
|
||||
#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON)
|
||||
|
||||
#include "keyboard_state.h"
|
||||
|
||||
@@ -46,12 +46,21 @@
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#endif
|
||||
|
||||
// Entry point wired as an assignable side-key action (see action.c).
|
||||
#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
|
||||
// Assignable resident/overlay launchers (see action.c).
|
||||
void ACTION_FoxHunt(void);
|
||||
void ACTION_Beacon(void);
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_FOXHUNT
|
||||
// Self-contained modal loop that owns the screen and keypad until EXIT.
|
||||
void APP_RunFoxHunt(void);
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_BEACON
|
||||
void APP_RunBeacon(void);
|
||||
#endif
|
||||
|
||||
#endif // ENABLE_FEAT_F4HWN_FOXHUNT
|
||||
#endif // APP_FOXHUNT_H
|
||||
+7
-7
@@ -20,7 +20,7 @@
|
||||
#include "app/chFrScanner.h"
|
||||
#include "app/common.h"
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
#include "app/fm.h"
|
||||
#endif
|
||||
|
||||
@@ -56,7 +56,7 @@ void GENERIC_Key_F(bool bKeyPressed, bool bKeyHeld)
|
||||
COMMON_KeypadLockToggle();
|
||||
}
|
||||
else { // released
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if ((gFmRadioMode || gScreenToDisplay != DISPLAY_MAIN) && gScreenToDisplay != DISPLAY_FM)
|
||||
return;
|
||||
#else
|
||||
@@ -77,7 +77,7 @@ void GENERIC_Key_F(bool bKeyPressed, bool bKeyHeld)
|
||||
}
|
||||
}
|
||||
else { // short pressed
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (gScreenToDisplay != DISPLAY_FM)
|
||||
#endif
|
||||
{
|
||||
@@ -85,7 +85,7 @@ void GENERIC_Key_F(bool bKeyPressed, bool bKeyHeld)
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (gFM_ScanState == FM_SCAN_OFF) { // not scanning
|
||||
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
|
||||
return;
|
||||
@@ -132,7 +132,7 @@ void GENERIC_Key_PTT(bool bKeyPressed)
|
||||
|
||||
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (gFM_ScanState != FM_SCAN_OFF) { // FM radio is scanning .. stop
|
||||
FM_PlayAndUpdate();
|
||||
#ifdef ENABLE_VOICE
|
||||
@@ -143,7 +143,7 @@ void GENERIC_Key_PTT(bool bKeyPressed)
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (gScreenToDisplay == DISPLAY_FM)
|
||||
goto start_tx; // listening to the FM radio .. start TX'ing
|
||||
#endif
|
||||
@@ -200,7 +200,7 @@ cancel_tx:
|
||||
done:
|
||||
gPttDebounceCounter = 0;
|
||||
if (gScreenToDisplay != DISPLAY_MENU
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
&& gRequestDisplayScreen != DISPLAY_FM
|
||||
#endif
|
||||
) {
|
||||
|
||||
+18
-9
@@ -20,7 +20,7 @@
|
||||
#include "app/app.h"
|
||||
#include "app/chFrScanner.h"
|
||||
#include "app/common.h"
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
#include "app/fm.h"
|
||||
#endif
|
||||
#include "app/generic.h"
|
||||
@@ -31,8 +31,11 @@
|
||||
#include "app/spectrum.h"
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_GAME
|
||||
#include "app/breakout.h"
|
||||
#if defined(ENABLE_FEAT_F4HWN_GAME) && !defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
|
||||
#include "app/breakout.h" // resident game only; the overlay path uses app_menu.h
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_APPS
|
||||
#include "apps/app_menu.h"
|
||||
#endif
|
||||
|
||||
#include "audio.h"
|
||||
@@ -269,9 +272,15 @@ static void processFKeyFunction(const KEY_Code_t Key, const bool beep)
|
||||
break;
|
||||
|
||||
case KEY_7:
|
||||
#ifdef ENABLE_FEAT_F4HWN_GAME
|
||||
// F + 7 opens the overlay-apps menu when that support is built;
|
||||
// otherwise it launches the resident game (GAME); otherwise VOX.
|
||||
#if defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) || defined(ENABLE_FEAT_F4HWN_GAME)
|
||||
if (!beep) {
|
||||
APP_RunBreakout();
|
||||
#if defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
|
||||
APP_MenuOpen(); // overlay-apps selector
|
||||
#else
|
||||
APP_RunBreakout(); // resident game (no overlay support)
|
||||
#endif
|
||||
} else {
|
||||
#endif
|
||||
#ifdef ENABLE_VOX
|
||||
@@ -279,7 +288,7 @@ static void processFKeyFunction(const KEY_Code_t Key, const bool beep)
|
||||
//#else
|
||||
// toggle_chan_scanlist();
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_GAME
|
||||
#if defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) || defined(ENABLE_FEAT_F4HWN_GAME)
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -719,7 +728,7 @@ static void MAIN_Key_EXIT(bool bKeyPressed, bool bKeyHeld)
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (!gFmRadioMode)
|
||||
#endif
|
||||
{
|
||||
@@ -754,7 +763,7 @@ static void MAIN_Key_EXIT(bool bKeyPressed, bool bKeyHeld)
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
ACTION_FM();
|
||||
#endif
|
||||
return;
|
||||
@@ -1055,7 +1064,7 @@ static void MAIN_Key_UP_DOWN(bool bKeyPressed, bool bKeyHeld, int8_t Direction)
|
||||
|
||||
void MAIN_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
|
||||
{
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (gFmRadioMode && Key != KEY_PTT && Key != KEY_EXIT) {
|
||||
if (!bKeyHeld && bKeyPressed)
|
||||
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
|
||||
|
||||
+55
-43
@@ -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;
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
#include "app/spectrum.h"
|
||||
#include "am_fix.h"
|
||||
#include "audio.h"
|
||||
#include "misc.h"
|
||||
|
||||
@@ -608,10 +607,6 @@ uint16_t GetRssi()
|
||||
// Discard first read (AGC may still be transitioning), keep second
|
||||
BK4819_GetRSSI();
|
||||
uint16_t rssi = BK4819_GetRSSI();
|
||||
#ifdef ENABLE_AM_FIX
|
||||
if (settings.modulationType == MODULATION_AM && gSetting_AM_fix)
|
||||
rssi += AM_fix_get_gain_diff() * 2;
|
||||
#endif
|
||||
return rssi;
|
||||
}
|
||||
|
||||
@@ -2474,12 +2469,6 @@ static void Tick()
|
||||
if (gNextTimeslice)
|
||||
{
|
||||
gNextTimeslice = false;
|
||||
#ifdef ENABLE_AM_FIX
|
||||
if (settings.modulationType == MODULATION_AM && !lockAGC)
|
||||
{
|
||||
AM_fix_10ms(vfo); // allow AM_Fix to apply its AGC action
|
||||
}
|
||||
#endif
|
||||
BACKLIGHT_Update();
|
||||
}
|
||||
|
||||
|
||||
+316
-26
@@ -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);
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
/* Copyright 2026 Armel F4HWN
|
||||
* https://github.com/armel
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Overlay-app ABI (POC).
|
||||
*
|
||||
* A leaf, modal "overlay app" is a self-contained code blob stored in the
|
||||
* external SPI flash and copied into the 4 KiB overlay RAM (the PY25Q16 sector
|
||||
* cache) to run, then discarded. It is linked with NOCROSSREFS and must not
|
||||
* reference resident firmware symbols: every service it needs is reached through
|
||||
* this table, which the loader fills and passes to the entry point.
|
||||
*
|
||||
* void app_main(const app_api_t *api); // entry, at blob offset 0
|
||||
*
|
||||
* Both the firmware loader and the app include THIS header. Within one ABI
|
||||
* major, app_api_t is append-only: existing fields may never move, disappear or
|
||||
* change signature. Append a service and bump APP_API_LEVEL; only apps using
|
||||
* that service need the new level. A breaking layout change bumps
|
||||
* APP_ABI_MAJOR and resets APP_API_LEVEL to 1.
|
||||
*/
|
||||
|
||||
#ifndef APPS_APP_API_H
|
||||
#define APPS_APP_API_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
/* First unpublished/public baseline: all services currently present below are
|
||||
* ABI major 1, API level 1. */
|
||||
#define APP_ABI_MAJOR 1u
|
||||
#define APP_API_LEVEL 1u
|
||||
|
||||
/* KEY codes mirrored from driver/keyboard.h (enum KEY_Code_e). Kept in sync by
|
||||
* value so the app stays independent of the firmware headers. */
|
||||
enum {
|
||||
APP_KEY_0 = 0,
|
||||
APP_KEY_1 = 1,
|
||||
APP_KEY_2 = 2,
|
||||
APP_KEY_3 = 3,
|
||||
APP_KEY_4 = 4,
|
||||
APP_KEY_5 = 5,
|
||||
APP_KEY_6 = 6,
|
||||
APP_KEY_7 = 7,
|
||||
APP_KEY_8 = 8,
|
||||
APP_KEY_9 = 9,
|
||||
APP_KEY_MENU = 10,
|
||||
APP_KEY_UP = 11,
|
||||
APP_KEY_DOWN = 12,
|
||||
APP_KEY_EXIT = 13,
|
||||
APP_KEY_STAR = 14,
|
||||
APP_KEY_F = 15,
|
||||
APP_KEY_PTT = 16,
|
||||
APP_KEY_INVALID = 19,
|
||||
APP_KEY_SAVER = 0xFD, /* virtual: saver active, skip this app frame */
|
||||
APP_KEY_WAKE = 0xFE, /* virtual: saver dismissed, redraw without action */
|
||||
};
|
||||
|
||||
/* The framebuffer is the resident gFrameBuffer[FRAME_LINES][128]; the app draws
|
||||
* into it and calls a blit_* to push it to the LCD. */
|
||||
typedef uint8_t (*app_fb_t)[128];
|
||||
|
||||
/* Broadcast FM shared state (mirrors gEeprom.FM_*), read/written via fm_state. */
|
||||
#define APP_FM_CH_MAX 48
|
||||
typedef struct {
|
||||
uint16_t freq_playing; /* current tuned frequency (0.1 MHz) */
|
||||
uint16_t sel_freq; /* last VFO frequency */
|
||||
uint8_t band; /* 0..3 */
|
||||
uint8_t is_mr; /* memory mode */
|
||||
uint8_t sel_ch; /* selected memory channel 0..47 */
|
||||
} app_fm_state_t;
|
||||
|
||||
/* Compact, pointer-free description of one receiver in the resident triple-VFO
|
||||
* service. Overlay apps must never see VFO_Info_t directly: its layout varies
|
||||
* with firmware features and contains resident pointers. */
|
||||
typedef struct {
|
||||
uint32_t frequency; /* RX frequency, x10 Hz */
|
||||
uint16_t channel; /* memory channel, zero based */
|
||||
uint16_t step; /* step, 10 Hz units */
|
||||
uint16_t code_value; /* CTCSS x0.1 Hz or DCS octal source value */
|
||||
int16_t rssi_dbm; /* last/current corrected RSSI */
|
||||
uint8_t modulation;
|
||||
uint8_t power;
|
||||
uint8_t bandwidth;
|
||||
uint8_t code_type;
|
||||
uint8_t code;
|
||||
uint8_t offset_direction;
|
||||
uint8_t reverse;
|
||||
uint8_t squelch;
|
||||
uint8_t flags; /* APP_TRIVFO_* below */
|
||||
char name[11]; /* channel name, trimmed and NUL terminated */
|
||||
} app_trivfo_info_t;
|
||||
|
||||
enum {
|
||||
APP_TRIVFO_SELECTED = 1u << 0,
|
||||
APP_TRIVFO_TUNED = 1u << 1,
|
||||
APP_TRIVFO_RECEIVING = 1u << 2,
|
||||
APP_TRIVFO_TX = 1u << 3,
|
||||
APP_TRIVFO_USER_POWER = 1u << 4,
|
||||
APP_TRIVFO_AUDIO_BAR = 1u << 5,
|
||||
APP_TRIVFO_GUI_CLASSIC = 1u << 6,
|
||||
APP_TRIVFO_PTT_ONEPUSH = 1u << 7,
|
||||
};
|
||||
|
||||
enum {
|
||||
APP_TRIVFO_SCAN = 0,
|
||||
APP_TRIVFO_RX = 1,
|
||||
APP_TRIVFO_HOLD = 2,
|
||||
APP_TRIVFO_TX_STATE = 3,
|
||||
};
|
||||
|
||||
/* Pointer-free BEAM channel description. The resident bridge translates this
|
||||
* stable ABI type to/from feature-dependent VFO_Info_t. */
|
||||
typedef struct {
|
||||
uint32_t rx_frequency;
|
||||
uint32_t tx_offset_frequency;
|
||||
uint8_t rx_code;
|
||||
uint8_t tx_code;
|
||||
uint8_t rx_codetype;
|
||||
uint8_t tx_codetype;
|
||||
uint8_t modulation;
|
||||
uint8_t tx_offset_direction;
|
||||
uint8_t tx_lock;
|
||||
uint8_t busy_channel_lock;
|
||||
uint8_t output_power;
|
||||
uint8_t channel_bandwidth;
|
||||
uint8_t frequency_reverse;
|
||||
uint8_t dtmf_ptt_id_mode;
|
||||
uint8_t dtmf_decoding_enable;
|
||||
uint8_t step_setting;
|
||||
uint8_t scrambling_type;
|
||||
uint8_t band;
|
||||
uint8_t scanlist;
|
||||
uint8_t compander;
|
||||
char name[16]; /* fixed-width wire field; NUL termination is not required */
|
||||
} app_beam_channel_t;
|
||||
|
||||
_Static_assert(sizeof(app_beam_channel_t) == 44u,
|
||||
"BEAM ABI/wire channel layout changed");
|
||||
|
||||
enum {
|
||||
APP_BEAM_RX_WAIT = 0,
|
||||
APP_BEAM_RX_READY = 1,
|
||||
APP_BEAM_RX_ERROR = 2,
|
||||
};
|
||||
|
||||
/* CRITICAL PERSISTENCE RULE
|
||||
*
|
||||
* The app executes from the RAM buffer also used as PY25Q16's sector cache.
|
||||
* Therefore no API callback may erase or write external flash while app_main()
|
||||
* is running: a read-modify-write would overwrite the executing app. Services
|
||||
* such as cfg_save, fm_commit and beam_save must only stage resident RAM state;
|
||||
* APP_LaunchOverlay commits it after app_main() returns. */
|
||||
|
||||
typedef struct app_api {
|
||||
/* Fixed four-byte prefix; services remain naturally pointer-aligned. */
|
||||
uint8_t abi_major; /* == APP_ABI_MAJOR */
|
||||
uint8_t api_level; /* == APP_API_LEVEL */
|
||||
uint16_t api_size; /* sizeof(app_api_t), for optional probing */
|
||||
|
||||
app_fb_t fb; /* -> gFrameBuffer */
|
||||
|
||||
/* ---- display ---- */
|
||||
void (*display_clear)(void); /* UI_DisplayClear */
|
||||
void (*status_clear)(void); /* UI_StatusClear */
|
||||
void (*draw_line)(app_fb_t fb, int16_t x1, int16_t y1,
|
||||
int16_t x2, int16_t y2, bool black); /* UI_DrawLineBuffer */
|
||||
void (*draw_rect)(app_fb_t fb, int16_t x1, int16_t y1,
|
||||
int16_t x2, int16_t y2, bool black); /* UI_DrawRectangleBuffer */
|
||||
void (*print_bold)(const char *s, uint8_t start,
|
||||
uint8_t end, uint8_t line); /* UI_PrintStringSmallBold */
|
||||
void (*print_tiny)(const char *s, uint8_t x, uint8_t y,
|
||||
bool statusbar, bool fill); /* GUI_DisplaySmallest */
|
||||
void (*blit_full)(void); /* ST7565_BlitFullScreen */
|
||||
void (*blit_line)(unsigned line); /* ST7565_BlitLine */
|
||||
void (*blit_status)(void); /* ST7565_BlitStatusLine */
|
||||
|
||||
/* ---- input / system ---- */
|
||||
uint8_t (*get_key)(void); /* KEYBOARD_GetKey, returns an APP_KEY_* code */
|
||||
void (*delay_ms)(uint32_t ms); /* SYSTEM_DelayMs */
|
||||
/* ---- audio / indicator ---- */
|
||||
void (*play_tone)(uint16_t tone, uint16_t ms); /* full BK4819 tone burst + AF path */
|
||||
void (*led)(bool on); /* green GPIO indicator */
|
||||
|
||||
/* ---- extra text drawing ---- */
|
||||
void (*print_normal)(const char *s, uint8_t start, uint8_t end, uint8_t line); /* UI_PrintStringSmallNormal */
|
||||
void (*print_inverse)(const char *s, uint8_t x, uint8_t line,
|
||||
bool statusbar, bool fill, uint8_t endX); /* GUI_DisplaySmallestInverse */
|
||||
void (*display_freq)(const char *s, uint8_t x, uint8_t y, bool statusbar); /* UI_DisplayFrequency (big font) */
|
||||
|
||||
/* ---- BK4819 radio access ---- */
|
||||
int16_t (*rssi_dbm)(void); /* corrected RSSI of the RX VFO, dBm */
|
||||
uint16_t (*bk_read)(uint8_t reg); /* BK4819_ReadRegister */
|
||||
void (*bk_write)(uint8_t reg, uint16_t v);/* BK4819_WriteRegister */
|
||||
void (*set_agc)(bool on); /* BK4819_SetAGC */
|
||||
void (*set_af)(uint8_t mode); /* BK4819_SetAF (APP_AF_* below) */
|
||||
void (*audio_path)(bool on); /* AUDIO_AudioPathOn/Off */
|
||||
void (*prepare_tone)(void); /* BK4819_PrepareToPlayTone(true) */
|
||||
void (*play_tone_raw)(uint16_t hz, uint16_t ms); /* BK4819_PlayToneRaw */
|
||||
void (*tones_off_rx)(void); /* BK4819_TurnsOffTones_TurnsOnRX */
|
||||
uint32_t (*rx_freq)(void); /* current RX VFO frequency (x10 Hz) */
|
||||
|
||||
/* ---- config persistence (deferred: staged now, committed on exit) ---- */
|
||||
void (*cfg_load)(uint8_t *buf, uint8_t len); /* read the app's saved config bytes */
|
||||
void (*cfg_save)(const uint8_t *buf, uint8_t len); /* stage bytes; resident commits after the app returns */
|
||||
|
||||
/* ---- battery + backlight ---- */
|
||||
void (*draw_battery)(void); /* UI_DrawStatusBattery into the status line */
|
||||
void (*battery_sample)(void); /* periodic ADC sample so the level stays live */
|
||||
void (*backlight_on)(void); /* BACKLIGHT_TurnOn */
|
||||
void (*backlight_update)(void); /* resident fade + BLTime service */
|
||||
void (*audio_scope)(uint8_t line, bool active); /* shared MAIN microphone scope */
|
||||
|
||||
uint8_t *status_line; /* -> gStatusLine (for status-bar icons) */
|
||||
|
||||
/* ---- TX (beacon) ---- */
|
||||
uint8_t (*tx_state)(void); /* 0 = OK to transmit, else a denial code */
|
||||
void (*tx_set_params)(void); /* RADIO_SetTxParameters (key up: carrier+PA) */
|
||||
void (*tx_tone)(uint16_t hz);/* BK4819_TransmitTone prime (MCW tone) */
|
||||
void (*tx_mute)(bool on); /* key the tone on(false)/off(true) via TxMute */
|
||||
void (*tx_end)(void); /* PA off + RADIO_SetupRegisters (back to RX) */
|
||||
void (*tx_carrier)(bool on); /* gate the PA on/off (beacon CARR carrier keying) */
|
||||
uint32_t (*tx_freq)(void); /* current TX VFO frequency (x10 Hz) */
|
||||
void (*boot_callsign)(char *buf, uint8_t len); /* sanitised callsign from the boot message */
|
||||
void (*print_string)(const char *s, uint8_t start, uint8_t end,
|
||||
uint8_t line, uint8_t width); /* UI_PrintString (big font) */
|
||||
|
||||
/* ---- broadcast FM (BK1080), sovereign: no BK4819 dual-watch ---- */
|
||||
void (*fm_enter)(uint16_t freq, uint8_t band); /* BK1080_Init + antenna filter + audio on */
|
||||
void (*fm_exit)(void); /* audio off + BK1080_Init0 + restore filter */
|
||||
void (*fm_set_freq)(uint16_t freq, uint8_t band);/* BK1080_SetFrequency (freq in 0.1 MHz) */
|
||||
uint16_t (*fm_lo)(uint8_t band); /* band low limit (0.1 MHz) */
|
||||
uint16_t (*fm_hi)(uint8_t band); /* band high limit (0.1 MHz) */
|
||||
void (*fm_mute)(bool mute); /* BK1080_Mute */
|
||||
int8_t (*fm_valid)(uint16_t freq, uint16_t lo); /* FM_CheckFrequencyLock: 0 = station */
|
||||
uint16_t *fm_channels; /* -> gFM_Channels[APP_FM_CH_MAX], shared RAM r/w */
|
||||
void (*fm_state)(app_fm_state_t *s, bool write);/* read/write the resident gEeprom.FM_* */
|
||||
void (*fm_commit)(void); /* deferred SETTINGS_SaveFM (config + channels) */
|
||||
|
||||
/* ---- navigation (API level 1 baseline) ----
|
||||
* Convert a raw APP_KEY_UP/DOWN into a semantic value direction:
|
||||
* UV-K5 UP/DOWN -> +1/-1
|
||||
* UV-K1 LEFT/RIGHT -> -1/+1
|
||||
* Returns 0 for any other key. Keep get_key() raw for spatial controls. */
|
||||
int8_t (*nav_dir)(uint8_t key);
|
||||
|
||||
/* ---- triple VFO (optional resident capability APP_CAP_TRIVFO) ----
|
||||
* A and B are the live Main Display VFOs. C is a resident temporary VFO
|
||||
* loaded from c_channel (or the first valid memory after B when invalid).
|
||||
* tick is called every 20 ms by the app and returns APP_TRIVFO_* state. */
|
||||
uint16_t (*trivfo_enter)(uint16_t c_channel);
|
||||
void (*trivfo_leave)(void);
|
||||
void (*trivfo_get)(uint8_t vfo, app_trivfo_info_t *info);
|
||||
void (*trivfo_select)(uint8_t vfo);
|
||||
uint16_t (*trivfo_step)(uint8_t vfo, int8_t direction);
|
||||
uint8_t (*trivfo_tick)(void);
|
||||
uint8_t (*trivfo_ptt)(bool pressed); /* physical PTT edge; resident applies SetPTT */
|
||||
|
||||
/* ---- BEAM channel transfer (optional resident capability APP_CAP_BEAM) ---- */
|
||||
void (*beam_prepare)(void); /* tune the fixed narrow-band FSK channel */
|
||||
void (*beam_leave)(void); /* defensively stop FSK before app return */
|
||||
void (*beam_get)(app_beam_channel_t *channel); /* export selected VFO */
|
||||
uint16_t (*beam_save)(const app_beam_channel_t *channel); /* first free MR, or 0xffff */
|
||||
void (*beam_send)(uint16_t *packet); /* transmit one 36-word FSK packet */
|
||||
void (*beam_rx)(bool start); /* arm or stop FSK reception */
|
||||
uint8_t (*beam_rx_poll)(uint16_t *packet); /* APP_BEAM_RX_* */
|
||||
void (*beam_draw)(const char *status); /* MAIN display with one BEAM center line */
|
||||
} app_api_t;
|
||||
|
||||
/* BK4819 AF modes for set_af (mirror driver/bk4819.h values). */
|
||||
enum { APP_AF_MUTE = 0, APP_AF_FM = 1, APP_AF_AM = 7 };
|
||||
|
||||
/* Register ids the apps use (mirror driver/bk4819-regs.h). */
|
||||
enum { APP_BK_REG_13 = 0x13 };
|
||||
|
||||
/* Entry point every app blob exports, placed at blob offset 0. */
|
||||
typedef void (*app_entry_t)(const app_api_t *api);
|
||||
|
||||
#endif /* APPS_APP_API_H */
|
||||
@@ -0,0 +1,335 @@
|
||||
/* Copyright 2026 Armel F4HWN
|
||||
* https://github.com/armel
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "apps/app_overlay.h"
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_APPS
|
||||
|
||||
#include <string.h>
|
||||
#include "apps/app_menu.h"
|
||||
#include "app/app.h"
|
||||
#include "driver/backlight.h"
|
||||
#include "driver/st7565.h"
|
||||
#include "driver/keyboard.h"
|
||||
#include "driver/system.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "ui/helper.h"
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
#include "k5viewer.h"
|
||||
#endif
|
||||
|
||||
/* "F4HWN APPS" banner and the same thin separator used by the multiboot
|
||||
* selector. Bit 3 leaves room for the selected-row capsule's top edge. */
|
||||
static void app_status_bar(void)
|
||||
{
|
||||
UI_StatusClear();
|
||||
GUI_DisplaySmallestInverse("F4HWN APPS", 44, 0, true, true, 84);
|
||||
|
||||
for (uint8_t x = 2u; x < LCD_WIDTH - 2u; x++)
|
||||
gFrameBuffer[0][x] |= 0x08u;
|
||||
}
|
||||
|
||||
/* Bottom key hints, matching the multiboot selector. */
|
||||
static void app_key_hints(void)
|
||||
{
|
||||
const uint8_t sp = 6u;
|
||||
const char *act_exit = "QUIT";
|
||||
const uint8_t ae = (uint8_t)strlen(act_exit);
|
||||
const uint8_t xm = 4u;
|
||||
const uint8_t xe = (uint8_t)(124u - ae * 4u - sp - 16u);
|
||||
|
||||
GUI_DisplaySmallestInverse("MENU", xm, 6, false, true, (uint8_t)(xm + 16u));
|
||||
GUI_DisplaySmallest("RUN", (uint8_t)(xm + 16u + sp), 49, false, true);
|
||||
|
||||
GUI_DisplaySmallestInverse("EXIT", xe, 6, false, true, (uint8_t)(xe + 16u));
|
||||
GUI_DisplaySmallest(act_exit, (uint8_t)(xe + 16u + sp), 49, false, true);
|
||||
}
|
||||
|
||||
/* Fixed selection capsule around the primary information (the app name).
|
||||
* Slot number stays in the normal font; size is plain 3x5 metadata. */
|
||||
#define APP_NAME_BOX_START 12u
|
||||
#define APP_NAME_BOX_END 102u
|
||||
#define APP_NAME_TEXT_X 14u
|
||||
|
||||
static void app_wait_release(void);
|
||||
|
||||
static void app_invert_name(uint8_t line)
|
||||
{
|
||||
gFrameBuffer[line][APP_NAME_BOX_START] ^= 0x7Fu;
|
||||
for (uint8_t x = APP_NAME_BOX_START + 1u; x < APP_NAME_BOX_END; x++)
|
||||
{
|
||||
gFrameBuffer[line][x] ^= 0xFFu;
|
||||
gFrameBuffer[line - 1u][x] ^= 0x80u;
|
||||
}
|
||||
gFrameBuffer[line][APP_NAME_BOX_END] ^= 0x7Fu;
|
||||
}
|
||||
|
||||
/* Debounced blocking key read, then wait for release (mirrors mb_get_key). */
|
||||
static KEY_Code_t app_get_key(void)
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
/* APP_MenuOpen() is modal and does not return to APP_Update(). Keep
|
||||
* serial key injection and the viewer connection alive while waiting. */
|
||||
K5VIEWER_ParseInput();
|
||||
#endif
|
||||
APP_ModalBacklightTick(true);
|
||||
|
||||
if (APP_IsScreenSaverDisplayed())
|
||||
{
|
||||
if (KEYBOARD_GetKey() != KEY_INVALID)
|
||||
{
|
||||
APP_ModalScreenSaverExit();
|
||||
BACKLIGHT_TurnOn();
|
||||
app_wait_release();
|
||||
return KEY_INVALID;
|
||||
}
|
||||
|
||||
SYSTEM_DelayMs(10);
|
||||
continue;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
K5VIEWER_Update(false);
|
||||
#endif
|
||||
KEY_Code_t key = KEYBOARD_Poll();
|
||||
if (key != KEY_INVALID)
|
||||
{
|
||||
SYSTEM_DelayMs(30);
|
||||
if (KEYBOARD_Poll() == key)
|
||||
{
|
||||
BACKLIGHT_TurnOn();
|
||||
while (KEYBOARD_Poll() != KEY_INVALID)
|
||||
{
|
||||
SYSTEM_DelayMs(10);
|
||||
APP_ModalBacklightTick(false);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
}
|
||||
SYSTEM_DelayMs(10);
|
||||
}
|
||||
}
|
||||
|
||||
static void app_wait_release(void)
|
||||
{
|
||||
uint8_t stable = 0;
|
||||
while (stable < 10u)
|
||||
{
|
||||
if (!GPIO_IsPttPressed() && KEYBOARD_Poll() == KEY_INVALID)
|
||||
stable++;
|
||||
else
|
||||
stable = 0;
|
||||
SYSTEM_DelayMs(10);
|
||||
APP_ModalBacklightTick(true);
|
||||
}
|
||||
}
|
||||
|
||||
static void app_copy(char *dst, uint8_t cap, const char *src, uint8_t src_cap)
|
||||
{
|
||||
uint8_t n = 0;
|
||||
while (n + 1u < cap && n < src_cap && src[n])
|
||||
{
|
||||
dst[n] = src[n];
|
||||
n++;
|
||||
}
|
||||
dst[n] = '\0';
|
||||
}
|
||||
|
||||
/* Display the code payload rounded up to 0.1 KiB, so the compact value never
|
||||
* understates the space occupied by the app. The space before "KB" is omitted
|
||||
* because this secondary value is rendered in the tiny 3x5 font. */
|
||||
static void app_format_size(char out[6], uint32_t bytes)
|
||||
{
|
||||
if (bytes > APP_OVERLAY_MAX)
|
||||
{
|
||||
memcpy(out, "--KB", 5u);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint16_t tenths = (uint16_t)((bytes * 10u + 1023u) / 1024u);
|
||||
out[0] = (char)('0' + tenths / 10u);
|
||||
out[1] = '.';
|
||||
out[2] = (char)('0' + tenths % 10u);
|
||||
out[3] = 'K';
|
||||
out[4] = 'B';
|
||||
out[5] = '\0';
|
||||
}
|
||||
|
||||
/* Human-readable action for an APP_LaunchOverlay / APP_ValidateSlot failure. */
|
||||
static const char *app_err_text(const app_header_t *header, uint8_t rc)
|
||||
{
|
||||
switch (rc)
|
||||
{
|
||||
case APP_ERR_SLOT: return "BAD SLOT";
|
||||
case APP_ERR_MAGIC: return "NO APP";
|
||||
case APP_ERR_ABI:
|
||||
/* api_min == 0 also identifies pre-reset development blobs whose
|
||||
* former uint16_t ABI value occupies these two bytes. */
|
||||
if (header->api_min == 0u || header->abi_major < APP_ABI_MAJOR)
|
||||
return "UPDATE APP";
|
||||
return "UPDATE FIRMWARE";
|
||||
case APP_ERR_NOT_COMMITTED: return "REINSTALL APP";
|
||||
case APP_ERR_SIZE: return "UPDATE APP";
|
||||
case APP_ERR_CRC: return "REINSTALL APP";
|
||||
case APP_ERR_VMA: return "UPDATE APP";
|
||||
case APP_ERR_AUTH: return "AUTH";
|
||||
case APP_ERR_CAP: return "NOT SUPPORTED";
|
||||
default: return "ERROR";
|
||||
}
|
||||
}
|
||||
|
||||
/* A launch failed: name the app and the action to take, then wait for a key. Without this
|
||||
* an incompatible app would silently "do nothing" when selected. */
|
||||
static void app_show_error(const app_header_t *header, uint8_t rc)
|
||||
{
|
||||
char nm[19];
|
||||
app_copy(nm, sizeof(nm), header->name, APP_NAME_LEN);
|
||||
|
||||
UI_DisplayClear();
|
||||
UI_StatusClear();
|
||||
GUI_DisplaySmallestInverse("APP ERROR", 46, 0, true, true, 82);
|
||||
UI_PrintStringSmallNormal(nm, 2, 0, 2); /* which app */
|
||||
UI_PrintStringSmallNormal(app_err_text(header, rc), 2, 0, 4); /* action */
|
||||
UI_PrintStringSmallNormal("Press any key", 2, 0, 6);
|
||||
ST7565_BlitStatusLine();
|
||||
ST7565_BlitFullScreen();
|
||||
|
||||
app_get_key(); /* blocking: dismiss on any key */
|
||||
}
|
||||
|
||||
/* Five visible slots; line 0 holds the separator and line 6 the key hints. */
|
||||
#define APP_MENU_ROWS 5u
|
||||
#define APP_MENU_SLOT_COUNT 8u
|
||||
|
||||
_Static_assert(APP_MENU_SLOT_COUNT <= APP_SLOT_COUNT,
|
||||
"APP_MENU_SLOT_COUNT exceeds the physical app slot count");
|
||||
|
||||
void APP_MenuOpen(void)
|
||||
{
|
||||
APP_ModalScreenSaverExit();
|
||||
BACKLIGHT_TurnOn();
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
/* Detach the modal selector from the key state that triggered F+7. The
|
||||
* normal K5Viewer updater suppresses frames while a key is held; without
|
||||
* clearing this stale state, the selector could never publish its first
|
||||
* frame to the viewer. */
|
||||
gKeyReading0 = KEY_INVALID;
|
||||
gKeyReading1 = KEY_INVALID;
|
||||
#endif
|
||||
|
||||
/* Apps are installed from UV Studio (0x073x) into physical slots 0..N-1,
|
||||
* shown here as 1..N. Keep empty slots in the list so their location is
|
||||
* visible and selectable while scrolling. */
|
||||
app_header_t hdr[APP_MENU_SLOT_COUNT];
|
||||
bool installed[APP_MENU_SLOT_COUNT];
|
||||
|
||||
for (uint8_t slot = 0; slot < APP_MENU_SLOT_COUNT; slot++)
|
||||
{
|
||||
installed[slot] = APP_SlotInfo(slot, &hdr[slot]) == APP_OK &&
|
||||
(hdr[slot].flags & APP_FLAG_COMMITTED);
|
||||
}
|
||||
|
||||
/* Remember the physical slot and scrolling window across menu openings. */
|
||||
static uint8_t sel = 0;
|
||||
static uint8_t top = 0; /* first visible row of the scrolling window */
|
||||
if (sel >= APP_MENU_SLOT_COUNT || top > APP_MENU_SLOT_COUNT - APP_MENU_ROWS)
|
||||
sel = top = 0u;
|
||||
app_wait_release();
|
||||
|
||||
for (;;)
|
||||
{
|
||||
UI_DisplayClear();
|
||||
app_status_bar(); /* also wipes the VFO status line (DW, battery, ...) */
|
||||
|
||||
/* Slide [top, top+APP_MENU_ROWS) so it always contains the selection. */
|
||||
if (sel < top)
|
||||
top = sel;
|
||||
else if (sel >= (uint8_t)(top + APP_MENU_ROWS))
|
||||
top = (uint8_t)(sel - APP_MENU_ROWS + 1u);
|
||||
|
||||
for (uint8_t slot = top;
|
||||
slot < APP_MENU_SLOT_COUNT && (uint8_t)(slot - top) < APP_MENU_ROWS;
|
||||
slot++)
|
||||
{
|
||||
char number[2];
|
||||
char name[14];
|
||||
char size[6];
|
||||
const uint8_t visible_number = (uint8_t)(slot + 1u);
|
||||
const uint8_t fbLine = (uint8_t)(slot - top + 1u);
|
||||
|
||||
number[0] = (char)('0' + visible_number);
|
||||
number[1] = '\0';
|
||||
|
||||
UI_PrintStringSmallNormal(number, 2u, 0, fbLine);
|
||||
if (installed[slot])
|
||||
{
|
||||
app_copy(name, sizeof(name), hdr[slot].name, APP_NAME_LEN);
|
||||
app_format_size(size, hdr[slot].code_size);
|
||||
UI_PrintStringSmallNormal(name, APP_NAME_TEXT_X, 0, fbLine);
|
||||
GUI_DisplaySmallest(size,
|
||||
(uint8_t)(LCD_WIDTH - 2u - strlen(size) * 4u),
|
||||
(uint8_t)(fbLine * 8u + 1u), false, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
UI_PrintStringSmallNormal("Empty", APP_NAME_TEXT_X, 0, fbLine);
|
||||
}
|
||||
|
||||
if (slot == sel)
|
||||
app_invert_name(fbLine);
|
||||
}
|
||||
|
||||
app_key_hints();
|
||||
|
||||
ST7565_BlitStatusLine();
|
||||
ST7565_BlitFullScreen();
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
K5VIEWER_Update(false);
|
||||
#endif
|
||||
|
||||
const KEY_Code_t key = app_get_key();
|
||||
if (key == KEY_EXIT)
|
||||
return;
|
||||
|
||||
switch (key)
|
||||
{
|
||||
case KEY_UP:
|
||||
sel = (sel == 0u) ? (uint8_t)(APP_MENU_SLOT_COUNT - 1u) : (uint8_t)(sel - 1u);
|
||||
break;
|
||||
case KEY_DOWN:
|
||||
sel = (uint8_t)((sel + 1u) % APP_MENU_SLOT_COUNT);
|
||||
break;
|
||||
case KEY_MENU:
|
||||
{
|
||||
if (!installed[sel])
|
||||
break;
|
||||
|
||||
const uint8_t rc = APP_LaunchOverlay(sel); /* runs until the app exits */
|
||||
if (rc != APP_OK)
|
||||
app_show_error(&hdr[sel], rc); /* no longer silent */
|
||||
app_wait_release();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* ENABLE_FEAT_F4HWN_OVERLAY_APPS */
|
||||
@@ -1,6 +1,5 @@
|
||||
|
||||
/* Copyright 2023 OneOfEleven
|
||||
* https://github.com/DualTachyon
|
||||
/* Copyright 2026 Armel F4HWN
|
||||
* https://github.com/armel
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -15,21 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef AM_FIXH
|
||||
#ifndef APPS_APP_MENU_H
|
||||
#define APPS_APP_MENU_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
/* Blocking "Apps" selector: scans the overlay-app slots, lists the committed
|
||||
* ones by name, and launches the chosen one. UP/DOWN move, MENU launches, EXIT
|
||||
* closes. Returns when the user leaves. */
|
||||
void APP_MenuOpen(void);
|
||||
|
||||
#ifdef ENABLE_AM_FIX
|
||||
void AM_fix_init(void);
|
||||
void AM_fix_reset(const unsigned vfo);
|
||||
void AM_fix_10ms(const unsigned vfo);
|
||||
#ifdef ENABLE_AM_FIX_SHOW_DATA
|
||||
void AM_fix_print_data(const unsigned vfo, char *s);
|
||||
#endif
|
||||
int8_t AM_fix_get_gain_diff();
|
||||
void AM_fix_enable(bool on);
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
#endif /* APPS_APP_MENU_H */
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,135 @@
|
||||
/* Copyright 2026 Armel F4HWN
|
||||
* https://github.com/armel
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Overlay-app loader (POC).
|
||||
*
|
||||
* A leaf, modal app is a self-contained blob held in the external SPI flash. To
|
||||
* run it the loader validates the header + code CRC, copies the code into the
|
||||
* 4 KiB overlay (the PY25Q16 sector cache, shared VMA with the multiboot RAM
|
||||
* stub), and calls its entry point. The blob runs from RAM and reaches every
|
||||
* resident service through app_api_t. The loader never touches the internal
|
||||
* application flash, so a corrupt or incompatible blob is only refused (header/CRC
|
||||
* mismatch) or crashes into a watchdog reset.
|
||||
*
|
||||
* This is NOT a security sandbox. The CRC is an integrity check, not authentication,
|
||||
* and a launched app is TRUSTED NATIVE CODE: it runs privileged with full access to
|
||||
* memory and peripherals (no MPU, no crypto auth), so a malicious or buggy app CAN
|
||||
* brick the radio - e.g. by driving the internal-flash controller itself. Only
|
||||
* install apps you trust.
|
||||
*
|
||||
* External-flash "Apps" region (carved from the free space in the PY25Q16 map,
|
||||
* after the multiboot markers, before the RX/TX log):
|
||||
*
|
||||
* 0x102000 slot 0 ] 16 slots x 8 KiB = 128 KiB
|
||||
* 0x104000 slot 1 ] each slot: 4 KiB header sector + 4 KiB code
|
||||
* ... ]
|
||||
* 0x120000 slot 15 ]
|
||||
*
|
||||
* Host tooling (APP_SlotErase/Write/Info) touches the external flash only and is
|
||||
* never brick-critical; a bad slot is simply refused at launch by the CRC check.
|
||||
*/
|
||||
|
||||
#ifndef APPS_APP_OVERLAY_H
|
||||
#define APPS_APP_OVERLAY_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include "app_api.h"
|
||||
|
||||
/* ---- external-flash Apps region ---- */
|
||||
#define APP_REGION_BASE 0x00102000u /* first app slot */
|
||||
#define APP_SLOT_STRIDE 0x00002000u /* 8 KiB per slot */
|
||||
#define APP_CODE_OFFSET 0x00001000u /* code starts after the 4 KiB header sector */
|
||||
#define APP_SECTOR_SIZE 0x00001000u /* external NOR erase granularity */
|
||||
#define APP_SLOT_COUNT 16u
|
||||
#define APP_SLOT_BASE(s) (APP_REGION_BASE + (uint32_t)(s) * APP_SLOT_STRIDE)
|
||||
|
||||
/* ---- overlay RAM budget: the PY25Q16 4 KiB sector cache reused as workspace.
|
||||
* The link VMA itself is pinned in Core/py32f071xb.ld (ORIGIN+0x280) and passed
|
||||
* via compile-app.sh; the loader checks each blob's link_vma against it. ---- */
|
||||
#define APP_OVERLAY_MAX 0x00001000u /* 4 KiB */
|
||||
|
||||
/* ---- blob header (64 bytes, little-endian; see App/apps/pack_app.py) ---- */
|
||||
#define APP_MAGIC 0x31504146u /* "FAP1" */
|
||||
#define APP_HDR_VERSION 1u
|
||||
#define APP_FLAG_COMMITTED 0x0001u
|
||||
#define APP_FLAG_SCREEN_SAVER 0x0002u
|
||||
#define APP_FLAG_SHORTCUT_SHIFT 8u
|
||||
#define APP_FLAG_SHORTCUT_MASK 0x0F00u
|
||||
#define APP_NAME_LEN 16
|
||||
#define APP_VERSION_LEN 16
|
||||
|
||||
#define APP_SHORTCUT_FM 0x01u
|
||||
#define APP_SHORTCUT_FOXHUNT 0x02u
|
||||
#define APP_SHORTCUT_BEACON 0x04u
|
||||
#define APP_SHORTCUT_BEAM 0x08u
|
||||
|
||||
/* Optional resident facilities an app may require. Requirements live in the
|
||||
* previously reserved header bytes, so app_header_t remains 64 bytes. */
|
||||
#define APP_CAP_FM 0x00000001u
|
||||
#define APP_CAP_TRIVFO 0x00000002u
|
||||
#define APP_CAP_BEAM 0x00000004u
|
||||
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint32_t magic; /* APP_MAGIC */
|
||||
uint16_t hdr_version; /* APP_HDR_VERSION */
|
||||
uint8_t abi_major; /* required ABI family */
|
||||
uint8_t api_min; /* minimum append-only API level */
|
||||
uint32_t code_size; /* bytes of code, <= APP_OVERLAY_MAX */
|
||||
uint32_t code_crc32; /* CRC-32 (zlib) over code_size bytes */
|
||||
uint16_t entry_off; /* entry offset within the code (0) */
|
||||
uint16_t flags; /* APP_FLAG_COMMITTED, ... */
|
||||
char name[APP_NAME_LEN]; /* human-readable, NUL-terminated */
|
||||
char version[APP_VERSION_LEN];/* app version string */
|
||||
uint32_t link_vma; /* RAM VMA the code was linked at */
|
||||
uint32_t required_caps; /* APP_CAP_* required by this app */
|
||||
uint8_t reserved[4]; /* pad to 64 bytes */
|
||||
} app_header_t;
|
||||
|
||||
enum {
|
||||
APP_OK = 0,
|
||||
APP_ERR_SLOT, /* slot index out of range */
|
||||
APP_ERR_MAGIC, /* no/invalid header */
|
||||
APP_ERR_ABI, /* ABI family/API level mismatch */
|
||||
APP_ERR_NOT_COMMITTED, /* image not marked complete */
|
||||
APP_ERR_SIZE, /* code_size out of range */
|
||||
APP_ERR_CRC, /* code CRC-32 mismatch */
|
||||
APP_ERR_VMA, /* overlay buffer not at the link VMA */
|
||||
APP_ERR_AUTH, /* host write refused: timestamp mismatch */
|
||||
APP_ERR_CAP, /* required firmware capability missing */
|
||||
};
|
||||
|
||||
/* Read + validate a slot header (no CRC of the code). */
|
||||
uint8_t APP_ValidateSlot(uint8_t slot, app_header_t *out_header);
|
||||
|
||||
/* Validate, load into the overlay, verify the code CRC, and run the app.
|
||||
* Returns when the app exits; the internal flash is never touched. */
|
||||
uint8_t APP_LaunchOverlay(uint8_t slot);
|
||||
|
||||
/* Launch the first installed app advertising this shortcut bit. */
|
||||
uint8_t APP_LaunchOverlayShortcut(uint8_t shortcut);
|
||||
|
||||
/* Cached availability of apps exposed as resident quick actions. */
|
||||
uint8_t APP_OverlayShortcutMask(void);
|
||||
|
||||
/* Host-tool slot management (external flash only, never brick-critical). */
|
||||
uint8_t APP_SlotInfo(uint8_t slot, app_header_t *out_header);
|
||||
uint8_t APP_SlotErase(uint8_t slot);
|
||||
uint8_t APP_SlotWrite(uint8_t slot, uint32_t offset, const uint8_t *data, uint32_t len);
|
||||
|
||||
|
||||
#endif /* APPS_APP_OVERLAY_H */
|
||||
@@ -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.*) }
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
/* Copyright 2026 Armel F4HWN
|
||||
* https://github.com/armel
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Beacon (fox) — overlay app. Ported from App/app/foxhunt.c, beacon sub-mode only.
|
||||
* Turns the radio into a hidden ARDF transmitter: keys up on the TX VFO and repeats
|
||||
* a CW fox identifier (MOE..MO5 / MO / "<call> MOE") for the TX window, then stays
|
||||
* silent for the idle gap. Two keying modes (key 4): TONE (default) keeps the carrier
|
||||
* up and keys only the tone (MCW / F2A); CARR keys the PA together with the tone, so
|
||||
* between elements the carrier itself is gone (carrier interruption, ARDF field pattern).
|
||||
*
|
||||
* Keys: 1 TX window · 2 idle gap · 3 fox id · 4 keying mode TONE/CARR (F reverses)
|
||||
* MENU restart idle · long F keypad lock · EXIT quit.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include "../app_api.h"
|
||||
|
||||
#define LCD_WIDTH 128
|
||||
#define TICK_MS 50
|
||||
#define LOCK_HOLD_MS 500
|
||||
#define MORSE_UNIT 100
|
||||
#define TONE_HZ 1000
|
||||
#define CALL_MAX 12
|
||||
|
||||
#define IDLE_DEF 30
|
||||
#define IDLE_MIN 5
|
||||
#define IDLE_MAX 240
|
||||
#define IDLE_STEP 5
|
||||
#define TX_DEF 30
|
||||
#define TX_MIN 5
|
||||
#define TX_MAX 60
|
||||
#define TX_STEP 5
|
||||
|
||||
#define FOX_MO 5
|
||||
#define FOX_CALL 6
|
||||
#define FOX_COUNT 7
|
||||
#define MSG_CALL 1
|
||||
#define MSG_ID 3
|
||||
#define MSG_ONE 2
|
||||
|
||||
#define CFG_MAGIC 0xF4
|
||||
|
||||
/* Sentinel-prefixed Morse (leading 1 sentinel, then elements MSB-first: 0 dit, 1 dah). */
|
||||
static const uint8_t M_LET[26] = {0x05,0x18,0x1A,0x0C,0x02,0x12,0x0E,0x10,0x04,0x17,0x0D,0x14,0x07,0x06,0x0F,0x16,0x1D,0x0A,0x08,0x03,0x09,0x11,0x0B,0x19,0x1B,0x1C};
|
||||
static const uint8_t M_DIG[10] = {0x3F,0x2F,0x27,0x23,0x21,0x20,0x30,0x38,0x3C,0x3E};
|
||||
static const char FOX_TAIL[5] = {'E','I','S','H','5'};
|
||||
static const uint8_t BMP_TX[16] = {0x1c,0x22,0x41,0x1c,0x22,0x00,0x08,0x1c,0x1c,0x08,0x00,0x22,0x1c,0x41,0x22,0x1c};
|
||||
static const uint8_t FONT_LOCK[9] = {0x7c,0x46,0x45,0x45,0x45,0x45,0x45,0x46,0x7c};
|
||||
static const uint8_t FONT_F[9] = {0x3e,0x7f,0x41,0x75,0x75,0x75,0x7d,0x7f,0x3e};
|
||||
|
||||
static const app_api_t *A;
|
||||
|
||||
static bool foxLocked, fArm, fLongDone, running, phaseTx, carrier;
|
||||
static uint16_t fHoldMs;
|
||||
static uint8_t beaconIdle, idleLeft, idleTick, beaconTx, secShown, charsSent, foxFox;
|
||||
static uint16_t txMsLeft;
|
||||
static char foxCall[CALL_MAX+1];
|
||||
static char msg[17];
|
||||
static uint8_t prevKey;
|
||||
static char str[16];
|
||||
|
||||
/* ---- tiny formatting ---- */
|
||||
static uint8_t slen(const char *s){ uint8_t n=0; while(s[n])n++; return n; }
|
||||
static char *put(char *o,const char *s){ while(*s)*o++=*s++; return o; }
|
||||
static char *putu(char *o,uint32_t v){ char t[6]; int8_t n=0; do{t[n++]=(char)('0'+v%10);v/=10;}while(v&&n<6); while(n--)*o++=t[n]; return o; }
|
||||
static void put2(char **o,uint8_t v){ if(v<10)*(*o)++='0'; *o=putu(*o,v); }
|
||||
static void cpy(uint8_t *d,const uint8_t *s,uint8_t n){ while(n--)*d++=*s++; }
|
||||
static const char *findsp(const char *s){ while(*s){ if(*s==' ')return s; s++; } return NULL; }
|
||||
static uint8_t mmin(uint8_t a,uint8_t b){ return a<b?a:b; }
|
||||
|
||||
/* ---- settings cycles ---- */
|
||||
static uint8_t rangeStep(uint8_t v,uint8_t lo,uint8_t hi,uint8_t step,int8_t dir){
|
||||
if(dir>0) return (v>=hi)?lo:(uint8_t)(v+step);
|
||||
return (v<=lo)?hi:(uint8_t)(v-step);
|
||||
}
|
||||
static bool settingKey(uint8_t key,int8_t dir){
|
||||
switch(key){
|
||||
case APP_KEY_1: beaconTx =rangeStep(beaconTx, TX_MIN, TX_MAX, TX_STEP, dir); return true;
|
||||
case APP_KEY_2: beaconIdle=rangeStep(beaconIdle,IDLE_MIN,IDLE_MAX,IDLE_STEP,dir); return true;
|
||||
case APP_KEY_3:
|
||||
if(dir>0){ if(++foxFox>=FOX_COUNT) foxFox=0; }
|
||||
else foxFox=foxFox?(uint8_t)(foxFox-1u):(uint8_t)(FOX_COUNT-1u);
|
||||
return true;
|
||||
case APP_KEY_4: carrier=!carrier; return true; /* TONE <-> CARR (dir moot) */
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- message ---- */
|
||||
static void buildMsg(void){
|
||||
char *o=msg;
|
||||
if(foxFox==FOX_CALL){
|
||||
if(foxCall[0]){ o=put(o,foxCall); o=put(o," MOE"); }
|
||||
else o=put(o,"MOE");
|
||||
} else if(foxFox==FOX_MO){ o=put(o,"MO"); }
|
||||
else { o=put(o,"MO"); *o++=FOX_TAIL[foxFox]; }
|
||||
*o='\0';
|
||||
}
|
||||
static void foxLabel(char *out){
|
||||
char *o=put(out,"FOX ");
|
||||
if(foxFox==FOX_CALL) o=put(o,"CALL");
|
||||
else if(foxFox==FOX_MO) o=put(o,"MO");
|
||||
else { o=put(o,"MO"); *o++=FOX_TAIL[foxFox]; }
|
||||
*o='\0';
|
||||
}
|
||||
static uint8_t morseByte(char c){
|
||||
if(c>='a'&&c<='z') c-=32;
|
||||
if(c>='A'&&c<='Z') return M_LET[c-'A'];
|
||||
if(c>='0'&&c<='9') return M_DIG[c-'0'];
|
||||
if(c=='/') return 0x32;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- drawing ---- */
|
||||
static void tag(const char *s,uint8_t x,uint8_t line){ A->print_inverse(s,x,line,false,true,(uint8_t)(x+slen(s)*4)); }
|
||||
|
||||
static void chrome(void){
|
||||
A->display_clear();
|
||||
A->status_clear();
|
||||
A->print_inverse("BEACON",2,0,true,true,26);
|
||||
A->draw_battery();
|
||||
if(foxLocked) cpy(A->status_line+70,FONT_LOCK,sizeof(FONT_LOCK));
|
||||
else if(fArm) cpy(A->status_line+70,FONT_F,sizeof(FONT_F));
|
||||
{ char *o=putu(str,A->tx_freq()/100000u); *o++='.'; uint32_t fr=A->tx_freq()%100000u;
|
||||
for(int8_t d=4;d>=0;d--){ uint32_t p=1; for(int8_t k=0;k<d;k++)p*=10; *o++=(char)('0'+(fr/p)%10);} *o='\0'; }
|
||||
A->print_normal(str,(uint8_t)(126-slen(str)*7),0,6);
|
||||
}
|
||||
static void drawTxSeconds(void){
|
||||
uint8_t sec=(uint8_t)((txMsLeft+999u)/1000u);
|
||||
for(uint8_t x=0;x<LCD_WIDTH;x++) A->fb[0][x]=0;
|
||||
A->print_normal("TX",1,0,0);
|
||||
char *o=str; put2(&o,sec); *o++='s'; *o='\0';
|
||||
A->print_normal(str,(uint8_t)(127-slen(str)*7),0,0);
|
||||
secShown=sec;
|
||||
}
|
||||
static void drawMessage(uint8_t vis){
|
||||
const char *sp=findsp(msg);
|
||||
if(!sp){
|
||||
uint8_t idLen=slen(msg), v=mmin(vis,idLen);
|
||||
char id[17]; cpy((uint8_t*)id,(const uint8_t*)msg,v); id[v]='\0';
|
||||
A->print_string(id,(uint8_t)((LCD_WIDTH-idLen*8u)/2u),0,MSG_ONE,8);
|
||||
return;
|
||||
}
|
||||
uint8_t callLen=(uint8_t)(sp-msg), vc=mmin(vis,callLen);
|
||||
char call[CALL_MAX+1]; cpy((uint8_t*)call,(const uint8_t*)msg,vc); call[vc]='\0';
|
||||
A->print_string(call,(uint8_t)((LCD_WIDTH-callLen*8u)/2u),0,MSG_CALL,8);
|
||||
if(vis>callLen+1){
|
||||
const char *id=sp+1; uint8_t idLen=slen(id), vi=mmin((uint8_t)(vis-callLen-1),idLen);
|
||||
char idb[8]; cpy((uint8_t*)idb,(const uint8_t*)id,vi); idb[vi]='\0';
|
||||
A->print_string(idb,(uint8_t)((LCD_WIDTH-idLen*8u)/2u),0,MSG_ID,8);
|
||||
}
|
||||
}
|
||||
static void updateProgress(uint8_t vis){
|
||||
const char *sp=findsp(msg); uint8_t top;
|
||||
charsSent=vis;
|
||||
if(!sp) top=MSG_ONE;
|
||||
else { uint8_t callLen=(uint8_t)(sp-msg); if(vis==callLen+1) return; top=(vis<=callLen)?MSG_CALL:MSG_ID; }
|
||||
for(uint8_t x=0;x<LCD_WIDTH;x++){ A->fb[top][x]=0; A->fb[top+1][x]=0; }
|
||||
drawMessage(charsSent);
|
||||
A->blit_line(top); A->blit_line(top+1);
|
||||
}
|
||||
static void beaconDraw(bool txNow,uint8_t il){
|
||||
chrome();
|
||||
if(txNow){
|
||||
cpy(A->status_line+48,BMP_TX,16);
|
||||
drawTxSeconds();
|
||||
drawMessage(charsSent);
|
||||
} else {
|
||||
A->print_string("IDLE",0,127,1,10);
|
||||
char *o=str; put2(&o,il); *o++='s'; *o='\0';
|
||||
A->print_string(str,0,127,3,10);
|
||||
}
|
||||
char *o=put(str,"TX "); o=putu(o,beaconTx); o=put(o,"s"); *o='\0'; tag(str,4,5);
|
||||
o=put(str,"IDLE "); o=putu(o,beaconIdle); o=put(o,"s"); *o='\0'; tag(str,4,6);
|
||||
foxLabel(str); tag(str,66,5);
|
||||
{ const char *m=carrier?"CARR":"TONE"; tag(m,(uint8_t)(125-slen(m)*4),5); }
|
||||
}
|
||||
static void blit(void){ A->blit_status(); A->blit_full(); }
|
||||
|
||||
/* ---- keypad-lock long-press ---- */
|
||||
/* true exactly on the lock/unlock toggle, so a caller that does not redraw every tick
|
||||
* (the TX loop) can refresh the padlock at once. */
|
||||
static bool lockTrack(uint8_t key,uint16_t ms){
|
||||
if(key!=APP_KEY_F){ fHoldMs=0; fLongDone=false; return false; }
|
||||
if(fLongDone) return false;
|
||||
fHoldMs+=ms;
|
||||
if(fHoldMs>=LOCK_HOLD_MS){ fLongDone=true; foxLocked=!foxLocked; fArm=false; A->backlight_on(); return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
/* ---- interactive TX delay: drains the window, keys, live seconds. true = abort ---- */
|
||||
static bool txDelay(uint16_t ms){
|
||||
while(ms){
|
||||
uint16_t slice=(ms>10)?10:ms;
|
||||
A->delay_ms(slice); ms-=slice;
|
||||
txMsLeft=(txMsLeft>slice)?(uint16_t)(txMsLeft-slice):0;
|
||||
A->backlight_update();
|
||||
if((uint8_t)((txMsLeft+999u)/1000u)!=secShown){ drawTxSeconds(); A->blit_line(0); }
|
||||
|
||||
uint8_t key=A->get_key();
|
||||
if(lockTrack(key,slice)){ beaconDraw(true,0); blit(); } /* show the padlock at once, mid-TX */
|
||||
if(key==APP_KEY_INVALID||key==prevKey){ prevKey=key; continue; }
|
||||
prevKey=key;
|
||||
A->backlight_on();
|
||||
if(foxLocked) continue;
|
||||
if(key==APP_KEY_EXIT){ running=false; return true; }
|
||||
if(key==APP_KEY_MENU) return true; /* cut short, restart idle */
|
||||
if(key==APP_KEY_F){ fArm=!fArm; beaconDraw(true,0); blit(); continue; }
|
||||
if(settingKey(key,fArm?-1:1)){ fArm=false; beaconDraw(true,0); blit(); }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/* Keying: both modes key the tone; CARR gates the PA in lockstep so the carrier is truly
|
||||
* gone between elements (tone muted too, else it bleeds through residual PA leakage). */
|
||||
static void keyOn(void){ A->tx_mute(false); A->tx_carrier(true); } /* carrier up both modes; re-arms after a mid-window switch */
|
||||
static void keyOff(void){ A->tx_mute(true); if(carrier) A->tx_carrier(false); }
|
||||
static bool morseChar(char c,uint8_t vis){
|
||||
uint8_t code=morseByte(c);
|
||||
if(code==0){ if(txDelay(MORSE_UNIT*4)) return true; updateProgress(vis); return false; }
|
||||
uint8_t bit=0x80; while(!(code&bit))bit>>=1;
|
||||
for(bit>>=1;bit;bit>>=1){
|
||||
uint16_t on=(code&bit)?(MORSE_UNIT*3):MORSE_UNIT;
|
||||
keyOn();
|
||||
if(txDelay(on)){ keyOff(); return true; }
|
||||
keyOff();
|
||||
if(txDelay(MORSE_UNIT)) return true;
|
||||
}
|
||||
updateProgress(vis);
|
||||
return txDelay(MORSE_UNIT*2);
|
||||
}
|
||||
static void transmit(void){
|
||||
A->tx_set_params();
|
||||
A->tx_tone(TONE_HZ);
|
||||
A->tx_mute(true); /* open muted (both modes) */
|
||||
if(carrier) A->tx_carrier(false); /* CARR: carrier off until first element */
|
||||
txMsLeft=(uint16_t)beaconTx*1000u;
|
||||
bool stop=false;
|
||||
while(!stop && txMsLeft>0){
|
||||
buildMsg();
|
||||
charsSent=0;
|
||||
beaconDraw(true,0); blit();
|
||||
for(uint8_t i=0;!stop && msg[i];i++) stop=morseChar(msg[i],(uint8_t)(i+1));
|
||||
if(!stop && txMsLeft>0) stop=txDelay(MORSE_UNIT*7);
|
||||
}
|
||||
A->tx_mute(true);
|
||||
A->tx_end(); /* PA off + restore RX */
|
||||
}
|
||||
|
||||
/* ---- idle-phase keys ---- */
|
||||
static void idleKeys(void){
|
||||
uint8_t key=A->get_key();
|
||||
lockTrack(key,TICK_MS);
|
||||
if(key==APP_KEY_INVALID||key==prevKey){ prevKey=key; return; }
|
||||
prevKey=key;
|
||||
A->backlight_on();
|
||||
if(foxLocked) return;
|
||||
if(key==APP_KEY_F){ fArm=!fArm; return; }
|
||||
switch(key){
|
||||
case APP_KEY_EXIT: running=false; break;
|
||||
case APP_KEY_MENU: idleLeft=beaconIdle; idleTick=0; break;
|
||||
default: settingKey(key,fArm?-1:1); break;
|
||||
}
|
||||
fArm=false;
|
||||
}
|
||||
|
||||
static void tickDelay(void){ for(uint8_t i=0;i<TICK_MS/10;i++){ A->delay_ms(10); A->backlight_update(); } }
|
||||
|
||||
/* ---- config (deferred) ---- */
|
||||
static void loadConfig(void){
|
||||
uint8_t c[5]; A->cfg_load(c,5);
|
||||
if(c[0]==CFG_MAGIC){
|
||||
if(c[1]>=IDLE_MIN&&c[1]<=IDLE_MAX&&(c[1]%IDLE_STEP)==0) beaconIdle=c[1];
|
||||
if(c[2]<FOX_COUNT) foxFox=c[2];
|
||||
if(c[3]>=TX_MIN&&c[3]<=TX_MAX&&(c[3]%TX_STEP)==0) beaconTx=c[3];
|
||||
if(c[4]<=1) carrier=c[4]; /* erased (0xFF) legacy config -> keep default */
|
||||
}
|
||||
}
|
||||
static void saveConfig(void){ uint8_t c[5]={CFG_MAGIC,beaconIdle,foxFox,beaconTx,(uint8_t)carrier}; A->cfg_save(c,5); }
|
||||
|
||||
static void txDenied(void){
|
||||
chrome();
|
||||
A->print_string("TX OFF",0,127,1,8);
|
||||
blit();
|
||||
for(uint8_t i=0;i<20;i++) tickDelay();
|
||||
}
|
||||
|
||||
__attribute__((section(".text.entry"),used))
|
||||
void app_main(const app_api_t *api){
|
||||
A=api;
|
||||
foxLocked=fArm=fLongDone=carrier=false; fHoldMs=0;
|
||||
beaconIdle=IDLE_DEF; beaconTx=TX_DEF; foxFox=FOX_CALL;
|
||||
prevKey=APP_KEY_INVALID;
|
||||
A->boot_callsign(foxCall,sizeof(foxCall));
|
||||
loadConfig();
|
||||
A->backlight_on();
|
||||
|
||||
phaseTx=true; idleLeft=beaconIdle; idleTick=0;
|
||||
running=true;
|
||||
while(running){
|
||||
if(phaseTx){
|
||||
if(A->tx_state()!=0){ txDenied(); phaseTx=false; idleLeft=beaconIdle; idleTick=0; continue; }
|
||||
transmit();
|
||||
phaseTx=false; idleLeft=beaconIdle; idleTick=0;
|
||||
continue;
|
||||
}
|
||||
idleKeys();
|
||||
if(!running) break;
|
||||
beaconDraw(false,idleLeft); blit();
|
||||
if(++idleTick>=(1000/TICK_MS)){ idleTick=0; if(idleLeft>0)idleLeft--; if(idleLeft==0)phaseTx=true; }
|
||||
A->battery_sample();
|
||||
tickDelay();
|
||||
}
|
||||
|
||||
saveConfig();
|
||||
A->tx_end(); /* safety: PA off + RX restored */
|
||||
A->audio_path(false);
|
||||
}
|
||||
Executable
+42
@@ -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"
|
||||
@@ -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.*) }
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
/* Copyright 2026 Armel F4HWN
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
*
|
||||
* BEAM overlay app. Packet version 2 remains wire-compatible with the
|
||||
* resident implementation: one selected VFO is sent and a received VFO is
|
||||
* stored in the first free memory channel.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include "../app_api.h"
|
||||
|
||||
#define PACKET_MAGIC 0xBEA5u
|
||||
#define PACKET_VERSION 2u
|
||||
#define PACKET_START 0xABCDu
|
||||
#define PACKET_END 0xDCBAu
|
||||
#define PACKET_WORDS 36u
|
||||
|
||||
enum {
|
||||
STATUS_READY = 0,
|
||||
STATUS_TX_WAIT,
|
||||
STATUS_TX_DONE,
|
||||
STATUS_RX_WAIT,
|
||||
STATUS_RX_SAVED,
|
||||
STATUS_RX_FULL,
|
||||
STATUS_ERROR,
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
uint16_t magic;
|
||||
uint8_t version;
|
||||
uint8_t pad;
|
||||
app_beam_channel_t channel;
|
||||
} beam_payload_t;
|
||||
|
||||
_Static_assert(sizeof(beam_payload_t) == 48u,
|
||||
"BEAM v2 wire payload layout changed");
|
||||
|
||||
static const uint16_t OBFUSCATION[8] = {
|
||||
0x6C16, 0xE614, 0x912E, 0x400D, 0x3521, 0x40D5, 0x0313, 0x80E9
|
||||
};
|
||||
|
||||
static const app_api_t *A;
|
||||
static uint32_t packet_store[18]; /* 72 bytes, 4-byte aligned for the payload */
|
||||
static uint16_t copiedChannel;
|
||||
static uint8_t mode, status;
|
||||
static bool receiving, running, dirty;
|
||||
|
||||
static uint16_t *packet(void) { return (uint16_t *)packet_store; }
|
||||
static beam_payload_t *payload(void) { return (beam_payload_t *)&packet()[2]; }
|
||||
|
||||
static void clear_bytes(void *ptr, uint8_t count)
|
||||
{
|
||||
uint8_t *p = (uint8_t *)ptr;
|
||||
while (count--) *p++ = 0;
|
||||
}
|
||||
|
||||
static uint16_t crc16(const void *buffer, uint16_t size)
|
||||
{
|
||||
const uint8_t *data = (const uint8_t *)buffer;
|
||||
uint16_t crc = 0;
|
||||
while (size--) {
|
||||
crc ^= (uint16_t)*data++ << 8;
|
||||
for (uint8_t bit = 0; bit < 8u; bit++)
|
||||
crc = (crc & 0x8000u) ? (uint16_t)((crc << 1) ^ 0x1021u)
|
||||
: (uint16_t)(crc << 1);
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
static void obfuscate(void)
|
||||
{
|
||||
uint16_t *p = packet();
|
||||
for (uint8_t i = 0; i < 32u; i++)
|
||||
p[i + 1u] ^= OBFUSCATION[i & 7u];
|
||||
}
|
||||
|
||||
static void draw(void)
|
||||
{
|
||||
const char *state;
|
||||
switch (status) {
|
||||
case STATUS_TX_WAIT: state = "SENDING"; break;
|
||||
case STATUS_TX_DONE: state = "SENT"; break;
|
||||
case STATUS_RX_WAIT: state = "WAITING"; break;
|
||||
case STATUS_RX_SAVED: state = "RECEIVED"; break;
|
||||
case STATUS_RX_FULL: state = "MEM FULL"; break;
|
||||
case STATUS_ERROR: state = "ERROR"; break;
|
||||
default: state = mode ? "BEAM RX" : "BEAM TX"; break;
|
||||
}
|
||||
A->beam_draw(state);
|
||||
A->blit_status();
|
||||
A->blit_full();
|
||||
}
|
||||
|
||||
static void stop_rx(void)
|
||||
{
|
||||
if (receiving) {
|
||||
A->beam_rx(false);
|
||||
receiving = false;
|
||||
}
|
||||
}
|
||||
|
||||
static void send_packet(void)
|
||||
{
|
||||
uint16_t *p = packet();
|
||||
clear_bytes(packet_store, sizeof(packet_store));
|
||||
p[0] = PACKET_START;
|
||||
payload()->magic = PACKET_MAGIC;
|
||||
payload()->version = PACKET_VERSION;
|
||||
A->beam_get(&payload()->channel);
|
||||
p[34] = crc16(&p[1], 2u + 64u);
|
||||
p[35] = PACKET_END;
|
||||
obfuscate();
|
||||
|
||||
status = STATUS_TX_WAIT;
|
||||
draw();
|
||||
A->beam_send(p);
|
||||
status = STATUS_TX_DONE;
|
||||
for (uint8_t i = 0; i < 3u; i++) {
|
||||
A->play_tone(880, 60);
|
||||
if (i < 2u) A->delay_ms(20);
|
||||
}
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
static void start(void)
|
||||
{
|
||||
stop_rx();
|
||||
/* A channel save is committed only after app_main() returns. Do not let a
|
||||
second RX overwrite that pending save; exit and relaunch to receive more. */
|
||||
if (mode && copiedChannel != 0xFFFFu)
|
||||
return;
|
||||
A->beam_prepare();
|
||||
if (!mode) {
|
||||
send_packet();
|
||||
return;
|
||||
}
|
||||
A->beam_rx(true);
|
||||
receiving = true;
|
||||
status = STATUS_RX_WAIT;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
static bool valid_packet(void)
|
||||
{
|
||||
uint16_t *p = packet();
|
||||
if (p[0] != PACKET_START || p[35] != PACKET_END)
|
||||
return false;
|
||||
obfuscate();
|
||||
if (p[34] != crc16(&p[1], 2u + 64u))
|
||||
return false;
|
||||
return payload()->magic == PACKET_MAGIC && payload()->version == PACKET_VERSION;
|
||||
}
|
||||
|
||||
static void poll_rx(void)
|
||||
{
|
||||
if (!receiving)
|
||||
return;
|
||||
const uint8_t result = A->beam_rx_poll(packet());
|
||||
if (result == APP_BEAM_RX_WAIT)
|
||||
return;
|
||||
if (result == APP_BEAM_RX_ERROR || !valid_packet()) {
|
||||
status = STATUS_ERROR;
|
||||
A->backlight_on();
|
||||
dirty = true;
|
||||
return;
|
||||
}
|
||||
|
||||
copiedChannel = A->beam_save(&payload()->channel);
|
||||
A->beam_rx(false);
|
||||
receiving = false;
|
||||
status = copiedChannel == 0xFFFFu ? STATUS_RX_FULL : STATUS_RX_SAVED;
|
||||
A->backlight_on();
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
static void key_press(uint8_t key)
|
||||
{
|
||||
switch (key) {
|
||||
case APP_KEY_UP:
|
||||
case APP_KEY_DOWN:
|
||||
stop_rx();
|
||||
mode ^= 1u;
|
||||
status = STATUS_READY;
|
||||
dirty = true;
|
||||
break;
|
||||
case APP_KEY_MENU:
|
||||
start();
|
||||
break;
|
||||
case APP_KEY_EXIT:
|
||||
running = false;
|
||||
break;
|
||||
case APP_KEY_PTT:
|
||||
break;
|
||||
default:
|
||||
A->play_tone(500, 60);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
__attribute__((section(".text.entry"), used))
|
||||
void app_main(const app_api_t *api)
|
||||
{
|
||||
A = api;
|
||||
mode = 0;
|
||||
status = STATUS_READY;
|
||||
copiedChannel = 0xFFFFu;
|
||||
receiving = false;
|
||||
running = true;
|
||||
dirty = true;
|
||||
A->backlight_on();
|
||||
|
||||
uint8_t previous = APP_KEY_INVALID;
|
||||
uint8_t batteryTicks = 0;
|
||||
while (running) {
|
||||
const uint8_t key = A->get_key();
|
||||
if (key == APP_KEY_SAVER) {
|
||||
previous = APP_KEY_INVALID;
|
||||
} else if (key == APP_KEY_WAKE) {
|
||||
previous = APP_KEY_INVALID;
|
||||
dirty = true;
|
||||
} else {
|
||||
if (key != previous && key != APP_KEY_INVALID) {
|
||||
A->backlight_on();
|
||||
key_press(key);
|
||||
}
|
||||
previous = key;
|
||||
}
|
||||
|
||||
poll_rx();
|
||||
if (dirty) { draw(); dirty = false; }
|
||||
A->delay_ms(10);
|
||||
A->backlight_update();
|
||||
if (++batteryTicks >= 50u) {
|
||||
batteryTicks = 0;
|
||||
A->battery_sample();
|
||||
dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
stop_rx();
|
||||
A->beam_leave();
|
||||
}
|
||||
Executable
+37
@@ -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"
|
||||
@@ -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.*) }
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
/* Copyright 2025 Armel F4HWN
|
||||
* https://github.com/armel
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Breakout — overlay-app port (POC).
|
||||
*
|
||||
* Self-contained: no firmware headers, no libc. Every resident service is
|
||||
* reached through the app_api_t table; the entry point app_main() is forced to
|
||||
* blob offset 0 via section ".text.entry". The loader zeroes the 4 KiB overlay,
|
||||
* copies this blob in, and calls app_main(&api).
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include "../app_api.h"
|
||||
|
||||
#define LCD_WIDTH 128
|
||||
#define BRICK_NUMBER 18
|
||||
#define BALL_NUMBER 5
|
||||
|
||||
#define BRICK_WIDTH 14
|
||||
#define BRICK_HEIGHT 5
|
||||
#define BALL_WIDTH 3
|
||||
#define BALL_HEIGHT 3
|
||||
#define RACKET_WIDTH 24
|
||||
#define RACKET_HEIGHT 2
|
||||
#define RACKET_Y 50
|
||||
|
||||
typedef struct { uint8_t x; uint8_t y; bool destroy; } Brick;
|
||||
typedef struct { int8_t x; uint8_t p; } Racket;
|
||||
typedef struct { int16_t x; int8_t y; int8_t dx; int8_t dy; } Ball;
|
||||
|
||||
/* ---- tiny freestanding helpers (replace libc / libgcc-free where cheap) ---- */
|
||||
void *memset(void *d, int c, size_t n) {
|
||||
uint8_t *p = d; while (n--) *p++ = (uint8_t)c; return d;
|
||||
}
|
||||
void *memcpy(void *d, const void *s, size_t n) {
|
||||
uint8_t *p = d; const uint8_t *q = s; while (n--) *p++ = *q++; return d;
|
||||
}
|
||||
static int iabs(int v) { return v < 0 ? -v : v; }
|
||||
static int imin(int a, int b) { return a < b ? a : b; }
|
||||
|
||||
/* zero-padded unsigned -> string, width w (<= 5). Replaces sprintf. */
|
||||
static void u2str(char *out, const char *label, uint16_t v, uint8_t w) {
|
||||
char *o = out;
|
||||
while (*label) *o++ = *label++;
|
||||
char tmp[6]; int8_t n = 0;
|
||||
do { tmp[n++] = (char)('0' + v % 10); v /= 10; } while (v && n < 6);
|
||||
while (n < w) tmp[n++] = '0';
|
||||
while (n--) *o++ = tmp[n];
|
||||
*o = '\0';
|
||||
}
|
||||
|
||||
/* ---- app-wide handle to the resident ABI, set once at entry ---- */
|
||||
static const app_api_t *A;
|
||||
|
||||
/* ---- game state (lives in the overlay; re-initialised at entry) ---- */
|
||||
static uint32_t randSeed;
|
||||
static uint8_t blockAnim;
|
||||
static bool isInitialized;
|
||||
static bool isPaused;
|
||||
static uint8_t levelCountBreakout;
|
||||
static uint16_t tone;
|
||||
static uint16_t score;
|
||||
static int16_t ballCount;
|
||||
static char str[12];
|
||||
static uint8_t kbdPrev, kbdCur;
|
||||
|
||||
static Brick brick[BRICK_NUMBER];
|
||||
static Racket racket;
|
||||
static Ball ball;
|
||||
|
||||
static const uint8_t BRICK_ANIM_PATTERNS[4] =
|
||||
{0b00110001, 0b00101001, 0b00100101, 0b00100011};
|
||||
|
||||
static void srand_custom(uint32_t seed) { randSeed = seed ? seed : 1; }
|
||||
static int rand_custom(void) { randSeed = randSeed * 1103515245u + 12345u; return (randSeed >> 16) & 0x7FFF; }
|
||||
static int randInt(int min, int max) { return min + (rand_custom() % (max - min + 1)); }
|
||||
|
||||
static void reset(void) { ballCount = BALL_NUMBER; levelCountBreakout = 1; score = 0; }
|
||||
|
||||
static void playBeep(uint16_t t) { A->play_tone(t, 100); }
|
||||
|
||||
static void drawScore(void) {
|
||||
A->status_clear();
|
||||
u2str(str, "Level ", levelCountBreakout, 2); A->print_tiny(str, 0, 1, true, true);
|
||||
u2str(str, "Ball ", (ballCount < 0) ? 0 : ballCount, 2); A->print_tiny(str, 45, 1, true, true);
|
||||
u2str(str, "Score ", score, 4); A->print_tiny(str, 88, 1, true, true);
|
||||
}
|
||||
|
||||
static void renderBall(bool state) {
|
||||
A->draw_rect(A->fb, ball.x, ball.y, ball.x + BALL_WIDTH - 1, ball.y + BALL_HEIGHT - 1, state);
|
||||
A->draw_line(A->fb, ball.x - 1, ball.y + 1, ball.x + BALL_WIDTH, ball.y + 1, state);
|
||||
}
|
||||
|
||||
static void initBall(void) { ball.x = 62; ball.y = 30; ball.dx = 0; ball.dy = 1; renderBall(true); }
|
||||
|
||||
static void directionBall(int16_t x, uint8_t w, int8_t num) {
|
||||
ball.dx = (int16_t)(x + w - ball.x) * (-num - num) / w + num;
|
||||
ball.dy *= -1;
|
||||
}
|
||||
|
||||
static void initWall(void) {
|
||||
Brick *current = brick;
|
||||
for (uint8_t y = 0; y < 24; y += 8)
|
||||
for (uint8_t x = 6; x < 126; x += 20) {
|
||||
current->x = x; current->y = y; current->destroy = false; current++;
|
||||
}
|
||||
}
|
||||
|
||||
static void initRacket(void);
|
||||
|
||||
static void drawBall(void) {
|
||||
renderBall(false);
|
||||
ball.x += ball.dx; ball.y += ball.dy;
|
||||
|
||||
if (ball.y <= 0) { ball.dx = randInt(-3, 3); ball.dy = 1; }
|
||||
else if (ball.x <= 2) { ball.dx = iabs(ball.dx); }
|
||||
else if (ball.x >= 124) { ball.dx = -iabs(ball.dx); }
|
||||
|
||||
if (ball.y == 47) {
|
||||
if (ball.x + 1 >= racket.x && ball.x - 1 <= racket.x + RACKET_WIDTH) {
|
||||
directionBall(racket.x, RACKET_WIDTH, 3); tone = 400;
|
||||
}
|
||||
} else if (ball.y > 49) {
|
||||
ballCount--;
|
||||
A->display_clear();
|
||||
drawScore();
|
||||
tone = 800;
|
||||
if (ballCount < 0) {
|
||||
reset(); initWall(); /* drawWall drawn by main loop */
|
||||
isPaused = true;
|
||||
A->print_bold("GAME OVER", 0, LCD_WIDTH - 1, 4);
|
||||
}
|
||||
initRacket(); initBall();
|
||||
}
|
||||
renderBall(true);
|
||||
}
|
||||
|
||||
static void drawWall(void) {
|
||||
for (uint8_t i = 0; i < BRICK_NUMBER; i++) {
|
||||
if (brick[i].destroy) continue;
|
||||
uint8_t *fb_ptr = A->fb[brick[i].y / 8] + brick[i].x;
|
||||
fb_ptr[0] = 0b00011110;
|
||||
fb_ptr[14] = 0b00011110;
|
||||
|
||||
if ((ball.x + 1 >= brick[i].x && ball.x - 1 <= brick[i].x + BRICK_WIDTH) &&
|
||||
(ball.y + 1 >= brick[i].y && ball.y - 1 <= brick[i].y + BRICK_HEIGHT)) {
|
||||
brick[i].destroy = true; score++;
|
||||
directionBall(brick[i].x, BRICK_WIDTH, 2);
|
||||
|
||||
A->led(true);
|
||||
memset(fb_ptr + 1, 0b00111111, 13);
|
||||
A->blit_line(brick[i].y / 8);
|
||||
playBeep(600);
|
||||
memset(fb_ptr + 0, 0b00000000, 15);
|
||||
A->blit_line(brick[i].y / 8);
|
||||
A->led(false);
|
||||
|
||||
if (score % BRICK_NUMBER == 0) {
|
||||
levelCountBreakout++; ballCount++; initWall(); return;
|
||||
}
|
||||
} else {
|
||||
for (uint8_t k = 0; k < 13; k++)
|
||||
fb_ptr[k + 1] = BRICK_ANIM_PATTERNS[(blockAnim + k) % 4];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void renderRacket(int x, bool state) {
|
||||
A->draw_rect(A->fb, x + 1, RACKET_Y, x + RACKET_WIDTH - 2, RACKET_Y + RACKET_HEIGHT, state);
|
||||
A->draw_line(A->fb, x, RACKET_Y + 1, x + RACKET_WIDTH - 1, RACKET_Y + 1, state);
|
||||
}
|
||||
|
||||
static void initRacket(void) { racket.x = 64 - (RACKET_WIDTH / 2); racket.p = racket.x; renderRacket(racket.x, true); }
|
||||
|
||||
static void drawRacket(void) {
|
||||
if (racket.p != racket.x) {
|
||||
renderRacket(racket.p, false);
|
||||
racket.p = racket.x;
|
||||
renderRacket(racket.x, true);
|
||||
}
|
||||
}
|
||||
|
||||
static void OnKeyDown(uint8_t key) {
|
||||
bool wasPaused = isPaused;
|
||||
switch (key) {
|
||||
case APP_KEY_4:
|
||||
case APP_KEY_UP: if (!isPaused && racket.x > 0) racket.x -= 2; isPaused = false; break;
|
||||
case APP_KEY_0:
|
||||
case APP_KEY_DOWN: if (!isPaused && racket.x < 102) racket.x += 2; isPaused = false; break;
|
||||
case APP_KEY_MENU:
|
||||
isPaused = !isPaused;
|
||||
if (isPaused) A->print_bold("PAUSE", 0, LCD_WIDTH - 1, 4);
|
||||
break;
|
||||
case APP_KEY_EXIT: isPaused = false; isInitialized = false; break;
|
||||
}
|
||||
if (wasPaused && !isPaused)
|
||||
for (uint8_t i = 0; i < 8; i++)
|
||||
A->draw_line(A->fb, 32, 32 + i, 96, 32 + i, false);
|
||||
}
|
||||
|
||||
static void handleInput(void) {
|
||||
kbdPrev = kbdCur;
|
||||
kbdCur = A->get_key();
|
||||
if (kbdCur == APP_KEY_INVALID) return;
|
||||
if (kbdCur == APP_KEY_UP || kbdCur == APP_KEY_DOWN ||
|
||||
kbdCur == APP_KEY_4 || kbdCur == APP_KEY_0 || kbdCur != kbdPrev)
|
||||
OnKeyDown(kbdCur);
|
||||
}
|
||||
|
||||
/* ---- entry point, pinned to blob offset 0 ---- */
|
||||
__attribute__((section(".text.entry"), used))
|
||||
void app_main(const app_api_t *api) {
|
||||
A = api;
|
||||
|
||||
#ifdef APP_POC_HELLO
|
||||
/* Minimal bisection test: prove jump + ABI + return work, no game body. */
|
||||
A->display_clear();
|
||||
A->print_bold("OVL HELLO", 0, LCD_WIDTH - 1, 3);
|
||||
A->blit_full();
|
||||
A->delay_ms(2000);
|
||||
return;
|
||||
#endif
|
||||
|
||||
/* reset all state (the overlay is not persistent across launches) */
|
||||
blockAnim = 0; isPaused = false; tone = 0; score = 0;
|
||||
kbdPrev = APP_KEY_INVALID; kbdCur = APP_KEY_INVALID;
|
||||
uint8_t swap = 0;
|
||||
|
||||
/* The low BK4819 counter bits vary continuously; mix them with the tuned
|
||||
* frequency instead of carrying a launch-only seed field in every ABI
|
||||
* table. */
|
||||
srand_custom(((uint32_t)A->bk_read(0x67u) << 16) ^ A->rx_freq());
|
||||
A->led(false);
|
||||
A->backlight_on();
|
||||
|
||||
A->display_clear();
|
||||
reset(); initWall(); initRacket(); initBall();
|
||||
A->status_clear();
|
||||
isInitialized = true;
|
||||
|
||||
while (isInitialized) {
|
||||
handleInput();
|
||||
if (!isPaused) {
|
||||
if (swap == 0) blockAnim = (blockAnim + 1) % 4;
|
||||
swap = (swap + 1) % 4;
|
||||
drawScore(); drawWall(); drawRacket(); drawBall();
|
||||
if (tone != 0) { playBeep(tone); tone = 0; }
|
||||
else A->delay_ms(40 - imin(levelCountBreakout - 1, 20));
|
||||
}
|
||||
A->blit_status();
|
||||
A->blit_full();
|
||||
}
|
||||
}
|
||||
Executable
+41
@@ -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"
|
||||
@@ -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.*) }
|
||||
}
|
||||
Executable
+41
@@ -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"
|
||||
@@ -0,0 +1,299 @@
|
||||
/* Copyright 2026 Armel F4HWN
|
||||
* https://github.com/armel
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Cube3D — overlay app. A real-time rotating solid on the 1-bit 128x64 LCD.
|
||||
* Vertices are spun by three axis rotations in Q14 fixed point (Cortex-M0+ has
|
||||
* no FPU and no hardware divide), then perspective-projected with a single
|
||||
* divide per vertex. Two looks, toggled with F:
|
||||
* - SOLID: hidden-line removal by back-face culling (a face is drawn only when
|
||||
* the signed area of its projected polygon shows it facing us).
|
||||
* - WIRE : every edge, with the far hemisphere dotted for a depth cue.
|
||||
* Edges use a self-clipped Bresenham writing the full 64 rows directly, since the
|
||||
* resident pixel helper does not bound-check. The eight solids' faces (with a
|
||||
* uniform outward winding) are generated offline by a convex-hull extractor, so
|
||||
* nothing here has to be hand-wound. Pure compute, no radio.
|
||||
*
|
||||
* Keys: UP/DOWN speed · 1-8 shape · STAR next shape · F solid/wire ·
|
||||
* MENU pause · EXIT quit.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include "../app_api.h"
|
||||
|
||||
#define W 128
|
||||
#define H 64
|
||||
#define CX 64 /* projection centre x */
|
||||
#define CY 32 /* projection centre y */
|
||||
#define DIST 150 /* camera distance along +z (keeps zc > 0) */
|
||||
#define FOCAL 80 /* focal length / field-of-view scale */
|
||||
#define MAXV 12 /* largest vertex count across the solids */
|
||||
|
||||
static const app_api_t *A;
|
||||
|
||||
/* One Q14 sine quadrant. Symmetry recovers the full 256-step wave while saving
|
||||
* 382 bytes for the renderer. */
|
||||
static const int16_t SIN_Q[65] = {
|
||||
0, 402, 804, 1205, 1606, 2006, 2404, 2801, 3196, 3590, 3981,
|
||||
4370, 4756, 5139, 5520, 5897, 6270, 6639, 7005, 7366, 7723, 8076,
|
||||
8423, 8765, 9102, 9434, 9760,10080,10394,10702,11003,11297,11585,
|
||||
11866,12140,12406,12665,12916,13160,13395,13623,13842,14053,14256,
|
||||
14449,14635,14811,14978,15137,15286,15426,15557,15679,15791,15893,
|
||||
15986,16069,16143,16207,16261,16305,16340,16364,16379,16384
|
||||
};
|
||||
|
||||
static int sin8(uint8_t angle)
|
||||
{
|
||||
const uint8_t quadrant = angle >> 6;
|
||||
uint8_t i = angle & 63u;
|
||||
if (quadrant & 1u)
|
||||
i = (uint8_t)(64u - i);
|
||||
const int value = SIN_Q[i];
|
||||
return quadrant >= 2u ? -value : value;
|
||||
}
|
||||
|
||||
/* A face is a polygon of up to 6 vertex indices, wound CCW as seen from outside
|
||||
* (generated offline by the hull extractor, so the signed-area cull sign is the
|
||||
* same for every solid). Unused slots are 0-padded and ignored (n gives length). */
|
||||
typedef struct { uint8_t n; uint8_t v[6]; } face_t;
|
||||
|
||||
static const int8_t CUBE_V[8][3]={{-26,-26,-26},{26,-26,-26},{26,26,-26},{-26,26,-26},{-26,-26,26},{26,-26,26},{26,26,26},{-26,26,26}};
|
||||
static const face_t CUBE_F[6]={{4,{2,1,0,3,0,0}},{4,{4,0,1,5,0,0}},{4,{7,3,0,4,0,0}},{4,{5,1,2,6,0,0}},{4,{6,2,3,7,0,0}},{4,{7,4,5,6,0,0}}};
|
||||
|
||||
static const int8_t OCTAHEDRON_V[6][3]={{38,0,0},{-38,0,0},{0,38,0},{0,-38,0},{0,0,38},{0,0,-38}};
|
||||
static const face_t OCTAHEDRON_F[8]={{3,{4,0,2,0,0,0}},{3,{2,0,5,0,0,0}},{3,{3,0,4,0,0,0}},{3,{5,0,3,0,0,0}},{3,{2,1,4,0,0,0}},{3,{5,1,2,0,0,0}},{3,{4,1,3,0,0,0}},{3,{3,1,5,0,0,0}}};
|
||||
|
||||
static const int8_t TETRAHEDRON_V[4][3]={{28,28,28},{28,-28,-28},{-28,28,-28},{-28,-28,28}};
|
||||
static const face_t TETRAHEDRON_F[4]={{3,{2,0,1,0,0,0}},{3,{1,0,3,0,0,0}},{3,{3,0,2,0,0,0}},{3,{2,1,3,0,0,0}}};
|
||||
|
||||
static const int8_t DIAMOND_V[8][3]={{30,0,0},{14,26,0},{-14,26,0},{-30,0,0},{-14,-26,0},{14,-26,0},{0,0,40},{0,0,-40}};
|
||||
static const face_t DIAMOND_F[12]={{3,{6,0,1,0,0,0}},{3,{1,0,7,0,0,0}},{3,{5,0,6,0,0,0}},{3,{7,0,5,0,0,0}},{3,{6,1,2,0,0,0}},{3,{2,1,7,0,0,0}},{3,{6,2,3,0,0,0}},{3,{3,2,7,0,0,0}},{3,{6,3,4,0,0,0}},{3,{4,3,7,0,0,0}},{3,{6,4,5,0,0,0}},{3,{5,4,7,0,0,0}}};
|
||||
|
||||
static const int8_t ICOSAHEDRON_V[12][3]={{0,18,29},{0,18,-29},{0,-18,29},{0,-18,-29},{18,29,0},{18,-29,0},{-18,29,0},{-18,-29,0},{29,0,18},{29,0,-18},{-29,0,18},{-29,0,-18}};
|
||||
static const face_t ICOSAHEDRON_F[20]={{3,{8,0,2,0,0,0}},{3,{2,0,10,0,0,0}},{3,{6,0,4,0,0,0}},{3,{4,0,8,0,0,0}},{3,{10,0,6,0,0,0}},{3,{3,1,9,0,0,0}},{3,{11,1,3,0,0,0}},{3,{4,1,6,0,0,0}},{3,{9,1,4,0,0,0}},{3,{6,1,11,0,0,0}},{3,{5,2,7,0,0,0}},{3,{8,2,5,0,0,0}},{3,{7,2,10,0,0,0}},{3,{7,3,5,0,0,0}},{3,{5,3,9,0,0,0}},{3,{11,3,7,0,0,0}},{3,{9,4,8,0,0,0}},{3,{8,5,9,0,0,0}},{3,{10,6,11,0,0,0}},{3,{11,7,10,0,0,0}}};
|
||||
|
||||
static const int8_t CUBOCTA_V[12][3]={{-24,-24,0},{-24,24,0},{24,-24,0},{24,24,0},{-24,0,-24},{-24,0,24},{24,0,-24},{24,0,24},{0,-24,-24},{0,-24,24},{0,24,-24},{0,24,24}};
|
||||
static const face_t CUBOCTA_F[14]={{4,{4,0,5,1,0,0}},{4,{2,9,0,8,0,0}},{3,{8,0,4,0,0,0}},{3,{5,0,9,0,0,0}},{4,{10,1,11,3,0,0}},{3,{4,1,10,0,0,0}},{3,{11,1,5,0,0,0}},{4,{3,7,2,6,0,0}},{3,{6,2,8,0,0,0}},{3,{9,2,7,0,0,0}},{3,{10,3,6,0,0,0}},{3,{7,3,11,0,0,0}},{4,{8,4,10,6,0,0}},{4,{7,11,5,9,0,0}}};
|
||||
|
||||
static const int8_t HEXPRISM_V[12][3]={{26,0,24},{12,22,24},{-12,22,24},{-26,0,24},{-12,-22,24},{12,-22,24},{26,0,-24},{12,22,-24},{-12,22,-24},{-26,0,-24},{-12,-22,-24},{12,-22,-24}};
|
||||
static const face_t HEXPRISM_F[8]={{6,{4,5,0,1,2,3}},{4,{7,1,0,6,0,0}},{4,{6,0,5,11,0,0}},{4,{8,2,1,7,0,0}},{4,{9,3,2,8,0,0}},{4,{10,4,3,9,0,0}},{4,{11,5,4,10,0,0}},{6,{9,8,7,6,11,10}}};
|
||||
|
||||
static const int8_t PENTAGEM_V[7][3]={{28,0,0},{8,26,0},{-22,16,0},{-22,-16,0},{8,-26,0},{0,0,42},{0,0,-42}};
|
||||
static const face_t PENTAGEM_F[10]={{3,{5,0,1,0,0,0}},{3,{1,0,6,0,0,0}},{3,{4,0,5,0,0,0}},{3,{6,0,4,0,0,0}},{3,{5,1,2,0,0,0}},{3,{2,1,6,0,0,0}},{3,{5,2,3,0,0,0}},{3,{3,2,6,0,0,0}},{3,{5,3,4,0,0,0}},{3,{4,3,6,0,0,0}}};
|
||||
|
||||
typedef struct {
|
||||
const int8_t (*v)[3];
|
||||
const face_t *f;
|
||||
uint8_t nv;
|
||||
uint8_t nf;
|
||||
const char *name;
|
||||
uint8_t namelen;
|
||||
} shape_t;
|
||||
|
||||
#define NSHAPE 8
|
||||
static const shape_t SHAPES[NSHAPE] = {
|
||||
{ CUBE_V, CUBE_F, 8, 6, "CUBE", 4 },
|
||||
{ OCTAHEDRON_V, OCTAHEDRON_F, 6, 8, "OCTAHEDRON", 10 },
|
||||
{ TETRAHEDRON_V, TETRAHEDRON_F, 4, 4, "TETRAHEDRON", 11 },
|
||||
{ DIAMOND_V, DIAMOND_F, 8, 12, "DIAMOND", 7 },
|
||||
{ ICOSAHEDRON_V, ICOSAHEDRON_F, 12, 20, "ICOSAHEDRON", 11 },
|
||||
{ CUBOCTA_V, CUBOCTA_F, 12, 14, "CUBOCTA", 7 },
|
||||
{ HEXPRISM_V, HEXPRISM_F, 12, 8, "HEXPRISM", 8 },
|
||||
{ PENTAGEM_V, PENTAGEM_F, 7, 10, "PENTAGEM", 8 },
|
||||
};
|
||||
|
||||
/* Per-frame projected screen coords + rotated depth of each vertex. */
|
||||
static int16_t px[MAXV], py[MAXV], pz[MAXV];
|
||||
|
||||
/* Quarter-half-units per frame. The low end has fractional angular steps;
|
||||
* level 16 reaches the old 16 half-units/frame once divided by four. */
|
||||
static const uint8_t ROT_RATE[16] = {
|
||||
1, 2, 3, 4, 6, 8, 10, 12,
|
||||
16, 20, 24, 30, 36, 44, 52, 64
|
||||
};
|
||||
|
||||
/* Set one pixel across the full 64 rows: 0..7 -> status line, 8..63 -> fb. */
|
||||
static void set_pixel(int x, int y)
|
||||
{
|
||||
if ((unsigned)x >= W || (unsigned)y >= H)
|
||||
return;
|
||||
const uint8_t bit = (uint8_t)(1u << (y & 7));
|
||||
if (y < 8)
|
||||
A->status_line[x] |= bit;
|
||||
else
|
||||
A->fb[(y >> 3) - 1][x] |= bit;
|
||||
}
|
||||
|
||||
/* Integer Bresenham; dotted skips every other step for the depth cue. */
|
||||
static void draw_edge(int x0, int y0, int x1, int y1, bool dotted)
|
||||
{
|
||||
const int dx = (x1 > x0 ? x1 - x0 : x0 - x1);
|
||||
const int dy = -(y1 > y0 ? y1 - y0 : y0 - y1);
|
||||
const int sx = (x0 < x1 ? 1 : -1);
|
||||
const int sy = (y0 < y1 ? 1 : -1);
|
||||
int err = dx + dy;
|
||||
unsigned step = 0;
|
||||
for (;;) {
|
||||
if (!dotted || (step & 1u) == 0u)
|
||||
set_pixel(x0, y0);
|
||||
if (x0 == x1 && y0 == y1)
|
||||
break;
|
||||
const int e2 = 2 * err;
|
||||
if (e2 >= dy) { err += dy; x0 += sx; }
|
||||
if (e2 <= dx) { err += dx; y0 += sy; }
|
||||
step++;
|
||||
}
|
||||
}
|
||||
|
||||
static void clear_screen(void)
|
||||
{
|
||||
for (uint8_t x = 0; x < W; x++) {
|
||||
A->status_line[x] = 0;
|
||||
for (uint8_t p = 0; p < 7u; p++)
|
||||
A->fb[p][x] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Signed area of a face's projected polygon (<0 == facing us, calibrated). */
|
||||
static int face_area(const face_t *f)
|
||||
{
|
||||
int sa = 0;
|
||||
for (uint8_t k = 0; k < f->n; k++) {
|
||||
const uint8_t a = f->v[k];
|
||||
const uint8_t b = f->v[(k + 1u == f->n) ? 0u : k + 1u];
|
||||
sa += (int)px[a] * py[b] - (int)px[b] * py[a];
|
||||
}
|
||||
return sa;
|
||||
}
|
||||
|
||||
__attribute__((section(".text.entry"), used))
|
||||
void app_main(const app_api_t *api)
|
||||
{
|
||||
A = api;
|
||||
A->backlight_on();
|
||||
A->status_clear();
|
||||
|
||||
uint16_t ax = 0, ay = 0, az = 0; /* Q2 half-units: 2048 = full turn */
|
||||
uint8_t shape = 0;
|
||||
uint8_t speed = 4; /* 1..16, shared by all three rotation axes */
|
||||
bool paused = false;
|
||||
bool wire = true; /* false = solid (hidden-line) */
|
||||
bool running = true;
|
||||
uint8_t prevKey = APP_KEY_INVALID;
|
||||
|
||||
while (running) {
|
||||
uint8_t key = A->get_key();
|
||||
if (key == APP_KEY_SAVER) {
|
||||
prevKey = APP_KEY_INVALID;
|
||||
A->delay_ms(10);
|
||||
A->backlight_update();
|
||||
continue;
|
||||
}
|
||||
if (key == APP_KEY_WAKE)
|
||||
key = APP_KEY_INVALID;
|
||||
if (key != prevKey && key != APP_KEY_INVALID) {
|
||||
A->backlight_on();
|
||||
switch (key) {
|
||||
case APP_KEY_EXIT:
|
||||
running = false;
|
||||
break;
|
||||
case APP_KEY_UP:
|
||||
case APP_KEY_DOWN: {
|
||||
const int8_t dir = A->nav_dir(key);
|
||||
if (dir > 0 && speed < 16u) speed++;
|
||||
if (dir < 0 && speed > 1u) speed--;
|
||||
break;
|
||||
}
|
||||
case APP_KEY_MENU:
|
||||
paused = !paused;
|
||||
break;
|
||||
case APP_KEY_STAR:
|
||||
shape = (uint8_t)((shape + 1u) % NSHAPE);
|
||||
break;
|
||||
case APP_KEY_F:
|
||||
wire = !wire;
|
||||
break;
|
||||
case APP_KEY_1: case APP_KEY_2: case APP_KEY_3: case APP_KEY_4:
|
||||
case APP_KEY_5: case APP_KEY_6: case APP_KEY_7: case APP_KEY_8:
|
||||
if ((uint8_t)(key - APP_KEY_1) < NSHAPE)
|
||||
shape = (uint8_t)(key - APP_KEY_1);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
prevKey = key;
|
||||
if (!running)
|
||||
break;
|
||||
|
||||
const shape_t *s = &SHAPES[shape];
|
||||
const uint8_t ia = (uint8_t)(ax >> 3), ib = (uint8_t)(ay >> 3), ic = (uint8_t)(az >> 3);
|
||||
const int cx = sin8((uint8_t)(ia + 64u)), sxr = sin8(ia);
|
||||
const int cy = sin8((uint8_t)(ib + 64u)), syr = sin8(ib);
|
||||
const int cz = sin8((uint8_t)(ic + 64u)), szr = sin8(ic);
|
||||
|
||||
for (uint8_t i = 0; i < s->nv; i++) {
|
||||
int x = s->v[i][0], y = s->v[i][1], z = s->v[i][2];
|
||||
int ny = (y * cx - z * sxr) >> 14; /* Rx */
|
||||
int nz = (y * sxr + z * cx) >> 14;
|
||||
y = ny; z = nz;
|
||||
int nx = (x * cy + z * syr) >> 14; /* Ry */
|
||||
nz = (z * cy - x * syr) >> 14;
|
||||
x = nx; z = nz;
|
||||
nx = (x * cz - y * szr) >> 14; /* Rz */
|
||||
ny = (x * szr + y * cz) >> 14;
|
||||
x = nx; y = ny;
|
||||
|
||||
const int zc = z + DIST; /* always > 0 */
|
||||
px[i] = (int16_t)(CX + (x * FOCAL) / zc);
|
||||
py[i] = (int16_t)(CY + (y * FOCAL) / zc);
|
||||
pz[i] = (int16_t)z;
|
||||
}
|
||||
|
||||
clear_screen();
|
||||
for (uint8_t i = 0; i < s->nf; i++) {
|
||||
const face_t *f = &s->f[i];
|
||||
if (!wire && face_area(f) >= 0)
|
||||
continue; /* hidden face culled */
|
||||
for (uint8_t k = 0; k < f->n; k++) {
|
||||
const uint8_t a = f->v[k];
|
||||
const uint8_t b = f->v[(k + 1u == f->n) ? 0u : k + 1u];
|
||||
const bool dotted = wire && (pz[a] + pz[b] > 0); /* far half */
|
||||
draw_edge(px[a], py[a], px[b], py[b], dotted);
|
||||
}
|
||||
}
|
||||
|
||||
/* Shape name: inverse label, top-left of the status bar (scan-list look). */
|
||||
const uint8_t end = (uint8_t)(2u + 4u * s->namelen);
|
||||
for (uint8_t i = 0; i <= end; i++)
|
||||
A->status_line[i] = 0;
|
||||
A->print_inverse(s->name, 2, 0, true, true, end);
|
||||
|
||||
A->blit_status();
|
||||
A->blit_full();
|
||||
|
||||
if (!paused) {
|
||||
const uint16_t rate = ROT_RATE[speed - 1u];
|
||||
ax += rate;
|
||||
ay += (uint16_t)(rate + rate / 2u);
|
||||
az += (uint16_t)((rate + 1u) / 2u);
|
||||
}
|
||||
|
||||
A->backlight_update();
|
||||
A->delay_ms((uint32_t)(32u - speed * 2u)); /* slow low end, no added delay at level 16 */
|
||||
}
|
||||
}
|
||||
@@ -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.*) }
|
||||
}
|
||||
Executable
+42
@@ -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"
|
||||
@@ -0,0 +1,378 @@
|
||||
/* Copyright 2026 Armel F4HWN
|
||||
* https://github.com/armel
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Broadcast FM — overlay app. Sovereign: drives the BK1080 directly and owns the
|
||||
* radio while running (NO BK4819 dual-watch, unlike the resident FM). Reimplements
|
||||
* the full resident FM (fm.c / UI_DisplayFM) - VFO + 48 memories - sharing the
|
||||
* resident channel table (gFM_Channels) and config (gEeprom.FM_*), committed to
|
||||
* EEPROM on exit. The BK1080 audio is a hardware path, so playback needs no CPU loop.
|
||||
*
|
||||
* Keys (iso legacy): 0-9 = frequency (VFO) / channel (MR) entry · UP/DOWN = tune /
|
||||
* channel step · STAR = manual scan · F+STAR = auto-scan (stores 48) · MENU = save
|
||||
* (VFO) / delete (MR) · F+1 = band · F+3 = VFO<->MR · F+0 / EXIT = quit.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include "../app_api.h"
|
||||
|
||||
#define STEP 1
|
||||
#define SCAN_SETTLE 100
|
||||
#define CHMAX APP_FM_CH_MAX
|
||||
|
||||
static const char BAND_NAME[4][10] = { "87.5-108M", "76-108M", "76-90M", "64-76M" };
|
||||
static const uint8_t FONT_F[9] = {0x3e,0x7f,0x41,0x75,0x75,0x75,0x7d,0x7f,0x3e};
|
||||
static void cpy(uint8_t *d, const uint8_t *s, uint8_t n){ while(n--)*d++=*s++; }
|
||||
|
||||
static const app_api_t *A;
|
||||
static uint16_t *ch; /* shared gFM_Channels[48] */
|
||||
static app_fm_state_t st; /* freq_playing, sel_freq, band, is_mr, sel_ch */
|
||||
static uint16_t lo, hi;
|
||||
static int8_t scanState; /* 0 = off, +1 up, -1 down (non-blocking) */
|
||||
static bool autoScan;
|
||||
static uint8_t chPos;
|
||||
static uint16_t scanTimer; /* ms until the next scan step */
|
||||
static bool foundFreq;
|
||||
static bool askSave, askDelete;
|
||||
static uint8_t savePos;
|
||||
static bool fArm;
|
||||
static uint8_t inBox[4], inIdx;
|
||||
static char str[16];
|
||||
|
||||
static char *putu(char *o, uint32_t v){ char t[6]; int8_t n=0; do{t[n++]=(char)('0'+v%10);v/=10;}while(v&&n<6); while(n--)*o++=t[n]; return o; }
|
||||
static char *put(char *o, const char *s){ while(*s)*o++=*s++; return o; }
|
||||
|
||||
static void bandLimits(void){ lo=A->fm_lo(st.band); hi=A->fm_hi(st.band); }
|
||||
static uint16_t wrap(uint16_t f){ if(f<lo)return hi; if(f>hi)return lo; return f; }
|
||||
static void tune(void){ A->fm_set_freq(st.freq_playing, st.band); }
|
||||
static void dirty(void){ /* nothing yet; committed on exit */ }
|
||||
|
||||
static bool validCh(uint8_t c){ return c<CHMAX && ch[c]>=lo && ch[c]<hi; }
|
||||
static uint8_t findNext(uint8_t c, int8_t dir)
|
||||
{
|
||||
for(uint8_t i=0;i<CHMAX;i++){
|
||||
if(c==0xFF) c=CHMAX-1;
|
||||
else if(c>=CHMAX) c=0;
|
||||
if(validCh(c)) return c;
|
||||
c=(uint8_t)(c+dir);
|
||||
}
|
||||
return 0xFF;
|
||||
}
|
||||
static int configChannel(void)
|
||||
{
|
||||
st.freq_playing=st.sel_freq;
|
||||
if(st.is_mr){
|
||||
uint8_t c=findNext(st.sel_ch,+1);
|
||||
if(c==0xFF){ st.is_mr=false; return -1; }
|
||||
st.sel_ch=c; st.freq_playing=ch[c];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- drawing (mirror UI_DisplayFM) ---- */
|
||||
static void freqBig(uint16_t f)
|
||||
{
|
||||
uint16_t mhz=f/10u; char *o=str;
|
||||
if(mhz<100u)*o++=' ';
|
||||
o=putu(o,mhz); *o++='.'; *o++=(char)('0'+f%10u); *o='\0';
|
||||
A->display_freq(str,36,1,true);
|
||||
}
|
||||
/* append a zero-padded 2-digit number (1..99), return the end pointer */
|
||||
static char *num2(char *o, uint8_t n){ if(n<10)*o++='0'; return putu(o,n); }
|
||||
|
||||
static void draw(void)
|
||||
{
|
||||
char b[12], *o;
|
||||
|
||||
A->display_clear();
|
||||
A->status_clear();
|
||||
A->draw_battery();
|
||||
if(fArm) cpy(A->status_line+70,FONT_F,sizeof(FONT_F)); /* F armed indicator */
|
||||
A->print_string("FM",2,0,0,8);
|
||||
A->print_normal(BAND_NAME[st.band],1,0,6);
|
||||
|
||||
/* ---- status (line 3) ---- */
|
||||
if(askSave) { o=put(b,"SAVE?"); *o='\0'; }
|
||||
else if(askDelete) { o=put(b,"DEL?"); *o='\0'; }
|
||||
else if(scanState) {
|
||||
if(autoScan){ o=put(b,"A-SCAN("); o=putu(o,chPos); *o++=')'; } /* count like UI_DisplayFM */
|
||||
else o=put(b,"M-SCAN");
|
||||
*o='\0';
|
||||
}
|
||||
else if(st.is_mr) { o=put(b,"MR(CH"); o=num2(o,(uint8_t)(st.sel_ch+1)); *o++=')'; *o='\0'; }
|
||||
else {
|
||||
o=put(b,"VFO"); *o='\0';
|
||||
for(uint8_t i=0;i<CHMAX;i++) if(ch[i]==st.freq_playing){ o=put(b,"VFO(CH"); o=num2(o,(uint8_t)(i+1)); *o++=')'; *o='\0'; break; }
|
||||
}
|
||||
A->print_string(b,0,127,3,10);
|
||||
|
||||
/* ---- line 1: input in progress / channel prompt / frequency ---- */
|
||||
if(inIdx>0){
|
||||
if(st.is_mr||askSave){ /* channel number: "CH-XX" */
|
||||
o=put(b,"CH-");
|
||||
*o++=(char)('0'+inBox[0]);
|
||||
*o++=(inIdx>1)?(char)('0'+inBox[1]):'-';
|
||||
*o='\0';
|
||||
A->print_string(b,0,127,1,10);
|
||||
} else { /* VFO frequency: "ddd.d" */
|
||||
b[0]=(char)('0'+inBox[0]);
|
||||
b[1]=(inIdx>1)?(char)('0'+inBox[1]):'-';
|
||||
b[2]=(inIdx>2)?(char)('0'+inBox[2]):'-';
|
||||
b[3]='.';
|
||||
b[4]=(inIdx>3)?(char)('0'+inBox[3]):'-';
|
||||
b[5]='\0';
|
||||
A->display_freq(b,36,1,false);
|
||||
}
|
||||
} else if(askSave){ /* pick a save slot */
|
||||
o=put(b,"CH-"); o=num2(o,(uint8_t)(savePos+1)); *o='\0';
|
||||
A->print_string(b,0,127,1,10);
|
||||
} else if(askDelete){
|
||||
o=put(b,"CH-"); o=num2(o,(uint8_t)(st.sel_ch+1)); *o='\0';
|
||||
A->print_string(b,0,127,1,10);
|
||||
} else {
|
||||
freqBig(st.freq_playing);
|
||||
}
|
||||
}
|
||||
static void show(void){ draw(); A->blit_status(); A->blit_full(); }
|
||||
|
||||
/* ---- non-blocking scan state machine (mirrors ACTION_Scan_FM / FM_Play / FM_Tune) ---- */
|
||||
static void scanTuneNext(void) /* FM_Tune(freq, scanState, false): step + mute + settle */
|
||||
{
|
||||
A->fm_mute(true);
|
||||
foundFreq=false;
|
||||
st.freq_playing=wrap((uint16_t)(st.freq_playing+scanState));
|
||||
tune();
|
||||
scanTimer=SCAN_SETTLE;
|
||||
}
|
||||
static void stopScan(void) /* FM_PlayAndUpdate */
|
||||
{
|
||||
if(autoScan){ st.is_mr=true; st.sel_ch=0; }
|
||||
scanState=0; autoScan=false;
|
||||
configChannel();
|
||||
tune();
|
||||
A->fm_mute(false);
|
||||
}
|
||||
static void scanStep(void) /* FM_Play: one step when the settle timer expires */
|
||||
{
|
||||
if(A->fm_valid(st.freq_playing,lo)==0){ /* station locked */
|
||||
if(!autoScan){
|
||||
scanState=0; foundFreq=true;
|
||||
if(!st.is_mr) st.sel_freq=st.freq_playing;
|
||||
A->fm_mute(false); /* audio on */
|
||||
return;
|
||||
}
|
||||
if(chPos<CHMAX) ch[chPos++]=st.freq_playing;
|
||||
if(chPos>=CHMAX){ stopScan(); return; }
|
||||
}
|
||||
if(autoScan && st.freq_playing>=A->fm_hi(1)){ stopScan(); return; }
|
||||
scanTuneNext();
|
||||
}
|
||||
static void startManual(int8_t dir)
|
||||
{
|
||||
if(scanState){ stopScan(); return; } /* STAR toggles */
|
||||
autoScan=false; chPos=0; scanState=dir;
|
||||
scanTuneNext();
|
||||
}
|
||||
static void startAuto(void)
|
||||
{
|
||||
if(scanState){ stopScan(); return; }
|
||||
autoScan=true; chPos=0; scanState=+1;
|
||||
for(uint8_t i=0;i<CHMAX;i++) ch[i]=0xFFFF;
|
||||
st.freq_playing=lo;
|
||||
A->fm_mute(true);
|
||||
tune(); /* FM_Tune(lo,1,true): no step */
|
||||
scanTimer=SCAN_SETTLE;
|
||||
}
|
||||
|
||||
/* ---- digit entry ---- */
|
||||
static void digit(uint8_t d)
|
||||
{
|
||||
if(askDelete||scanState) return;
|
||||
if(askSave || st.is_mr){ /* 2-digit channel */
|
||||
if(inIdx<2) inBox[inIdx++]=d;
|
||||
if(inIdx>=2){
|
||||
uint8_t c=(uint8_t)(inBox[0]*10+inBox[1]-1); inIdx=0;
|
||||
if(askSave){ if(c<CHMAX) savePos=c; }
|
||||
else if(validCh(c)){ st.sel_ch=c; st.freq_playing=ch[c]; tune(); }
|
||||
}
|
||||
return;
|
||||
}
|
||||
/* VFO frequency: 4 digits, with the leading-digit>1 zero-pad trick */
|
||||
inBox[inIdx++]=d;
|
||||
if(inIdx==1 && inBox[0]>1){ inBox[1]=inBox[0]; inBox[0]=0; inIdx=2; }
|
||||
else if(inIdx>3){
|
||||
uint16_t f=(uint16_t)(inBox[0]*1000+inBox[1]*100+inBox[2]*10+inBox[3]); inIdx=0;
|
||||
if(f>=lo && f<=hi){ st.sel_freq=f; st.freq_playing=f; tune(); }
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- MENU: save (VFO) / delete (MR) ---- */
|
||||
static void menu(void)
|
||||
{
|
||||
inIdx=0;
|
||||
if(!st.is_mr){ /* VFO: save */
|
||||
if(askSave){ ch[savePos]=st.freq_playing; dirty(); }
|
||||
askSave=!askSave;
|
||||
} else { /* MR: delete */
|
||||
if(askDelete){ ch[st.sel_ch]=0xFFFF; configChannel(); tune(); dirty(); }
|
||||
askDelete=!askDelete;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- UP/DOWN: tune (VFO) or channel step (MR) ---- */
|
||||
static void upDown(int8_t step)
|
||||
{
|
||||
if(scanState){ scanState=step; scanTuneNext(); return; } /* continue scan, new direction */
|
||||
if(askSave){
|
||||
if(step>0){ if(++savePos>=CHMAX) savePos=0; }
|
||||
else savePos=savePos?(uint8_t)(savePos-1u):(uint8_t)(CHMAX-1u);
|
||||
return;
|
||||
}
|
||||
if(st.is_mr){
|
||||
uint8_t c=findNext((uint8_t)(st.sel_ch+step),step);
|
||||
if(c!=0xFF && c!=st.sel_ch){ st.sel_ch=c; st.freq_playing=ch[c]; tune(); }
|
||||
} else {
|
||||
st.sel_freq=wrap((uint16_t)(st.sel_freq+step));
|
||||
st.freq_playing=st.sel_freq; tune();
|
||||
}
|
||||
}
|
||||
|
||||
static void toggleMr(void)
|
||||
{
|
||||
st.is_mr=!st.is_mr;
|
||||
if(configChannel()!=0){ /* no valid channel: stayed VFO */ }
|
||||
tune();
|
||||
askSave=askDelete=false; inIdx=0;
|
||||
}
|
||||
|
||||
static bool running;
|
||||
|
||||
static void cycleBand(void)
|
||||
{
|
||||
st.band=(uint8_t)((st.band+1)&3u); bandLimits();
|
||||
if(st.freq_playing<lo||st.freq_playing>hi){ st.freq_playing=lo; st.sel_freq=lo; }
|
||||
tune();
|
||||
}
|
||||
/* F+key and long-press functions (band / VFO<->MR / auto-scan / quit). */
|
||||
static void func(uint8_t key)
|
||||
{
|
||||
switch(key){
|
||||
case APP_KEY_0: running=false; break;
|
||||
case APP_KEY_1: cycleBand(); break;
|
||||
case APP_KEY_3: toggleMr(); break;
|
||||
case APP_KEY_STAR: startAuto(); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
static void onShort(uint8_t key)
|
||||
{
|
||||
if(key==APP_KEY_F){ fArm=!fArm; return; }
|
||||
if(fArm){ fArm=false; func(key); return; } /* F + key */
|
||||
if(key<=APP_KEY_9){ digit(key); return; }
|
||||
switch(key){
|
||||
case APP_KEY_UP:
|
||||
case APP_KEY_DOWN: upDown(A->nav_dir(key)); break;
|
||||
case APP_KEY_STAR: startManual(+1); break;
|
||||
case APP_KEY_MENU: menu(); break;
|
||||
case APP_KEY_EXIT:
|
||||
if(inIdx) inIdx--;
|
||||
else if(askSave||askDelete) askSave=askDelete=false;
|
||||
else running=false;
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
static void onLong(uint8_t key){ fArm=false; func(key); } /* long band / MR / scan / quit */
|
||||
|
||||
__attribute__((section(".text.entry"),used))
|
||||
void app_main(const app_api_t *api)
|
||||
{
|
||||
A=api;
|
||||
A->backlight_on();
|
||||
ch=A->fm_channels;
|
||||
A->fm_state(&st,false); /* inherit the resident FM state */
|
||||
if(st.band>3) st.band=0;
|
||||
bandLimits();
|
||||
if(st.freq_playing<lo||st.freq_playing>hi) st.freq_playing=lo;
|
||||
if(st.sel_freq<lo||st.sel_freq>hi) st.sel_freq=st.freq_playing;
|
||||
if(st.is_mr) configChannel();
|
||||
askSave=askDelete=fArm=false; inIdx=0; savePos=0;
|
||||
|
||||
A->fm_enter(st.freq_playing,st.band);
|
||||
|
||||
/* short = on release (held < ~0.5 s) · long = at the 0.5 s threshold ·
|
||||
UP/DOWN = immediate step on press, then auto-repeat while held. */
|
||||
running=true;
|
||||
uint8_t held=APP_KEY_INVALID;
|
||||
uint16_t heldMs=0;
|
||||
bool firedLong=false;
|
||||
bool dirty=true; /* redraw only on change */
|
||||
|
||||
while(running){
|
||||
uint8_t key=A->get_key();
|
||||
bool rep=(key==APP_KEY_UP||key==APP_KEY_DOWN);
|
||||
|
||||
if(key==APP_KEY_SAVER){
|
||||
held=APP_KEY_INVALID; heldMs=0; firedLong=false;
|
||||
A->delay_ms(10); A->backlight_update();
|
||||
continue;
|
||||
} else if(key==APP_KEY_WAKE||key==APP_KEY_PTT){
|
||||
held=APP_KEY_INVALID; heldMs=0; firedLong=false; dirty=true;
|
||||
} else if(key==APP_KEY_INVALID){
|
||||
if(held!=APP_KEY_INVALID && !firedLong &&
|
||||
held!=APP_KEY_UP && held!=APP_KEY_DOWN){
|
||||
onShort(held); dirty=true;
|
||||
}
|
||||
held=APP_KEY_INVALID; heldMs=0; firedLong=false;
|
||||
} else if(key!=held){
|
||||
A->backlight_on();
|
||||
held=key; heldMs=0; firedLong=false;
|
||||
if(rep){ onShort(key); dirty=true; } /* immediate first step */
|
||||
} else {
|
||||
heldMs=(uint16_t)(heldMs+50u);
|
||||
if(rep){
|
||||
if(heldMs>=300){ onShort(held); dirty=true; } /* auto-repeat */
|
||||
} else if(!firedLong && heldMs>=400){
|
||||
firedLong=true; onLong(held); dirty=true;
|
||||
}
|
||||
}
|
||||
|
||||
/* non-blocking scan: one step each time the settle timer expires */
|
||||
if(scanState){
|
||||
/* Match resident FM: an active scan keeps the display awake. */
|
||||
A->backlight_on();
|
||||
if(scanTimer<=50) scanStep();
|
||||
else scanTimer=(uint16_t)(scanTimer-50);
|
||||
dirty=true;
|
||||
}
|
||||
|
||||
if(dirty){ show(); dirty=false; }
|
||||
/* Keep the resident 10 ms fade cadence while retaining this app's
|
||||
* existing 50 ms key/scan state-machine tick. */
|
||||
for(uint8_t i=0;i<5u;i++){
|
||||
A->delay_ms(10);
|
||||
A->backlight_update();
|
||||
}
|
||||
}
|
||||
|
||||
/* push our state back to the resident FM and persist (config + 48 channels) */
|
||||
if(!st.is_mr) st.sel_freq=st.freq_playing;
|
||||
A->fm_state(&st,true);
|
||||
A->fm_commit();
|
||||
A->fm_exit();
|
||||
}
|
||||
@@ -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.*) }
|
||||
}
|
||||
Executable
+42
@@ -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"
|
||||
@@ -0,0 +1,375 @@
|
||||
/* Copyright 2026 Armel F4HWN
|
||||
* https://github.com/armel
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
* FoxHunt (RX) — overlay app. Ported from App/app/foxhunt.c, hunt sub-mode only
|
||||
* (the beacon becomes a separate app). Signal-strength hunter: corrected dBm with
|
||||
* a big number, an IARU S-meter staircase or a scrolling history, peak/min hold,
|
||||
* a 1 s trend, a front-end attenuator ladder, and Geiger/station audio.
|
||||
*
|
||||
* Keys: 1 gauge (bar/history) · 2 audio (off/beep/station) · 3 + UP/DOWN attenuator
|
||||
* MENU reset holds · long F keypad lock · EXIT quit.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include "../app_api.h"
|
||||
|
||||
#define LCD_WIDTH 128
|
||||
#define BK_REG_13 0x13
|
||||
|
||||
/* --- tuning (from foxhunt.c) --- */
|
||||
#define DBM_FLOOR (-141)
|
||||
#define DBM_CEIL (-53)
|
||||
#define TICK_MS 50
|
||||
#define TREND_TICKS 20
|
||||
#define LOCK_HOLD_MS 500
|
||||
#define SILENCE_DBM (-120)
|
||||
#define TONE_MIN 400
|
||||
#define TONE_MAX 2400
|
||||
#define RATE_SLOW 20
|
||||
#define RATE_FAST 2
|
||||
#define BLIP_MS 50
|
||||
#define AUDIO_SETTLE 60
|
||||
#define ATT_SETTLE 40
|
||||
|
||||
#define ATT_COUNT 6
|
||||
#define ATT_BYP0 4
|
||||
#define SEG_COUNT 13
|
||||
#define BAR_X0 6
|
||||
#define SEG_PITCH 9
|
||||
#define SEG_W 8
|
||||
#define SEG_BOTTOM 37
|
||||
|
||||
#define HIST_LEN 120
|
||||
#define HIST_DECIM 3
|
||||
#define GRAPH_X0 4
|
||||
#define GRAPH_TOP 27
|
||||
#define GRAPH_BOT 45
|
||||
#define GRAPH_FLOOR 2
|
||||
|
||||
#define AUDIO_OFF 0
|
||||
#define AUDIO_BEEP 1
|
||||
#define AUDIO_STATION 2
|
||||
#define GRAPH_BAR 0
|
||||
#define GRAPH_HIST 1
|
||||
|
||||
#define CFG_MAGIC 0xF1 /* config validity marker */
|
||||
|
||||
static const uint16_t ATT_REG13[ATT_COUNT] = { 0x03DF, 0x03DD, 0x03DB, 0x03D9, 0x0379, 0x0139 };
|
||||
static const uint8_t ATT_DB[ATT_BYP0] = { 0, 6, 15, 27 };
|
||||
|
||||
/* Status-bar / trend icons (bytes copied verbatim from App/bitmaps.c). */
|
||||
static const uint8_t BMP_BARS[11] = {0x40,0x40,0x00,0x70,0x70,0x00,0x7c,0x7c,0x00,0x7f,0x7f};
|
||||
static const uint8_t BMP_GRAPH[15] = {0x08,0x04,0x02,0x04,0x08,0x10,0x20,0x10,0x08,0x04,0x02,0x04,0x08,0x10,0x20};
|
||||
static const uint8_t BMP_SIGNAL[10] = {0x08,0x1c,0x1c,0x08,0x00,0x22,0x1c,0x41,0x22,0x1c};
|
||||
static const uint8_t BMP_SPEAKER[10] = {0x1c,0x1c,0x3e,0x7f,0x00,0x22,0x1c,0x41,0x22,0x1c};
|
||||
static const uint8_t BMP_UP[11] = {0x20,0x30,0x38,0x3c,0x3e,0x3f,0x3e,0x3c,0x38,0x30,0x20};
|
||||
static const uint8_t BMP_DOWN[11] = {0x01,0x03,0x07,0x0f,0x1f,0x3f,0x1f,0x0f,0x07,0x03,0x01};
|
||||
static const uint8_t BMP_FLAT[11] = {0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12};
|
||||
static const uint8_t FONT_F[9] = {0x3e,0x7f,0x41,0x75,0x75,0x75,0x7d,0x7f,0x3e};
|
||||
static const uint8_t FONT_LOCK[9] = {0x7c,0x46,0x45,0x45,0x45,0x45,0x45,0x46,0x7c};
|
||||
|
||||
static void cpy(uint8_t *d, const uint8_t *s, uint8_t n){ while(n--)*d++=*s++; }
|
||||
|
||||
static const app_api_t *A;
|
||||
|
||||
static bool foxLocked, fArm, fLongDone;
|
||||
static uint16_t fHoldMs;
|
||||
static uint8_t foxAudioMode, foxGraphMode, attStep, audioTick;
|
||||
static int16_t curDbm, peakDbm, minDbm, trendRef, trendDelta;
|
||||
static uint8_t trendTick;
|
||||
static uint8_t histBuf[HIST_LEN];
|
||||
static uint8_t histHead, histTick;
|
||||
static int16_t histEma;
|
||||
static uint8_t prevKey;
|
||||
static bool running;
|
||||
static char str[16];
|
||||
|
||||
/* ---- tiny formatting ---- */
|
||||
static uint8_t slen(const char *s){ uint8_t n=0; while(s[n])n++; return n; }
|
||||
static char *put(char *o,const char *s){ while(*s)*o++=*s++; return o; }
|
||||
static char *puti(char *o,int v){
|
||||
uint32_t u;
|
||||
if(v<0){*o++='-';u=(uint32_t)(-v);} else u=(uint32_t)v;
|
||||
char t[6]; int8_t n=0;
|
||||
do{t[n++]=(char)('0'+u%10u);u/=10u;}while(u&&n<6);
|
||||
while(n--)*o++=t[n];
|
||||
return o;
|
||||
}
|
||||
static void i2str(char *out,int v){ *puti(out,v)='\0'; }
|
||||
|
||||
/* ---- radio helpers ---- */
|
||||
static int16_t iabs16(int16_t v){ return v<0?-v:v; }
|
||||
|
||||
static void applyAtt(void){
|
||||
uint16_t reg = A->bk_read(BK_REG_13);
|
||||
reg = (uint16_t)((reg & ~0x03FFu) | ATT_REG13[attStep]);
|
||||
A->bk_write(BK_REG_13, reg);
|
||||
}
|
||||
|
||||
static void setAudio(void){
|
||||
if(foxAudioMode==AUDIO_OFF){ A->audio_path(false); return; }
|
||||
A->audio_path(true);
|
||||
A->delay_ms(AUDIO_SETTLE);
|
||||
A->set_af(APP_AF_MUTE);
|
||||
}
|
||||
|
||||
static int32_t lerp(int16_t dbm,int32_t lo,int32_t hi){
|
||||
if(dbm<=DBM_FLOOR) return lo;
|
||||
if(dbm>=DBM_CEIL) return hi;
|
||||
const int32_t delta = hi - lo;
|
||||
const uint32_t distance = ((uint32_t)(dbm-DBM_FLOOR) *
|
||||
(uint32_t)(delta < 0 ? -delta : delta)) /
|
||||
(uint32_t)(DBM_CEIL-DBM_FLOOR);
|
||||
return lo + (delta < 0 ? -(int32_t)distance : (int32_t)distance);
|
||||
}
|
||||
static void blip(uint16_t freq){
|
||||
A->prepare_tone();
|
||||
A->play_tone_raw(freq, BLIP_MS);
|
||||
A->tones_off_rx();
|
||||
A->set_agc(false);
|
||||
applyAtt();
|
||||
}
|
||||
|
||||
static uint8_t fillCount(int16_t dbm){
|
||||
int16_t n;
|
||||
if(dbm<-141) return 0;
|
||||
if(dbm<=-93) n=(int16_t)(1u+(uint16_t)(dbm+141)/6u);
|
||||
else n=(int16_t)(9u+(uint16_t)(dbm+93)/10u);
|
||||
if(n>SEG_COUNT) n=SEG_COUNT;
|
||||
return (uint8_t)n;
|
||||
}
|
||||
static void buildS(char *out,int16_t dbm){
|
||||
if(dbm>=-93){ int16_t o=dbm-(-93); if(o>40)o=40; char *p=put(out,"S9+"); if(o<10)*p++='0'; i2str(p,o); }
|
||||
else if(dbm<-141) put(out,"S0")[0]='\0';
|
||||
else { char *p=put(out,"S"); i2str(p,(int)((uint16_t)(dbm+147)/6u)); }
|
||||
}
|
||||
|
||||
static int16_t div_trunc_pow2(int32_t v, uint8_t shift)
|
||||
{
|
||||
if (v < 0)
|
||||
return (int16_t)-((uint32_t)(-v) >> shift);
|
||||
return (int16_t)((uint32_t)v >> shift);
|
||||
}
|
||||
|
||||
static uint8_t cycleIndex(uint8_t value, uint8_t count, int8_t dir)
|
||||
{
|
||||
if (dir > 0)
|
||||
return ++value < count ? value : 0u;
|
||||
return value > 0u ? (uint8_t)(value - 1u) : (uint8_t)(count - 1u);
|
||||
}
|
||||
|
||||
static void histSample(void){
|
||||
histEma += div_trunc_pow2((int32_t)curDbm * 8 - histEma, 2u);
|
||||
if(++histTick<HIST_DECIM) return;
|
||||
histTick=0;
|
||||
histBuf[histHead]=fillCount(div_trunc_pow2(histEma, 3u));
|
||||
if(++histHead>=HIST_LEN) histHead=0;
|
||||
}
|
||||
static void rebase(void){
|
||||
curDbm=A->rssi_dbm();
|
||||
peakDbm=minDbm=trendRef=curDbm;
|
||||
trendDelta=0; trendTick=0;
|
||||
uint8_t lvl=fillCount(curDbm);
|
||||
for(uint8_t i=0;i<HIST_LEN;i++) histBuf[i]=lvl;
|
||||
histHead=0; histTick=0; histEma=(int16_t)(curDbm*8);
|
||||
}
|
||||
static void attCycle(int8_t dir){
|
||||
attStep=cycleIndex(attStep,ATT_COUNT,dir);
|
||||
applyAtt();
|
||||
A->delay_ms(ATT_SETTLE);
|
||||
rebase();
|
||||
}
|
||||
|
||||
/* ---- drawing ---- */
|
||||
static void fillRect(int16_t x0,int16_t y0,int16_t x1,int16_t y1){
|
||||
for(int16_t x=x0;x<=x1;x++) A->draw_line(A->fb,x,y0,x,y1,true);
|
||||
}
|
||||
static void drawBar(void){
|
||||
uint8_t n=fillCount(curDbm);
|
||||
for(uint8_t i=0;i<n;i++){
|
||||
int16_t sx=BAR_X0+i*SEG_PITCH;
|
||||
fillRect(sx,SEG_BOTTOM-i,sx+SEG_W-1,SEG_BOTTOM);
|
||||
}
|
||||
A->draw_line(A->fb,BAR_X0,SEG_BOTTOM+2,BAR_X0+(SEG_COUNT-1)*SEG_PITCH+SEG_W-1,SEG_BOTTOM+2,true);
|
||||
}
|
||||
static void drawHist(void){
|
||||
const uint8_t floorY=GRAPH_BOT-GRAPH_FLOOR;
|
||||
const uint8_t span=floorY-GRAPH_TOP;
|
||||
A->draw_line(A->fb,GRAPH_X0,GRAPH_BOT,GRAPH_X0+HIST_LEN-1,GRAPH_BOT,true);
|
||||
uint8_t prevY=0, idx=histHead;
|
||||
for(uint8_t c=0;c<HIST_LEN;c++){
|
||||
uint8_t lvl=histBuf[idx]; if(++idx>=HIST_LEN)idx=0;
|
||||
if(lvl>SEG_COUNT)lvl=SEG_COUNT;
|
||||
uint8_t x=GRAPH_X0+c;
|
||||
uint8_t y=(uint8_t)(floorY-((uint32_t)lvl*span)/(uint32_t)SEG_COUNT);
|
||||
for(uint8_t yy=y+1;yy<=floorY;yy++) if(((x+yy)&1)==0) A->draw_line(A->fb,x,yy,x,yy,true);
|
||||
if(c==0){ A->draw_line(A->fb,x,y,x,y,true); }
|
||||
else { uint8_t lo=(y<prevY)?y:prevY, hi=(y<prevY)?prevY:y; A->draw_line(A->fb,x,lo,x,hi,true); }
|
||||
prevY=y;
|
||||
}
|
||||
}
|
||||
static void tag(const char *s,uint8_t x,uint8_t line){
|
||||
A->print_inverse(s,x,line,false,true,(uint8_t)(x+slen(s)*4));
|
||||
}
|
||||
static void draw(void){
|
||||
char big[8], sMeter[8];
|
||||
|
||||
A->display_clear();
|
||||
A->status_clear();
|
||||
A->print_inverse("FOX HUNT",2,0,true,true,34);
|
||||
A->draw_battery();
|
||||
|
||||
/* status-bar icons: graph mode @38, audio mode @55, F/lock @69 */
|
||||
if(foxGraphMode==GRAPH_HIST) cpy(A->status_line+38,BMP_GRAPH,15);
|
||||
else cpy(A->status_line+38,BMP_BARS,11);
|
||||
if(foxAudioMode==AUDIO_BEEP) cpy(A->status_line+55,BMP_SIGNAL,10);
|
||||
else if(foxAudioMode==AUDIO_STATION) cpy(A->status_line+55,BMP_SPEAKER,10);
|
||||
if(foxLocked) cpy(A->status_line+70,FONT_LOCK,9);
|
||||
else if(fArm) cpy(A->status_line+70,FONT_F,sizeof(FONT_F));
|
||||
|
||||
i2str(big,curDbm);
|
||||
A->display_freq(big,2,0,false);
|
||||
A->print_normal("dBm",(uint8_t)(slen(big)*13+4),0,1);
|
||||
|
||||
/* trend arrow (line 0, right) + signed delta (line 1) */
|
||||
cpy(A->fb[0]+115, trendDelta>0?BMP_UP:trendDelta<0?BMP_DOWN:BMP_FLAT, 11);
|
||||
if(trendDelta!=0){
|
||||
char *p=str; if(trendDelta>0)*p++='+'; else{*p++='-';}
|
||||
int16_t a=iabs16(trendDelta); if(a<10){*p++='0';} i2str(p,a);
|
||||
A->print_normal("dBm",127-3*7,0,1);
|
||||
A->print_normal(str,(uint8_t)(127-3*7-2-slen(str)*7),1,1);
|
||||
}
|
||||
|
||||
i2str(put(str,"PK "),peakDbm);
|
||||
tag(str,4,2);
|
||||
i2str(put(str,"MN "),minDbm);
|
||||
tag(str,(uint8_t)(((uint32_t)LCD_WIDTH-(uint32_t)slen(str)*4u)/2u),2);
|
||||
buildS(sMeter,curDbm);
|
||||
A->print_inverse(sMeter,(uint8_t)(126-slen(sMeter)*4),2,false,true,125);
|
||||
|
||||
if(foxGraphMode==GRAPH_HIST) drawHist();
|
||||
else drawBar();
|
||||
|
||||
if(attStep<ATT_BYP0){ char *o=put(str,"ATT "); o=puti(o,ATT_DB[attStep]); put(o,"dB")[0]='\0'; }
|
||||
else put(str,(attStep==ATT_BYP0)?"BYP":"BYP+")[0]='\0';
|
||||
tag(str,4,6);
|
||||
|
||||
{ uint32_t f=A->rx_freq(); char *o=puti(str,(int)(f/100000u)); *o++='.';
|
||||
uint32_t fr=f%100000u; for(int8_t d=4;d>=0;d--){ uint32_t p=1; for(int8_t k=0;k<d;k++)p*=10; *o++=(char)('0'+(fr/p)%10);} *o='\0'; }
|
||||
A->print_normal(str,(uint8_t)(126-slen(str)*7),0,6);
|
||||
}
|
||||
|
||||
/* ---- config (deferred) ---- */
|
||||
static void loadConfig(void){
|
||||
uint8_t c[4];
|
||||
A->cfg_load(c,4);
|
||||
if(c[0]==CFG_MAGIC){
|
||||
if(c[1]<ATT_COUNT) attStep=c[1];
|
||||
if(c[2]<=GRAPH_HIST) foxGraphMode=c[2];
|
||||
if(c[3]<=AUDIO_STATION) foxAudioMode=c[3];
|
||||
}
|
||||
}
|
||||
static void saveConfig(void){
|
||||
uint8_t c[4]={CFG_MAGIC,attStep,foxGraphMode,foxAudioMode};
|
||||
A->cfg_save(c,4);
|
||||
}
|
||||
|
||||
/* ---- input ---- */
|
||||
static void handleKeys(void){
|
||||
uint8_t key=A->get_key();
|
||||
|
||||
/* long-press F -> keypad lock */
|
||||
if(key==APP_KEY_F){
|
||||
if(!fLongDone){ fHoldMs+=TICK_MS; if(fHoldMs>=LOCK_HOLD_MS){ fLongDone=true; foxLocked=!foxLocked; fArm=false; A->backlight_on(); } }
|
||||
} else { fHoldMs=0; fLongDone=false; }
|
||||
|
||||
if(key==APP_KEY_INVALID||key==prevKey){ prevKey=key; return; }
|
||||
prevKey=key;
|
||||
A->backlight_on();
|
||||
|
||||
if(foxLocked){
|
||||
if(key==APP_KEY_UP||key==APP_KEY_DOWN) attCycle(A->nav_dir(key));
|
||||
return;
|
||||
}
|
||||
if(key==APP_KEY_F){ fArm=!fArm; return; }
|
||||
int8_t dir=fArm?-1:1;
|
||||
|
||||
switch(key){
|
||||
case APP_KEY_EXIT: running=false; break;
|
||||
case APP_KEY_1: foxGraphMode^=1; break;
|
||||
case APP_KEY_2:
|
||||
foxAudioMode=cycleIndex(foxAudioMode,3u,dir); setAudio();
|
||||
if(foxAudioMode==AUDIO_BEEP){ audioTick=RATE_SLOW; }
|
||||
break;
|
||||
case APP_KEY_3: attCycle(dir); break;
|
||||
case APP_KEY_UP:
|
||||
case APP_KEY_DOWN: attCycle(A->nav_dir(key)); break;
|
||||
case APP_KEY_MENU: peakDbm=minDbm=trendRef=curDbm; break;
|
||||
default: break;
|
||||
}
|
||||
fArm=false;
|
||||
}
|
||||
|
||||
static void tickDelay(void){
|
||||
for(uint8_t i=0;i<TICK_MS/10;i++){ A->delay_ms(10); A->backlight_update(); }
|
||||
}
|
||||
|
||||
__attribute__((section(".text.entry"),used))
|
||||
void app_main(const app_api_t *api){
|
||||
A=api;
|
||||
foxLocked=fArm=fLongDone=false; fHoldMs=0;
|
||||
attStep=0; foxGraphMode=GRAPH_BAR; foxAudioMode=AUDIO_OFF;
|
||||
prevKey=APP_KEY_INVALID;
|
||||
|
||||
loadConfig();
|
||||
A->backlight_on();
|
||||
A->set_agc(false);
|
||||
applyAtt();
|
||||
setAudio();
|
||||
A->delay_ms(ATT_SETTLE);
|
||||
rebase();
|
||||
|
||||
running=true;
|
||||
while(running){
|
||||
handleKeys();
|
||||
if(!running) break;
|
||||
|
||||
curDbm=A->rssi_dbm();
|
||||
if(curDbm>peakDbm) peakDbm=curDbm;
|
||||
if(curDbm<minDbm) minDbm=curDbm;
|
||||
if(++trendTick>=TREND_TICKS){ trendDelta=curDbm-trendRef; trendRef=curDbm; trendTick=0; }
|
||||
histSample();
|
||||
|
||||
draw();
|
||||
A->blit_status();
|
||||
A->blit_full();
|
||||
|
||||
if(foxAudioMode==AUDIO_BEEP && curDbm>=SILENCE_DBM){
|
||||
if(++audioTick>=(uint8_t)lerp(curDbm,RATE_SLOW,RATE_FAST)){ audioTick=0; blip((uint16_t)lerp(curDbm,TONE_MIN,TONE_MAX)); }
|
||||
} else if(foxAudioMode==AUDIO_STATION){
|
||||
A->set_af(curDbm>=SILENCE_DBM?APP_AF_FM:APP_AF_MUTE);
|
||||
}
|
||||
|
||||
A->battery_sample();
|
||||
tickDelay();
|
||||
}
|
||||
|
||||
saveConfig();
|
||||
A->audio_path(false);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python3
|
||||
# Pack an overlay-app raw binary into a .app blob: 64-byte header + code.
|
||||
# Header layout mirrors app_overlay.h : app_header_t (little-endian, packed).
|
||||
# The format/version constants (magic, header + ABI/API levels, overlay budget) are
|
||||
# read straight from the C headers the firmware itself compiles, so there is a
|
||||
# SINGLE source of truth - the packer can never silently drift from the loader.
|
||||
# CRC-32 is zlib/PKZIP (init 0xFFFFFFFF, poly 0xEDB88320, final XOR) to match
|
||||
# the firmware's mb_ext_image_crc32 / mb_crc32_bytes.
|
||||
#
|
||||
# ./pack_app.py breakout/breakout.bin breakout/breakout.app --name Breakout \
|
||||
# --ver 1.0 --vma 0x20000280 --api-min 1
|
||||
import argparse, os, re, struct, zlib, sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
def cdefine(header: str, name: str) -> int:
|
||||
"""Value of a `#define <name> <int-literal>` in a sibling C header.
|
||||
Accepts a decimal or 0x-hex literal with optional u/U/l/L suffixes. Keeps the
|
||||
blob format tied to the firmware's own headers (single source of truth)."""
|
||||
pat = re.compile(r"^\s*#define\s+" + re.escape(name) +
|
||||
r"\s+(0[xX][0-9a-fA-F]+|\d+)[uUlL]*\b")
|
||||
with open(os.path.join(HERE, header)) as f:
|
||||
for line in f:
|
||||
m = pat.match(line)
|
||||
if m:
|
||||
return int(m.group(1), 0)
|
||||
sys.exit(f"{header}: #define {name} not found")
|
||||
|
||||
# Single source of truth: the C headers the firmware also compiles.
|
||||
MAGIC = cdefine("app_overlay.h", "APP_MAGIC").to_bytes(4, "little") # 0x31504146 -> b"FAP1"
|
||||
HDR_VERSION = cdefine("app_overlay.h", "APP_HDR_VERSION")
|
||||
ABI_MAJOR = cdefine("app_api.h", "APP_ABI_MAJOR")
|
||||
API_LEVEL = cdefine("app_api.h", "APP_API_LEVEL")
|
||||
OVERLAY_MAX = cdefine("app_overlay.h", "APP_OVERLAY_MAX") # 4 KiB overlay budget
|
||||
FLAG_COMMITTED = cdefine("app_overlay.h", "APP_FLAG_COMMITTED")
|
||||
FLAG_SCREEN_SAVER = cdefine("app_overlay.h", "APP_FLAG_SCREEN_SAVER")
|
||||
FLAG_SHORTCUT_SHIFT = cdefine("app_overlay.h", "APP_FLAG_SHORTCUT_SHIFT")
|
||||
SHORTCUTS = {
|
||||
"none": 0,
|
||||
"fm": cdefine("app_overlay.h", "APP_SHORTCUT_FM"),
|
||||
"foxhunt": cdefine("app_overlay.h", "APP_SHORTCUT_FOXHUNT"),
|
||||
"beacon": cdefine("app_overlay.h", "APP_SHORTCUT_BEACON"),
|
||||
"beam": cdefine("app_overlay.h", "APP_SHORTCUT_BEAM"),
|
||||
}
|
||||
CAPABILITIES = {
|
||||
"fm": cdefine("app_overlay.h", "APP_CAP_FM"),
|
||||
"trivfo": cdefine("app_overlay.h", "APP_CAP_TRIVFO"),
|
||||
"beam": cdefine("app_overlay.h", "APP_CAP_BEAM"),
|
||||
}
|
||||
|
||||
def field(s: str, n: int) -> bytes:
|
||||
b = s.encode("ascii", "strict")[: n - 1]
|
||||
return b + b"\x00" * (n - len(b))
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("infile")
|
||||
ap.add_argument("outfile")
|
||||
ap.add_argument("--name", default="app")
|
||||
ap.add_argument("--ver", default="1.0")
|
||||
ap.add_argument("--entry", type=lambda x: int(x, 0), default=0)
|
||||
ap.add_argument("--api-min", type=int, required=True,
|
||||
help="minimum append-only firmware API level required by this app")
|
||||
ap.add_argument("--vma", type=lambda x: int(x, 0), required=True,
|
||||
help="RAM VMA the app was linked at (must match the firmware overlay)")
|
||||
ap.add_argument("--screensaver", action="store_true",
|
||||
help="allow the resident BLTime screen saver while this app is idle")
|
||||
ap.add_argument("--shortcut", choices=SHORTCUTS, default="none",
|
||||
help="resident quick action advertised by this app")
|
||||
ap.add_argument("--require", action="append", choices=CAPABILITIES, default=[],
|
||||
help="resident capability required by this app (repeatable)")
|
||||
a = ap.parse_args()
|
||||
|
||||
if not 1 <= a.api_min <= API_LEVEL:
|
||||
sys.exit(f"--api-min must be between 1 and current API level {API_LEVEL}")
|
||||
|
||||
code = open(a.infile, "rb").read()
|
||||
if len(code) == 0:
|
||||
sys.exit("empty input")
|
||||
if len(code) > OVERLAY_MAX:
|
||||
sys.exit(f"code {len(code)} B exceeds overlay budget {OVERLAY_MAX} B")
|
||||
|
||||
crc = zlib.crc32(code) & 0xFFFFFFFF
|
||||
flags = (FLAG_COMMITTED |
|
||||
(FLAG_SCREEN_SAVER if a.screensaver else 0) |
|
||||
(SHORTCUTS[a.shortcut] << FLAG_SHORTCUT_SHIFT))
|
||||
required_caps = 0
|
||||
for capability in a.require:
|
||||
required_caps |= CAPABILITIES[capability]
|
||||
header = struct.pack(
|
||||
"<4sHBBIIHH16s16sII4s",
|
||||
MAGIC, HDR_VERSION, ABI_MAJOR, a.api_min,
|
||||
len(code), crc, a.entry, flags,
|
||||
field(a.name, 16), field(a.ver, 16), a.vma,
|
||||
required_caps, b"\x00" * 4,
|
||||
)
|
||||
assert len(header) == 64, len(header)
|
||||
|
||||
with open(a.outfile, "wb") as f:
|
||||
f.write(header)
|
||||
f.write(code)
|
||||
|
||||
print(f"{a.outfile}: name={a.name!r} ver={a.ver!r} "
|
||||
f"abi={ABI_MAJOR} api>={a.api_min} caps=0x{required_caps:08x} "
|
||||
f"vma=0x{a.vma:08x} "
|
||||
f"code={len(code)} B crc32=0x{crc:08x} -> blob {64 + len(code)} B")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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.*) }
|
||||
}
|
||||
Executable
+41
@@ -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"
|
||||
@@ -0,0 +1,180 @@
|
||||
/* Copyright 2026 Armel F4HWN
|
||||
* https://github.com/armel
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Plasma — overlay app. A full-screen demoscene plasma on the 1-bit 128x64 LCD.
|
||||
* The scalar field sums four scrolled sines: horizontal (x), vertical (y),
|
||||
* diagonal (x+y) and a radial ripple around a slowly drifting centre. Because
|
||||
* (x-cx)^2 + (y-cy)^2 is separable, the whole field is still built from per-column
|
||||
* and per-row tables, so each of the 8192 pixels costs only a handful of adds.
|
||||
* Rendered either with an ordered 4x4 Bayer dither (stipple) or as sweeping bands.
|
||||
* Pure compute + framebuffer, no radio — it just works.
|
||||
*
|
||||
* Keys: UP/DOWN speed · 1-5 pattern · STAR stipple/bands · F auto-cycle ·
|
||||
* MENU pause · EXIT quit.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include "../app_api.h"
|
||||
|
||||
#define W 128
|
||||
#define H 64
|
||||
|
||||
static const app_api_t *A;
|
||||
|
||||
/* 32 * sin(2*pi*i/256): four of these sum to -128..128. */
|
||||
static const int8_t SIN[256] = {
|
||||
0, 1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 9, 9, 10, 11, 12,
|
||||
12, 13, 14, 14, 15, 16, 16, 17, 18, 18, 19, 20, 20, 21, 21, 22,
|
||||
23, 23, 24, 24, 25, 25, 26, 26, 27, 27, 27, 28, 28, 29, 29, 29,
|
||||
30, 30, 30, 30, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32,
|
||||
32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 30, 30, 30,
|
||||
30, 29, 29, 29, 28, 28, 27, 27, 27, 26, 26, 25, 25, 24, 24, 23,
|
||||
23, 22, 21, 21, 20, 20, 19, 18, 18, 17, 16, 16, 15, 14, 14, 13,
|
||||
12, 12, 11, 10, 9, 9, 8, 7, 6, 5, 5, 4, 3, 2, 2, 1,
|
||||
0, -1, -2, -2, -3, -4, -5, -5, -6, -7, -8, -9, -9,-10,-11,-12,
|
||||
-12,-13,-14,-14,-15,-16,-16,-17,-18,-18,-19,-20,-20,-21,-21,-22,
|
||||
-23,-23,-24,-24,-25,-25,-26,-26,-27,-27,-27,-28,-28,-29,-29,-29,
|
||||
-30,-30,-30,-30,-31,-31,-31,-31,-31,-32,-32,-32,-32,-32,-32,-32,
|
||||
-32,-32,-32,-32,-32,-32,-32,-32,-31,-31,-31,-31,-31,-30,-30,-30,
|
||||
-30,-29,-29,-29,-28,-28,-27,-27,-27,-26,-26,-25,-25,-24,-24,-23,
|
||||
-23,-22,-21,-21,-20,-20,-19,-18,-18,-17,-16,-16,-15,-14,-14,-13,
|
||||
-12,-12,-11,-10, -9, -9, -8, -7, -6, -5, -5, -4, -3, -2, -2, -1,
|
||||
};
|
||||
|
||||
/* 4x4 ordered-dither matrix, flattened: idx = (y&3)*4 + (x&3), values 0..15. */
|
||||
static const uint8_t BAYER[16] = { 0,8,2,10, 12,4,14,6, 3,11,1,9, 15,7,13,5 };
|
||||
|
||||
/* Pattern presets: {x scale, y scale, diagonal scale, radial ring shift}. */
|
||||
#define NVAR 5
|
||||
static const uint8_t VAR[NVAR][4] = {
|
||||
{4,4,3,5}, {6,3,5,4}, {3,7,2,6}, {5,5,4,5}, {2,8,6,4},
|
||||
};
|
||||
|
||||
/* Per-frame separable tables. */
|
||||
static int8_t colA[W]; /* horizontal sine, by column x */
|
||||
static int8_t rowA[H]; /* vertical sine, by row y */
|
||||
static int8_t diagA[W + H]; /* diagonal sine, by (x + y) */
|
||||
static uint16_t sqx[W]; /* (x - cx)^2 for the radial term */
|
||||
static uint16_t sqy[H]; /* (y - cy)^2 */
|
||||
|
||||
static uint8_t prevKey;
|
||||
|
||||
__attribute__((section(".text.entry"), used))
|
||||
void app_main(const app_api_t *api)
|
||||
{
|
||||
A = api;
|
||||
A->backlight_on();
|
||||
A->status_clear();
|
||||
|
||||
uint16_t t1 = 0, t2 = 0, t3 = 0, tr = 0, tc = 0, tc2 = 0;
|
||||
uint16_t autoCtr = 0;
|
||||
uint8_t speed = 3;
|
||||
uint8_t var = 0;
|
||||
bool bands = true;
|
||||
bool autoc = false;
|
||||
bool paused = false;
|
||||
bool running = true;
|
||||
prevKey = APP_KEY_INVALID;
|
||||
|
||||
while (running) {
|
||||
uint8_t key = A->get_key();
|
||||
if (key == APP_KEY_SAVER) {
|
||||
prevKey = APP_KEY_INVALID;
|
||||
A->delay_ms(10);
|
||||
A->backlight_update();
|
||||
continue;
|
||||
}
|
||||
if (key == APP_KEY_WAKE)
|
||||
key = APP_KEY_INVALID;
|
||||
if (key != prevKey && key != APP_KEY_INVALID) {
|
||||
A->backlight_on();
|
||||
switch (key) {
|
||||
case APP_KEY_EXIT: running = false; break;
|
||||
case APP_KEY_UP:
|
||||
case APP_KEY_DOWN: {
|
||||
const int8_t direction = A->nav_dir(key);
|
||||
if (direction > 0 && speed < 8u) speed++;
|
||||
if (direction < 0 && speed > 1u) speed--;
|
||||
break;
|
||||
}
|
||||
case APP_KEY_MENU: paused = !paused; break;
|
||||
case APP_KEY_STAR: bands = !bands; break;
|
||||
case APP_KEY_F: autoc = !autoc; break;
|
||||
case APP_KEY_1: case APP_KEY_2: case APP_KEY_3:
|
||||
case APP_KEY_4: case APP_KEY_5:
|
||||
var = (uint8_t)(key - APP_KEY_1); autoc = false; break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
prevKey = key;
|
||||
if (!running)
|
||||
break;
|
||||
|
||||
const uint8_t sx = VAR[var][0], sy = VAR[var][1];
|
||||
const uint8_t sd = VAR[var][2], rsh = VAR[var][3];
|
||||
|
||||
/* drifting radial centre (gentle Lissajous), then the separable squares */
|
||||
const int cx = 64 + SIN[(uint8_t)tc]; /* 32..96 */
|
||||
const int cy = 32 + (SIN[(uint8_t)tc2] >> 1);/* 16..48 */
|
||||
|
||||
for (uint8_t x = 0; x < W; x++) {
|
||||
colA[x] = SIN[(uint8_t)(x * sx + t1)];
|
||||
int dx = (int)x - cx; sqx[x] = (uint16_t)(dx * dx);
|
||||
}
|
||||
for (uint8_t y = 0; y < H; y++) {
|
||||
rowA[y] = SIN[(uint8_t)(y * sy + t2)];
|
||||
int dy = (int)y - cy; sqy[y] = (uint16_t)(dy * dy);
|
||||
}
|
||||
for (uint16_t s = 0; s < W + H; s++) diagA[s] = SIN[(uint8_t)(s * sd + t3)];
|
||||
|
||||
/* render all 8 pages: page 0 -> status line (top), pages 1..7 -> fb[0..6] */
|
||||
for (uint8_t x = 0; x < W; x++) {
|
||||
const int8_t cxv = colA[x];
|
||||
const uint16_t sxv = sqx[x];
|
||||
const uint8_t bx = x & 3u;
|
||||
for (uint8_t p = 0; p < 8u; p++) {
|
||||
uint8_t byte = 0;
|
||||
for (uint8_t b = 0; b < 8u; b++) {
|
||||
const uint8_t y = (uint8_t)(p * 8u + b);
|
||||
int v = cxv + rowA[y] + diagA[x + y]
|
||||
+ SIN[(uint8_t)(((sxv + sqy[y]) >> rsh) + tr)];
|
||||
v += 128; /* 0..256 */
|
||||
bool on;
|
||||
if (bands) on = ((v >> 4) & 1u) != 0u; /* sweeping stripes */
|
||||
else {
|
||||
int lvl = v >> 4; if (lvl > 15) lvl = 15; /* 0..15 stipple */
|
||||
on = lvl > (int)BAYER[((y & 3u) << 2) | bx];
|
||||
}
|
||||
if (on) byte = (uint8_t)(byte | (1u << b));
|
||||
}
|
||||
if (p == 0) A->status_line[x] = byte;
|
||||
else A->fb[p - 1][x] = byte;
|
||||
}
|
||||
}
|
||||
A->blit_status();
|
||||
A->blit_full();
|
||||
|
||||
if (!paused) {
|
||||
t1 += speed; t2 += (uint16_t)(speed + 1u); t3 += 1u;
|
||||
tr += speed; tc += 1u; tc2 += 2u;
|
||||
if (autoc && ++autoCtr >= 400u) { autoCtr = 0; var = (uint8_t)((var + 1u) % NVAR); }
|
||||
}
|
||||
|
||||
A->backlight_update();
|
||||
}
|
||||
}
|
||||
@@ -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.*) }
|
||||
}
|
||||
Executable
+40
@@ -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"
|
||||
@@ -0,0 +1,555 @@
|
||||
/* Copyright 2026 Armel F4HWN
|
||||
* https://github.com/armel
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Tetris — compact overlay game for the 128x64 monochrome LCD.
|
||||
*
|
||||
* The 16x16 visible well fills the screen height down the left side; two hidden
|
||||
* spawn rows sit just above it. Rather than solid walls, the well is framed by
|
||||
* short corner brackets and a floor line. The narrow right-hand panel shows the
|
||||
* next piece, score, cleared lines, level and persistent best score. Pieces
|
||||
* come from a shuffled seven-piece bag; a dotted ghost previews the landing.
|
||||
*
|
||||
* Keys: LEFT/RIGHT or 4/6 move, MENU or 2 rotates, 8 soft-drops,
|
||||
* STAR or 0 hard-drops, F pauses and EXIT quits.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include "../app_api.h"
|
||||
|
||||
#define W 128u
|
||||
#define H 64u
|
||||
#define COLS 16
|
||||
#define VISIBLE_ROWS 16
|
||||
#define HIDDEN_ROWS 2
|
||||
#define ROWS (VISIBLE_ROWS + HIDDEN_ROWS)
|
||||
#define CELL 4
|
||||
#define WELL_X 3 /* physical x of board column 0 */
|
||||
#define WELL_Y 0 /* physical y of visible row 0 (full height) */
|
||||
#define WELL_L 1 /* container left rail (corner brackets) */
|
||||
#define WELL_R 67 /* container right rail (corner brackets) */
|
||||
#define WELL_B 63 /* solid floor line, physical y */
|
||||
#define LABEL_X 72 /* left column shared by every panel label */
|
||||
#define PREVIEW_CX 108 /* centre of the NEXT-piece preview area */
|
||||
#define PREVIEW_CY 15
|
||||
#define TICK_MS 20u
|
||||
#define CFG_MAGIC 0x5445u
|
||||
|
||||
typedef struct {
|
||||
uint16_t magic;
|
||||
uint16_t version;
|
||||
uint32_t best;
|
||||
} config_t;
|
||||
|
||||
static const app_api_t *A;
|
||||
|
||||
/* GCC may lower aggregate clears to memset even for this freestanding blob. */
|
||||
void *memset(void *dst, int value, size_t size)
|
||||
{
|
||||
uint8_t *p = dst;
|
||||
while (size--)
|
||||
*p++ = (uint8_t)value;
|
||||
return dst;
|
||||
}
|
||||
|
||||
/* Four 4x4 masks for I, O, T, S, Z, J and L. */
|
||||
static const uint16_t MASK[7][4] = {
|
||||
{0x00F0, 0x4444, 0x0F00, 0x2222},
|
||||
{0x0066, 0x0066, 0x0066, 0x0066},
|
||||
{0x0072, 0x0262, 0x0270, 0x0232},
|
||||
{0x0036, 0x0462, 0x0360, 0x0231},
|
||||
{0x0063, 0x0264, 0x0630, 0x0132},
|
||||
{0x0071, 0x0226, 0x0470, 0x0322},
|
||||
{0x0074, 0x0622, 0x0170, 0x0223},
|
||||
};
|
||||
|
||||
static const uint16_t LINE_POINTS[5] = {0, 100, 300, 500, 800};
|
||||
static const uint32_t DECIMAL_PLACE[6] = {100000u, 10000u, 1000u, 100u, 10u, 1u};
|
||||
|
||||
static uint16_t board[ROWS];
|
||||
static uint8_t bag[7], bagPos;
|
||||
static uint8_t piece, nextPiece, rotation;
|
||||
static int8_t pieceX, pieceY;
|
||||
static uint32_t randomState, score, best;
|
||||
static uint16_t lines, gravityMs;
|
||||
static uint8_t level;
|
||||
static bool paused, gameOver, running, saverPaused, saverActive, bestDirty, newBest;
|
||||
static uint8_t previousKey;
|
||||
static uint16_t repeatMs;
|
||||
static char text[9];
|
||||
|
||||
static void render(bool showPiece, uint32_t clearRows, uint8_t effect);
|
||||
static void new_game(void);
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* RNG and the shuffled 7-piece bag */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
static uint32_t random_next(void)
|
||||
{
|
||||
randomState = randomState * 1664525u + 1013904223u;
|
||||
return randomState;
|
||||
}
|
||||
|
||||
static void refill_bag(void)
|
||||
{
|
||||
for (uint8_t i = 0; i < 7u; i++)
|
||||
bag[i] = i;
|
||||
for (uint8_t i = 6u; i > 0u; i--) {
|
||||
uint8_t j;
|
||||
do {
|
||||
j = (uint8_t)(random_next() >> 29);
|
||||
} while (j > i);
|
||||
const uint8_t t = bag[i]; bag[i] = bag[j]; bag[j] = t;
|
||||
}
|
||||
bagPos = 0;
|
||||
}
|
||||
|
||||
static uint8_t take_piece(void)
|
||||
{
|
||||
if (bagPos >= 7u)
|
||||
refill_bag();
|
||||
return bag[bagPos++];
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Framebuffer drawing: cells, pieces, side panel and dialogs */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
static void set_pixel(int x, int y, bool on)
|
||||
{
|
||||
if ((unsigned)x >= W || (unsigned)y >= H)
|
||||
return;
|
||||
uint8_t *p;
|
||||
if (y < 8)
|
||||
p = &A->status_line[x];
|
||||
else
|
||||
p = &A->fb[(y >> 3) - 1][x];
|
||||
const uint8_t bit = (uint8_t)(1u << (y & 7));
|
||||
if (on) *p |= bit; else *p &= (uint8_t)~bit;
|
||||
}
|
||||
|
||||
/* Short straight runs, in physical coordinates so they may cross the
|
||||
* status-line / framebuffer seam that A->draw_line cannot. */
|
||||
static void hline(int x0, int x1, int y)
|
||||
{
|
||||
for (int x = x0; x <= x1; x++) set_pixel(x, y, true);
|
||||
}
|
||||
|
||||
static void vline(int x, int y0, int y1)
|
||||
{
|
||||
for (int y = y0; y <= y1; y++) set_pixel(x, y, true);
|
||||
}
|
||||
|
||||
/* Corner brackets plus a solid floor — the well's frame, no vertical walls. */
|
||||
static void container(void)
|
||||
{
|
||||
hline(WELL_L, WELL_R, WELL_B);
|
||||
hline(WELL_L, WELL_L + 5, WELL_Y);
|
||||
hline(WELL_R - 5, WELL_R, WELL_Y);
|
||||
vline(WELL_L, WELL_Y, WELL_Y + 5);
|
||||
vline(WELL_R, WELL_Y, WELL_Y + 5);
|
||||
vline(WELL_L, WELL_B - 5, WELL_B);
|
||||
vline(WELL_R, WELL_B - 5, WELL_B);
|
||||
}
|
||||
|
||||
static void cell(int gx, int gy, uint8_t style)
|
||||
{
|
||||
const int visibleY = gy - HIDDEN_ROWS;
|
||||
if ((unsigned)gx >= COLS || (unsigned)visibleY >= VISIBLE_ROWS)
|
||||
return;
|
||||
const int x = WELL_X + gx * CELL;
|
||||
const int y = WELL_Y + visibleY * CELL;
|
||||
if (style == 2u) {
|
||||
set_pixel(x, y + 1, true);
|
||||
set_pixel(x + 1, y, true);
|
||||
set_pixel(x + 2, y + 1, true);
|
||||
set_pixel(x + 1, y + 2, true);
|
||||
} else {
|
||||
const uint8_t size = style == 3u ? CELL : CELL - 1u;
|
||||
for (uint8_t dy = 0; dy < size; dy++)
|
||||
for (uint8_t dx = 0; dx < size; dx++)
|
||||
set_pixel(x + dx, y + dy, true);
|
||||
}
|
||||
}
|
||||
|
||||
static bool collision(uint8_t type, uint8_t rot, int8_t px, int8_t py)
|
||||
{
|
||||
const uint16_t mask = MASK[type][rot & 3u];
|
||||
for (uint8_t i = 0; i < 16u; i++) {
|
||||
if (!(mask & (uint16_t)(1u << i)))
|
||||
continue;
|
||||
const int x = px + (i & 3u);
|
||||
const int y = py + (i >> 2);
|
||||
if (x < 0 || x >= COLS || y >= ROWS)
|
||||
return true;
|
||||
if (y >= 0 && (board[y] & (uint16_t)(1u << x)))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void draw_piece(uint8_t type, uint8_t rot, int8_t px, int8_t py, uint8_t style)
|
||||
{
|
||||
const uint16_t mask = MASK[type][rot & 3u];
|
||||
for (uint8_t i = 0; i < 16u; i++)
|
||||
if (mask & (uint16_t)(1u << i))
|
||||
cell(px + (i & 3u), py + (i >> 2), style);
|
||||
}
|
||||
|
||||
static void number(uint32_t value, uint8_t width)
|
||||
{
|
||||
const uint8_t first = (uint8_t)(6u - width);
|
||||
for (uint8_t i = 0; i < width; i++) {
|
||||
const uint32_t place = DECIMAL_PLACE[first + i];
|
||||
uint8_t digit = 0;
|
||||
while (value >= place) {
|
||||
value -= place;
|
||||
digit++;
|
||||
}
|
||||
text[i] = (char)('0' + digit);
|
||||
}
|
||||
text[width] = 0;
|
||||
}
|
||||
|
||||
static void preview(void)
|
||||
{
|
||||
const uint16_t mask = MASK[nextPiece][0];
|
||||
const int ox = nextPiece < 2u ? PREVIEW_CX - 7 : PREVIEW_CX - 5;
|
||||
const int oy = nextPiece == 0u ? PREVIEW_CY - 5 : PREVIEW_CY - 3;
|
||||
for (uint8_t i = 0; i < 16u; i++) {
|
||||
if (!(mask & (uint16_t)(1u << i)))
|
||||
continue;
|
||||
const int x = ox + (i & 3u) * CELL;
|
||||
const int y = oy + (i >> 2) * CELL;
|
||||
for (uint8_t dy = 0; dy < CELL - 1u; dy++)
|
||||
for (uint8_t dx = 0; dx < CELL - 1u; dx++)
|
||||
set_pixel(x + dx, y + dy, true);
|
||||
}
|
||||
}
|
||||
|
||||
/* One "LABEL value" row of the side panel; y is framebuffer-relative
|
||||
* (physical y - 8), value right-aligned near the screen edge. */
|
||||
static void stat(const char *label, uint8_t y, uint32_t value,
|
||||
uint8_t valueX, uint8_t width)
|
||||
{
|
||||
A->print_tiny(label, LABEL_X, y, false, true);
|
||||
number(value, width);
|
||||
A->print_tiny(text, valueX, y, false, true);
|
||||
}
|
||||
|
||||
static void panel(void)
|
||||
{
|
||||
A->print_tiny("NEXT", LABEL_X, 2, false, true);
|
||||
preview();
|
||||
if (gameOver)
|
||||
A->print_inverse(newBest ? "NEW BEST!" : "GAME OVER", 82, 2, false, true, 118);
|
||||
else if (paused)
|
||||
A->print_inverse("PAUSE", 90, 2, false, true, 110);
|
||||
stat("SCORE", 24, score > 999999u ? 999999u : score, 101, 6);
|
||||
stat("LINES", 32, lines > 999u ? 999u : lines, 113, 3);
|
||||
stat("LEVEL", 40, level, 117, 2);
|
||||
stat("BEST", 48, best > 999999u ? 999999u : best, 101, 6);
|
||||
}
|
||||
|
||||
/* effect 0 is normal, 1..8 is the line-clear sweep, 0xFF flashes a lock. */
|
||||
static void render(bool showPiece, uint32_t clearRows, uint8_t effect)
|
||||
{
|
||||
A->display_clear();
|
||||
A->status_clear();
|
||||
A->print_inverse("TETRIS", 74, 0, true, true, 101);
|
||||
|
||||
container();
|
||||
|
||||
for (uint8_t y = 0; y < ROWS; y++)
|
||||
for (uint8_t x = 0; x < COLS; x++)
|
||||
if ((board[y] & (uint16_t)(1u << x)) && (!(clearRows & (1u << y)) ||
|
||||
(x >= effect && x < COLS - effect)))
|
||||
cell(x, y, 1u);
|
||||
|
||||
if (showPiece && !gameOver) {
|
||||
int8_t ghostY = pieceY;
|
||||
while (!collision(piece, rotation, pieceX, ghostY + 1))
|
||||
ghostY++;
|
||||
if (ghostY != pieceY)
|
||||
draw_piece(piece, rotation, pieceX, ghostY, 2u);
|
||||
draw_piece(piece, rotation, pieceX, pieceY, effect == 0xFFu ? 3u : 1u);
|
||||
}
|
||||
|
||||
panel();
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Board mechanics: scoring, line clears, spawn, lock and moves */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
static void update_best(void)
|
||||
{
|
||||
if (score > best) {
|
||||
best = score;
|
||||
bestDirty = true;
|
||||
newBest = true;
|
||||
}
|
||||
}
|
||||
|
||||
static uint8_t clear_lines(void)
|
||||
{
|
||||
uint32_t full = 0;
|
||||
uint8_t count = 0;
|
||||
for (uint8_t y = 0; y < ROWS; y++) {
|
||||
if (board[y] == 0xFFFFu) { full |= 1u << y; count++; }
|
||||
}
|
||||
if (!count)
|
||||
return 0;
|
||||
|
||||
A->led(true);
|
||||
for (uint8_t step = 1u; step <= COLS / 2u; step++) {
|
||||
render(false, full, step);
|
||||
A->blit_status(); A->blit_full(); A->delay_ms(28);
|
||||
}
|
||||
A->led(false);
|
||||
|
||||
int8_t dst = ROWS - 1;
|
||||
for (int8_t src = ROWS - 1; src >= 0; src--) {
|
||||
if (full & (1u << src))
|
||||
continue;
|
||||
if (dst != src)
|
||||
board[dst] = board[src];
|
||||
dst--;
|
||||
}
|
||||
while (dst >= 0) {
|
||||
board[dst] = 0;
|
||||
dst--;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static void spawn_piece(void)
|
||||
{
|
||||
piece = nextPiece;
|
||||
nextPiece = take_piece();
|
||||
rotation = 0;
|
||||
pieceX = COLS / 2 - 2;
|
||||
pieceY = HIDDEN_ROWS - 1;
|
||||
if (collision(piece, rotation, pieceX, pieceY)) {
|
||||
gameOver = true;
|
||||
update_best();
|
||||
}
|
||||
}
|
||||
|
||||
static void lock_piece(void)
|
||||
{
|
||||
render(true, 0, 0xFFu);
|
||||
A->blit_status(); A->blit_full(); A->delay_ms(45);
|
||||
|
||||
const uint16_t mask = MASK[piece][rotation];
|
||||
bool above = false;
|
||||
for (uint8_t i = 0; i < 16u; i++) {
|
||||
if (!(mask & (uint16_t)(1u << i)))
|
||||
continue;
|
||||
const int x = pieceX + (i & 3u);
|
||||
const int y = pieceY + (i >> 2);
|
||||
if (y < HIDDEN_ROWS) above = true;
|
||||
else board[y] |= (uint16_t)(1u << x);
|
||||
}
|
||||
if (above) {
|
||||
gameOver = true;
|
||||
update_best();
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t cleared = clear_lines();
|
||||
lines = (uint16_t)(lines + cleared);
|
||||
while (level < 15u && lines >= (uint16_t)level * 10u)
|
||||
level++;
|
||||
score += (uint32_t)LINE_POINTS[cleared] * level;
|
||||
update_best();
|
||||
spawn_piece();
|
||||
}
|
||||
|
||||
static bool move_down(bool manual)
|
||||
{
|
||||
if (!collision(piece, rotation, pieceX, pieceY + 1)) {
|
||||
pieceY++;
|
||||
if (manual) { score++; update_best(); }
|
||||
return true;
|
||||
}
|
||||
lock_piece();
|
||||
return false;
|
||||
}
|
||||
|
||||
static void rotate_piece(void)
|
||||
{
|
||||
const uint8_t next = (uint8_t)((rotation + 1u) & 3u);
|
||||
static const int8_t kick[5] = {0, -1, 1, -2, 2};
|
||||
for (uint8_t i = 0; i < 5u; i++)
|
||||
if (!collision(piece, next, pieceX + kick[i], pieceY)) {
|
||||
pieceX += kick[i]; rotation = next; return;
|
||||
}
|
||||
if (!collision(piece, next, pieceX, pieceY - 1)) {
|
||||
pieceY--; rotation = next;
|
||||
}
|
||||
}
|
||||
|
||||
static void hard_drop(void)
|
||||
{
|
||||
uint8_t distance = 0;
|
||||
while (!collision(piece, rotation, pieceX, pieceY + 1)) {
|
||||
pieceY++; distance++;
|
||||
}
|
||||
score += (uint32_t)distance * 2u;
|
||||
update_best();
|
||||
lock_piece();
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Input, screensaver handling and the main loop */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
static void key_action(uint8_t key, bool repeat)
|
||||
{
|
||||
if (key == APP_KEY_EXIT) { running = false; return; }
|
||||
if (gameOver) {
|
||||
if (!repeat && (key == APP_KEY_MENU || key == APP_KEY_STAR || key == APP_KEY_0))
|
||||
new_game();
|
||||
return;
|
||||
}
|
||||
if (!repeat && key == APP_KEY_F) { paused = !paused; return; }
|
||||
if (paused)
|
||||
return;
|
||||
|
||||
int8_t direction = 0;
|
||||
if (key == APP_KEY_4) direction = -1;
|
||||
else if (key == APP_KEY_6) direction = 1;
|
||||
else if (key == APP_KEY_UP || key == APP_KEY_DOWN) direction = A->nav_dir(key);
|
||||
if (direction) {
|
||||
if (!collision(piece, rotation, pieceX + direction, pieceY))
|
||||
pieceX += direction;
|
||||
return;
|
||||
}
|
||||
if (key == APP_KEY_8) { move_down(true); return; }
|
||||
if (repeat)
|
||||
return;
|
||||
if (key == APP_KEY_MENU || key == APP_KEY_2) rotate_piece();
|
||||
else if (key == APP_KEY_STAR || key == APP_KEY_0) hard_drop();
|
||||
}
|
||||
|
||||
static void poll_key(void)
|
||||
{
|
||||
uint8_t key = A->get_key();
|
||||
if (key == APP_KEY_SAVER) {
|
||||
if (!paused && !gameOver) { paused = true; saverPaused = true; }
|
||||
saverActive = true;
|
||||
previousKey = APP_KEY_INVALID;
|
||||
repeatMs = 0;
|
||||
return;
|
||||
}
|
||||
if (key == APP_KEY_WAKE || key == APP_KEY_PTT) {
|
||||
if (saverPaused) paused = false;
|
||||
saverPaused = false;
|
||||
saverActive = false;
|
||||
key = APP_KEY_INVALID;
|
||||
}
|
||||
if (key == APP_KEY_INVALID) {
|
||||
previousKey = key;
|
||||
repeatMs = 0;
|
||||
return;
|
||||
}
|
||||
if (key != previousKey) {
|
||||
key_action(key, false);
|
||||
repeatMs = 260u;
|
||||
} else if (repeatMs > TICK_MS) {
|
||||
repeatMs -= TICK_MS;
|
||||
} else {
|
||||
key_action(key, true);
|
||||
repeatMs = 80u;
|
||||
}
|
||||
previousKey = key;
|
||||
}
|
||||
|
||||
static void new_game(void)
|
||||
{
|
||||
for (uint8_t y = 0; y < ROWS; y++)
|
||||
board[y] = 0;
|
||||
score = 0;
|
||||
lines = 0;
|
||||
level = 1;
|
||||
gravityMs = 0;
|
||||
paused = false;
|
||||
gameOver = false;
|
||||
saverPaused = false;
|
||||
saverActive = false;
|
||||
newBest = false;
|
||||
bagPos = 7;
|
||||
nextPiece = take_piece();
|
||||
spawn_piece();
|
||||
}
|
||||
|
||||
__attribute__((section(".text.entry"), used))
|
||||
void app_main(const app_api_t *api)
|
||||
{
|
||||
A = api;
|
||||
config_t cfg;
|
||||
A->cfg_load((uint8_t *)&cfg, sizeof(cfg));
|
||||
best = cfg.magic == CFG_MAGIC && cfg.version == 1u ? cfg.best : 0u;
|
||||
bestDirty = false;
|
||||
|
||||
randomState = ((uint32_t)A->bk_read(0x67u) << 16) ^ A->rx_freq() ^ best;
|
||||
if (!randomState) randomState = 1;
|
||||
previousKey = APP_KEY_INVALID;
|
||||
repeatMs = 0;
|
||||
running = true;
|
||||
A->led(false);
|
||||
A->backlight_on();
|
||||
new_game();
|
||||
|
||||
while (running) {
|
||||
poll_key();
|
||||
if (!running)
|
||||
break;
|
||||
if (saverActive) {
|
||||
A->backlight_update();
|
||||
A->delay_ms(TICK_MS);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!paused && !gameOver) {
|
||||
const uint16_t interval = level >= 15u ? 100u : (uint16_t)(900u - (level - 1u) * 55u);
|
||||
gravityMs = (uint16_t)(gravityMs + TICK_MS);
|
||||
if (gravityMs >= interval) {
|
||||
gravityMs = 0;
|
||||
move_down(false);
|
||||
}
|
||||
}
|
||||
|
||||
render(true, 0, 0);
|
||||
A->blit_status();
|
||||
A->blit_full();
|
||||
A->backlight_update();
|
||||
A->delay_ms(TICK_MS);
|
||||
}
|
||||
|
||||
update_best();
|
||||
if (bestDirty) {
|
||||
cfg.magic = CFG_MAGIC;
|
||||
cfg.version = 1u;
|
||||
cfg.best = best;
|
||||
A->cfg_save((const uint8_t *)&cfg, sizeof(cfg));
|
||||
}
|
||||
A->led(false);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
APP_VMA = DEFINED(APP_VMA) ? APP_VMA : 0x20000280;
|
||||
APP_LENGTH = 0x1000;
|
||||
ENTRY(app_main)
|
||||
MEMORY { APP (rwx) : ORIGIN = APP_VMA, LENGTH = APP_LENGTH }
|
||||
SECTIONS {
|
||||
.app APP_VMA : {
|
||||
KEEP(*(.text.entry)) *(.text .text.*) *(.rodata .rodata.*)
|
||||
. = ALIGN(4); *(.data .data.*) . = ALIGN(4); *(.bss .bss.* COMMON) . = ALIGN(4);
|
||||
} > APP
|
||||
ASSERT(. <= APP_VMA + APP_LENGTH, "Triple VFO overlay app overflows the 4 KiB overlay")
|
||||
/DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.eh_frame*) *(.comment) *(.note.*) }
|
||||
}
|
||||
Executable
+21
@@ -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"
|
||||
@@ -0,0 +1,266 @@
|
||||
/* Copyright 2026 Armel F4HWN
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Triple VFO — modal overlay receiver/transmitter. A and B are the resident
|
||||
* Main Display VFOs; C is an app-persistent memory channel. The resident ABI
|
||||
* performs all RF sequencing while this blob owns the seven-line Tiny UI.
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include "../app_api.h"
|
||||
|
||||
#define TICK_MS 20u
|
||||
#define LONG_MS 400u
|
||||
#define CFG_MAGIC 0xC3u
|
||||
#define VFO_COUNT 3u
|
||||
#define MR_MAX 1024u
|
||||
#define STATUS_PTT_X 54u
|
||||
|
||||
static const app_api_t *A;
|
||||
static app_trivfo_info_t vi[VFO_COUNT];
|
||||
static uint8_t selected, state;
|
||||
static uint16_t cChannel;
|
||||
static bool running, fArm, txDenied, channelLabelOn, showFrequency;
|
||||
static uint8_t batteryTicks;
|
||||
static char text[16];
|
||||
|
||||
/* Resident status.c uses this exact 8-column inverted glyph at x=69. Keep
|
||||
* the overlay copy byte-for-byte identical so F has the same position and
|
||||
* polarity as every other firmware screen. */
|
||||
static const uint8_t fontF[8] = {
|
||||
0x7f, 0x00, 0x76, 0x76, 0x76, 0x76, 0x7e, 0x7f
|
||||
};
|
||||
|
||||
/* Same two 2x6 glyphs and same x=54 slot as UI_DisplayStatus(). */
|
||||
static const uint8_t fontPttOnePush[12] = {
|
||||
0x00, 0x3e, 0x41, 0x41, 0x41, 0x3e,
|
||||
0x00, 0x7f, 0x09, 0x09, 0x09, 0x06
|
||||
};
|
||||
static const uint8_t fontPttClassic[12] = {
|
||||
0x00, 0x3e, 0x41, 0x41, 0x41, 0x22,
|
||||
0x00, 0x7f, 0x40, 0x40, 0x40, 0x40
|
||||
};
|
||||
|
||||
static uint8_t slen(const char *s){ uint8_t n=0; while(s[n])n++; return n; }
|
||||
static bool copyName(char *d,const char *s){
|
||||
uint8_t n=0;
|
||||
while(n<10u){
|
||||
uint8_t c=(uint8_t)s[n];
|
||||
if(c==0u||c==0xffu) break;
|
||||
d[n++]=(char)c;
|
||||
}
|
||||
while(n>0u&&d[n-1u]==' ') n--;
|
||||
d[n]='\0';
|
||||
return n>0u;
|
||||
}
|
||||
static char *put(char *o,const char *s){ while(*s)*o++=*s++; return o; }
|
||||
static char *putu(char *o,uint32_t v){ char t[10]; int8_t n=0; do{t[n++]=(char)('0'+v%10u);v/=10u;}while(v); while(n--)*o++=t[n]; return o; }
|
||||
static char *puti(char *o,int16_t v){ if(v<0){*o++='-';v=(int16_t)-v;} return putu(o,(uint16_t)v); }
|
||||
static char *put3(char *o,uint16_t v){ *o++=(char)('0'+(v/100u)%10u); *o++=(char)('0'+(v/10u)%10u); *o++=(char)('0'+v%10u); return o; }
|
||||
static char *put4(char *o,uint16_t v){ *o++=(char)('0'+(v/1000u)%10u); return put3(o,(uint16_t)(v%1000u)); }
|
||||
|
||||
static void formatFreq(char *s,uint32_t f){
|
||||
char *o=putu(s,f/100000u); *o++='.'; f%=100000u;
|
||||
uint32_t p=10000u; while(p){ *o++=(char)('0'+(f/p)%10u); p/=10u; } *o='\0';
|
||||
}
|
||||
static void formatChannel(char *s,const app_trivfo_info_t *v){
|
||||
char *o=s;
|
||||
if(v->channel<MR_MAX) o=put4(o,(uint16_t)(v->channel+1u));
|
||||
else { *o++='F'; o=putu(o,(uint16_t)(v->channel-MR_MAX+1u)); }
|
||||
*o='\0';
|
||||
}
|
||||
static void formatStep(char *s,uint16_t step){
|
||||
char *o=putu(s,step/100u);
|
||||
if(step%100u){ *o++='.'; *o++=(char)('0'+(step/10u)%10u); *o++=(char)('0'+step%10u); }
|
||||
*o++='K'; *o='\0';
|
||||
}
|
||||
static void formatCode(char *s,const app_trivfo_info_t *v){
|
||||
char *o=s;
|
||||
if(v->code_type==1u){ o=putu(o,v->code_value/10u); *o++='.'; *o++=(char)('0'+v->code_value%10u); }
|
||||
else if(v->code_type==2u||v->code_type==3u){
|
||||
uint16_t n=v->code_value; *o++=(char)('0'+((n>>6)&7u)); *o++=(char)('0'+((n>>3)&7u)); *o++=(char)('0'+(n&7u)); *o++=(v->code_type==2u?'N':'I');
|
||||
} else return formatStep(s,v->step);
|
||||
*o='\0';
|
||||
}
|
||||
static void drawMeter(const app_trivfo_info_t *v){
|
||||
int16_t dbm=v->rssi_dbm; if(dbm>-53)dbm=-53;
|
||||
uint8_t s=0,over=0;
|
||||
if(dbm>=-93){ s=9; over=(uint8_t)(dbm+93); if(over>40)over=40; }
|
||||
/* dbm + 147 is non-negative in this branch. Keep the division unsigned:
|
||||
* otherwise GCC pulls the ~460-byte signed division helper into the 4 KiB
|
||||
* overlay even though no signed quotient is required. */
|
||||
else if(dbm>=-141) s=(uint8_t)((uint16_t)(dbm+147)/6u);
|
||||
|
||||
char *o=text; if(dbm>-100)*o++=' '; o=puti(o,dbm); o=put(o," dBm"); *o='\0';
|
||||
A->print_tiny(text,2,1,false,true);
|
||||
if(over){ o=text; *o++='+'; if(over<10)*o++='0'; o=putu(o,over); }
|
||||
else { o=text; *o++='S'; o=putu(o,s); }
|
||||
*o='\0'; A->print_normal(text,38,0,0);
|
||||
|
||||
uint8_t level=(uint8_t)(s+over/10u); if(level>13u)level=13u;
|
||||
for(uint8_t i=0;i<level;i++){
|
||||
uint8_t *p=&A->fb[0][62u+i*5u];
|
||||
p[0]=0x3e; p[1]=(i<9u)?0x3e:0x22; p[2]=(i<9u)?0x3e:0x22; p[3]=0x3e;
|
||||
}
|
||||
}
|
||||
|
||||
static void drawPttStatus(void){
|
||||
const uint8_t *glyph=(vi[0].flags&APP_TRIVFO_PTT_ONEPUSH)?fontPttOnePush:fontPttClassic;
|
||||
for(uint8_t i=0;i<12u;i++) A->status_line[STATUS_PTT_X+i]=glyph[i];
|
||||
}
|
||||
|
||||
static void drawVfo(uint8_t n){
|
||||
const app_trivfo_info_t *v=&vi[n];
|
||||
uint8_t mainLine=(uint8_t)(n*2u+1u), techLine=(uint8_t)(mainLine+1u);
|
||||
uint8_t techY=(uint8_t)(techLine*8u+1u);
|
||||
|
||||
formatChannel(text,v);
|
||||
if(!(v->flags&APP_TRIVFO_RECEIVING)||channelLabelOn)
|
||||
A->print_inverse(text,2,mainLine,false,true,(uint8_t)(slen(text)*4u+3u));
|
||||
if(n==selected && txDenied)
|
||||
A->print_tiny("TX DISABLE",22,(uint8_t)(mainLine*8u+1u),false,true);
|
||||
else {
|
||||
bool useName=false;
|
||||
if(v->channel<MR_MAX&&!showFrequency) useName=copyName(text,v->name);
|
||||
if(!useName) formatFreq(text,v->frequency);
|
||||
if(n==selected)
|
||||
A->print_bold(text,22,0,mainLine);
|
||||
else
|
||||
A->print_normal(text,22,0,mainLine);
|
||||
}
|
||||
|
||||
const char *mod=v->modulation==0u?"FM":v->modulation==1u?"AM":v->modulation==2u?"USB":v->modulation==3u?"BYP":v->modulation==4u?"RAW":"?";
|
||||
static const char *const power[7]={"LOW1","LOW2","LOW3","LOW4","LOW5","MID","HIGH"};
|
||||
uint8_t p=(v->power>=1u&&v->power<=7u)?(uint8_t)(v->power-1u):0u;
|
||||
if(v->flags&APP_TRIVFO_GUI_CLASSIC){
|
||||
A->print_normal(mod,2,0,techLine);
|
||||
if(v->modulation==0u&&v->code_type==1u) A->print_normal("CT",22,0,techLine);
|
||||
else if(v->modulation==0u&&(v->code_type==2u||v->code_type==3u)) A->print_normal("DC",22,0,techLine);
|
||||
static const char *const powerShort[7]={"L1","L2","L3","L4","L5","M","H"};
|
||||
A->print_normal(powerShort[p],42,0,techLine);
|
||||
if(v->flags&APP_TRIVFO_USER_POWER){
|
||||
A->fb[techLine][38]=0x3e; A->fb[techLine][39]=0x1c; A->fb[techLine][40]=0x08;
|
||||
}
|
||||
if(v->offset_direction==1u) A->print_normal("+",60,0,techLine);
|
||||
else if(v->offset_direction==2u) A->print_normal("-",60,0,techLine);
|
||||
if(v->reverse) A->print_normal("R",68,0,techLine);
|
||||
A->print_normal(v->bandwidth==0u?"W":v->bandwidth==1u?"N":"N+",80,0,techLine);
|
||||
text[0]='S'; text[1]='Q'; text[2]='L'; text[3]=(char)('0'+(v->squelch%10u)); text[4]='\0';
|
||||
A->print_normal(text,98,0,techLine);
|
||||
} else {
|
||||
A->print_tiny(mod,3,techY,false,true);
|
||||
A->print_tiny(power[p],24,techY,false,true);
|
||||
if(v->flags&APP_TRIVFO_USER_POWER){
|
||||
A->fb[techLine][19]=0x3e; A->fb[techLine][20]=0x1c; A->fb[techLine][21]=0x08;
|
||||
}
|
||||
if(v->offset_direction==1u) A->print_normal("+",41,0,techLine);
|
||||
else if(v->offset_direction==2u) A->print_normal("-",41,0,techLine);
|
||||
if(v->reverse) A->print_tiny("R",51,techY,false,true);
|
||||
if(v->code_type==1u){ A->print_tiny("CT",58,techY,false,true); formatCode(text,v); A->print_tiny(text,68,techY,false,true); }
|
||||
else if(v->code_type==2u||v->code_type==3u){ A->print_tiny("DC",58,techY,false,true); formatCode(text,v); A->print_tiny(text,68,techY,false,true); }
|
||||
else { formatStep(text,v->step); A->print_tiny(text,58,techY,false,true); }
|
||||
A->print_tiny(v->bandwidth==0u?"WIDE":v->bandwidth==1u?"NAR":"NAR+",91,techY,false,true);
|
||||
text[0]='S'; text[1]='Q'; text[2]='L'; text[3]=(char)('0'+(v->squelch%10u)); text[4]='\0';
|
||||
A->print_tiny(text,110,techY,false,true);
|
||||
}
|
||||
}
|
||||
|
||||
static void draw(void){
|
||||
for(uint8_t i=0;i<VFO_COUNT;i++) {
|
||||
A->trivfo_get(i,&vi[i]);
|
||||
/* The resident scanner can temporarily select the VFO carrying the
|
||||
* reception. Mirror that selection locally so the bold main field
|
||||
* follows it immediately. */
|
||||
if(vi[i].flags&APP_TRIVFO_SELECTED) selected=i;
|
||||
}
|
||||
A->display_clear(); A->status_clear();
|
||||
/* Keep the application title in the resident upper-left position. */
|
||||
A->draw_battery();
|
||||
A->print_inverse("TRIPLE VFO",2,0,true,true,42);
|
||||
drawPttStatus();
|
||||
if(fArm){ for(uint8_t i=0;i<8u;i++) A->status_line[69u+i]=fontF[i]; }
|
||||
for(uint8_t i=0;i<VFO_COUNT;i++) drawVfo(i);
|
||||
if(state==APP_TRIVFO_TX_STATE&&(vi[selected].flags&APP_TRIVFO_AUDIO_BAR))
|
||||
A->audio_scope(0u,true);
|
||||
else {
|
||||
A->audio_scope(0u,false);
|
||||
for(uint8_t i=0;i<VFO_COUNT;i++)
|
||||
if(vi[i].flags&APP_TRIVFO_RECEIVING){ drawMeter(&vi[i]); break; }
|
||||
}
|
||||
A->blit_status(); A->blit_full();
|
||||
}
|
||||
|
||||
static void selectNext(void){ selected=(uint8_t)((selected+1u)%VFO_COUNT); A->trivfo_select(selected); }
|
||||
static void toggleNameFrequency(void){ showFrequency=!showFrequency; }
|
||||
static void stepSelected(uint8_t key){ int8_t d=A->nav_dir(key); uint16_t ch=A->trivfo_step(selected,d); if(selected==2u&&ch!=0xFFFFu)cChannel=ch; }
|
||||
|
||||
static void loadCfg(void){ uint8_t c[4]; A->cfg_load(c,4); cChannel=(c[0]==CFG_MAGIC)?(uint16_t)(c[1]|((uint16_t)c[2]<<8)):0xFFFFu; }
|
||||
static void saveCfg(void){ uint8_t c[3]={CFG_MAGIC,(uint8_t)cChannel,(uint8_t)(cChannel>>8)}; A->cfg_save(c,3); }
|
||||
|
||||
__attribute__((section(".text.entry"),used))
|
||||
void app_main(const app_api_t *api){
|
||||
A=api; running=true; fArm=false; txDenied=false; channelLabelOn=true; showFrequency=false; selected=batteryTicks=0; loadCfg();
|
||||
cChannel=A->trivfo_enter(cChannel); A->backlight_on(); draw();
|
||||
|
||||
uint8_t held=APP_KEY_INVALID, blinkTicks=0; uint16_t heldMs=0; bool longDone=false, ptt=false;
|
||||
while(running){
|
||||
uint8_t key=A->get_key();
|
||||
if(key==APP_KEY_SAVER){
|
||||
state=A->trivfo_tick();
|
||||
if(state==APP_TRIVFO_RX){
|
||||
A->backlight_on();
|
||||
draw();
|
||||
}
|
||||
for(uint8_t i=0;i<TICK_MS/10u;i++){
|
||||
A->delay_ms(10); A->backlight_update();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if(key!=APP_KEY_INVALID&&key!=APP_KEY_WAKE) A->backlight_on();
|
||||
if(key==APP_KEY_WAKE) key=APP_KEY_INVALID;
|
||||
if(key==APP_KEY_PTT){
|
||||
if(!ptt){ ptt=true; txDenied=A->trivfo_ptt(true)!=0; }
|
||||
} else if(ptt){ ptt=false; A->trivfo_ptt(false); txDenied=false; }
|
||||
|
||||
if(key==APP_KEY_INVALID||key==APP_KEY_PTT){
|
||||
if(held!=APP_KEY_INVALID&&!longDone){
|
||||
if(held==APP_KEY_F) fArm=!fArm;
|
||||
else if(held==APP_KEY_1&&fArm){ fArm=false; toggleNameFrequency(); }
|
||||
else if(held==APP_KEY_2&&fArm){ fArm=false; selectNext(); }
|
||||
else if(held==APP_KEY_EXIT) running=false;
|
||||
}
|
||||
held=APP_KEY_INVALID; heldMs=0; longDone=false;
|
||||
} else if(key!=held){
|
||||
held=key; heldMs=0; longDone=false;
|
||||
if(key==APP_KEY_UP||key==APP_KEY_DOWN){ stepSelected(key); longDone=true; }
|
||||
} else {
|
||||
heldMs=(uint16_t)(heldMs+TICK_MS);
|
||||
if((key==APP_KEY_UP||key==APP_KEY_DOWN)&&heldMs>=300u){ stepSelected(key); }
|
||||
else if(!longDone&&heldMs>=LONG_MS){
|
||||
longDone=true;
|
||||
/* Long-1 toggles name/frequency and Long-2 selects the next
|
||||
* VFO; both are deliberately independent of F. */
|
||||
if(key==APP_KEY_1){ fArm=false; toggleNameFrequency(); }
|
||||
else if(key==APP_KEY_2){ fArm=false; selectNext(); }
|
||||
}
|
||||
}
|
||||
|
||||
state=A->trivfo_tick();
|
||||
if(state==APP_TRIVFO_RX||state==APP_TRIVFO_TX_STATE)
|
||||
A->backlight_on();
|
||||
if(state==APP_TRIVFO_RX){
|
||||
if(++blinkTicks>=25u){ blinkTicks=0; channelLabelOn=!channelLabelOn; }
|
||||
} else { blinkTicks=0; channelLabelOn=true; }
|
||||
draw();
|
||||
/* Match MAIN: no loaded-voltage samples during TX, then allow one
|
||||
* second for the pack to recover before refreshing the four-sample
|
||||
* battery average. */
|
||||
if(state==APP_TRIVFO_TX_STATE) batteryTicks=0;
|
||||
else if(++batteryTicks>=50u){ batteryTicks=0; A->battery_sample(); }
|
||||
A->delay_ms(TICK_MS); A->backlight_update();
|
||||
}
|
||||
if(ptt)
|
||||
A->trivfo_ptt(false);
|
||||
saveCfg();
|
||||
A->trivfo_leave();
|
||||
}
|
||||
+8
-8
@@ -15,11 +15,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
#include "app/fm.h"
|
||||
#endif
|
||||
#include "audio.h"
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
#include "driver/bk1080.h"
|
||||
#endif
|
||||
#include "driver/bk4819.h"
|
||||
@@ -70,7 +70,7 @@ void AUDIO_PlayBeep(BEEP_Type_t Beep)
|
||||
if (Beep >= ARRAY_SIZE(BEEP_Classic_array))
|
||||
return;
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (gFmRadioMode)
|
||||
BK1080_Mute(true);
|
||||
#endif
|
||||
@@ -112,7 +112,7 @@ void AUDIO_PlayBeep(BEEP_Type_t Beep)
|
||||
SYSTEM_DelayMs(5);
|
||||
BK4819_WriteRegister(BK4819_REG_71, ToneConfig);
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
const bool isFmRadio = gFmRadioMode;
|
||||
|
||||
if (isFmRadio)
|
||||
@@ -122,7 +122,7 @@ void AUDIO_PlayBeep(BEEP_Type_t Beep)
|
||||
if (gEnableSpeaker)
|
||||
AUDIO_AudioPathOn();
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (isFmRadio)
|
||||
BK1080_Mute(false);
|
||||
#endif
|
||||
@@ -275,7 +275,7 @@ void AUDIO_PlaySingleVoice(bool bFlag)
|
||||
if (FUNCTION_IsRx()) // 1of11
|
||||
BK4819_SetAF(BK4819_AF_MUTE);
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (gFmRadioMode)
|
||||
BK1080_Mute(true);
|
||||
#endif
|
||||
@@ -299,7 +299,7 @@ void AUDIO_PlaySingleVoice(bool bFlag)
|
||||
if (FUNCTION_IsRx()) // 1of11
|
||||
RADIO_SetModulation(gRxVfo->Modulation);
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (gFmRadioMode)
|
||||
BK1080_Mute(false);
|
||||
#endif
|
||||
@@ -440,7 +440,7 @@ void AUDIO_PlayQueuedVoice(void)
|
||||
RADIO_SetModulation(gRxVfo->Modulation); // 1of11
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (gFmRadioMode)
|
||||
BK1080_Mute(false);
|
||||
#endif
|
||||
|
||||
+10
-9
@@ -22,16 +22,17 @@ const uint8_t gFontPttClassic[2][6] =
|
||||
{0x00, 0x7f, 0x40, 0x40, 0x40, 0x40},
|
||||
};
|
||||
|
||||
const uint8_t gFontF[8] =
|
||||
const uint8_t gFontF[9] =
|
||||
{
|
||||
0b00111110,
|
||||
0b01111111,
|
||||
0b00000000,
|
||||
0b01110110,
|
||||
0b01110110,
|
||||
0b01110110,
|
||||
0b01110110,
|
||||
0b01111110,
|
||||
0b01111111
|
||||
0b01000001,
|
||||
0b01110101,
|
||||
0b01110101,
|
||||
0b01110101,
|
||||
0b01111101,
|
||||
0b01111111,
|
||||
0b00111110
|
||||
};
|
||||
|
||||
const uint8_t gFontS[6] =
|
||||
@@ -301,7 +302,7 @@ const uint8_t BITMAP_NOAA[12] =
|
||||
};
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_FOXHUNT
|
||||
#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON)
|
||||
const uint8_t BITMAP_FoxHuntSignal[10] =
|
||||
{ // point source + the speaker bitmap's two sound waves, oriented horizontally
|
||||
0b00001000,
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@
|
||||
extern const uint8_t gFontPowerSave[2][6];
|
||||
extern const uint8_t gFontPttOnePush[2][6];
|
||||
extern const uint8_t gFontPttClassic[2][6];
|
||||
extern const uint8_t gFontF[8];
|
||||
extern const uint8_t gFontF[9];
|
||||
extern const uint8_t gFontS[6];
|
||||
|
||||
extern const uint8_t gFontKeyLock[9];
|
||||
@@ -45,7 +45,7 @@ extern const uint8_t BITMAP_compand[6];
|
||||
|
||||
extern const uint8_t BITMAP_NOAA[12];
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_FOXHUNT
|
||||
#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON)
|
||||
extern const uint8_t BITMAP_FoxHuntSignal[10];
|
||||
extern const uint8_t BITMAP_FoxHuntSpeaker[10];
|
||||
extern const uint8_t BITMAP_FoxHuntUp[11];
|
||||
|
||||
+13
-4
@@ -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
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include "driver/bk4819.h"
|
||||
#include "string.h"
|
||||
#include "external/printf/printf.h"
|
||||
#include "am_fix.h"
|
||||
|
||||
static inline void LogUart(const char *const str)
|
||||
{
|
||||
|
||||
+12
-17
@@ -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;
|
||||
}
|
||||
|
||||
@@ -31,14 +31,6 @@ extern uint8_t gBacklightBrightness;
|
||||
extern uint16_t gSleepModeCountdown_500ms;
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_BLMIN_TMP_OFF
|
||||
typedef enum {
|
||||
BLMIN_STAT_ON,
|
||||
BLMIN_STAT_OFF,
|
||||
BLMIN_STAT_UNKNOWN
|
||||
} BLMIN_STAT_t;
|
||||
#endif
|
||||
|
||||
void BACKLIGHT_InitHardware();
|
||||
void BACKLIGHT_UpdateTickless(void);
|
||||
void BACKLIGHT_TurnOn();
|
||||
|
||||
+1
-1
@@ -1095,7 +1095,7 @@ void BK4819_TurnsOffTones_TurnsOnRX(void)
|
||||
BK4819_REG_30_ENABLE_RX_DSP);
|
||||
}
|
||||
|
||||
#ifdef ENABLE_AIRCOPY
|
||||
#if defined(ENABLE_AIRCOPY) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
|
||||
void BK4819_SetupAircopy(void)
|
||||
{
|
||||
BK4819_WriteRegister(BK4819_REG_70, 0x00E0); // Enable Tone2, tuning gain 48
|
||||
|
||||
+1
-1
@@ -114,7 +114,7 @@ void BK4819_EnterTxMute(void);
|
||||
void BK4819_ExitTxMute(void);
|
||||
void BK4819_Sleep(void);
|
||||
void BK4819_TurnsOffTones_TurnsOnRX(void);
|
||||
#ifdef ENABLE_AIRCOPY
|
||||
#if defined(ENABLE_AIRCOPY) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
|
||||
void BK4819_SetupAircopy(void);
|
||||
#endif
|
||||
void BK4819_ResetFSK(void);
|
||||
|
||||
+1
-1
@@ -1133,7 +1133,7 @@ void BK4819_TurnsOffTones_TurnsOnRX(void)
|
||||
BK4819_REG_30_ENABLE_RX_DSP);
|
||||
}
|
||||
|
||||
#ifdef ENABLE_AIRCOPY
|
||||
#if defined(ENABLE_AIRCOPY) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
|
||||
void BK4819_SetupAircopy(void)
|
||||
{
|
||||
BK4819_WriteRegister(BK4819_REG_70, 0x00C3); // Enable Tone2, tuning gain 48
|
||||
|
||||
+21
-16
@@ -39,25 +39,30 @@ void EEPROM_ReadBuffer(uint16_t Address, void *pBuffer, uint8_t Size)
|
||||
I2C_Stop();
|
||||
}
|
||||
|
||||
void EEPROM_WriteBuffer(uint16_t Address, const void *pBuffer)
|
||||
void EEPROM_WriteBuffer(uint16_t Address, const void *pBuffer, uint8_t Size)
|
||||
{
|
||||
if (pBuffer == NULL || Address >= 0x2000)
|
||||
if (pBuffer == NULL)
|
||||
return;
|
||||
|
||||
|
||||
uint8_t buffer[8];
|
||||
EEPROM_ReadBuffer(Address, buffer, 8);
|
||||
if (memcmp(pBuffer, buffer, 8) == 0) {
|
||||
return;
|
||||
while (Size >= sizeof(buffer) && Address < 0x2000)
|
||||
{
|
||||
EEPROM_ReadBuffer(Address, buffer, sizeof(buffer));
|
||||
if (memcmp(pBuffer, buffer, sizeof(buffer)) != 0)
|
||||
{
|
||||
I2C_Start();
|
||||
I2C_Write(0xA0);
|
||||
I2C_Write((Address >> 8) & 0xFF);
|
||||
I2C_Write((Address >> 0) & 0xFF);
|
||||
I2C_WriteBuffer(pBuffer, sizeof(buffer));
|
||||
I2C_Stop();
|
||||
|
||||
// give the EEPROM time to burn the data in (apparently takes 5ms)
|
||||
SYSTEM_DelayMs(8);
|
||||
}
|
||||
|
||||
Address += sizeof(buffer);
|
||||
pBuffer += sizeof(buffer);
|
||||
Size -= sizeof(buffer);
|
||||
}
|
||||
|
||||
I2C_Start();
|
||||
I2C_Write(0xA0);
|
||||
I2C_Write((Address >> 8) & 0xFF);
|
||||
I2C_Write((Address >> 0) & 0xFF);
|
||||
I2C_WriteBuffer(pBuffer, 8);
|
||||
I2C_Stop();
|
||||
|
||||
// give the EEPROM time to burn the data in (apparently takes 5ms)
|
||||
SYSTEM_DelayMs(8);
|
||||
}
|
||||
+1
-2
@@ -20,7 +20,6 @@
|
||||
#include <stdint.h>
|
||||
|
||||
void EEPROM_ReadBuffer(uint16_t Address, void *pBuffer, uint8_t Size);
|
||||
void EEPROM_WriteBuffer(uint16_t Address, const void *pBuffer);
|
||||
void EEPROM_WriteBuffer(uint16_t Address, const void *pBuffer, uint8_t Size);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -51,13 +51,12 @@ static const AddrMapping_t ADDR_MAPPINGS[] = {
|
||||
_MK_MAPPING(0x008000, 0x008000, 0x00886E), // 1024 MR + 7 VFO Attributes * 2 Bytes (ex 0x002000) 0x008000 -> 0x00880E
|
||||
// List name * 4 Bytes 0x00880E -> 0x00886E
|
||||
|
||||
_MK_MAPPING(0x009000, 0x009000, 0x0090E7), // 14 VFO * 16 Bytes = 0x9000 -> 0x90E0 (the old
|
||||
_MK_MAPPING(0x009000, 0x009000, 0x0090E8), // 14 VFO * 16 Bytes = 0x9000 -> 0x90E0 (the old
|
||||
// 0x90D6 bound was 10 B short: it truncated the
|
||||
// 470 MHz VFO1 record at 0x90D0 -> 0x90DF)
|
||||
// Fox Hunt settings * 7 Bytes 0x90E0 -> 0x90E7
|
||||
// (ENABLE_FEAT_F4HWN_FOXHUNT, written directly by
|
||||
// app/foxhunt.c; concatenated here so aircopy clones
|
||||
// them together with the VFOs)
|
||||
// Fox Hunt / Beacon settings * 8 Bytes 0x90E0 -> 0x90E8
|
||||
// (written directly by app/foxhunt.c; concatenated
|
||||
// here so aircopy clones them with the VFOs)
|
||||
|
||||
_MK_MAPPING(0x00A000, 0x00A000, 0x00A170), // Settings * 16 Bytes (ex 0x004000) 0x00A000 -> 0x00A010
|
||||
// Settings * 16 Bytes (ex 0x005000) 0x00A010 -> 0x00A020
|
||||
@@ -110,11 +109,8 @@ void EEPROM_ReadBuffer(uint16_t Address, void *pBuffer, uint8_t Size)
|
||||
}
|
||||
}
|
||||
|
||||
void EEPROM_WriteBuffer(uint16_t Address, const void *pBuffer)
|
||||
void EEPROM_WriteBuffer(uint16_t Address, const void *pBuffer, uint8_t Size)
|
||||
{
|
||||
// Write 8 bytes!!
|
||||
|
||||
uint16_t Size = 8;
|
||||
while (Size)
|
||||
{
|
||||
uint32_t PY_Addr;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,252 @@
|
||||
/* Copyright 2026 Armel F4HWN
|
||||
* https://github.com/armel
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Multiboot flash programmer.
|
||||
*
|
||||
* M1 (validated): brick-critical core - reprogram the internal application flash
|
||||
* from an image held in the external SPI flash, running from RAM.
|
||||
*
|
||||
* M2 (this file): slot format + integrity validation. Each slot starts with a
|
||||
* header (magic, size, CRC32, name, version); a restore validates the header and
|
||||
* the image CRC32 *before* erasing anything, so an incompatible or corrupt image
|
||||
* can never brick the radio. There is a single firmware for both the K1 and the
|
||||
* K5v3 (the keypad difference is handled at runtime by the hidden SetNav menu),
|
||||
* so no per-model guard is needed.
|
||||
*/
|
||||
|
||||
#ifndef DRIVER_MB_FLASH_H
|
||||
#define DRIVER_MB_FLASH_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
/* Internal flash application region (see Core/py32f071xb.ld:
|
||||
* FLASH origin 0x08002800, length 118 KiB). 0x08002800 is 256-byte aligned, so
|
||||
* the whole region can be page-erased (256 B granularity) without touching the
|
||||
* factory bootloader that lives just below it. */
|
||||
#define MB_INT_APP_BASE 0x08002800u
|
||||
#define MB_INT_APP_SIZE 0x0001D800u /* 118 KiB */
|
||||
|
||||
/* External SPI flash slot layout.
|
||||
* Each 128 KiB slot = one header sector (4 KiB) followed by the image.
|
||||
* Slot 0 is the firmware-managed BACKUP of the normally-flashed firmware
|
||||
* (written by MB_BackupInternalToSlot0, protected from host writes); slots
|
||||
* 1..4 are the user slots managed from UV Studio. */
|
||||
#define MB_SLOT_STRIDE 0x00020000u /* 128 KiB per slot */
|
||||
#define MB_SLOT_IMG_OFFSET 0x00001000u /* image starts after header sector */
|
||||
#define MB_SLOT0_EXT_BASE 0x00020000u /* slot 0 (backup) header base */
|
||||
#define MB_SLOT_COUNT 5u /* slot 0 backup + slots 1..4 */
|
||||
#define MB_SLOT_BACKUP 0u /* slot 0 = firmware base backup */
|
||||
|
||||
/* Slot header (stored at the slot base, first 4 KiB sector). 64 bytes. */
|
||||
#define MB_SLOT_MAGIC 0x31424D46u /* "FMB1" */
|
||||
#define MB_HDR_VERSION 1u
|
||||
#define MB_FLAG_COMMITTED (1u << 0) /* image written and verified */
|
||||
#define MB_NAME_LEN 16
|
||||
#define MB_VERSION_LEN 16
|
||||
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint32_t magic; /* MB_SLOT_MAGIC */
|
||||
uint16_t hdr_version; /* MB_HDR_VERSION */
|
||||
uint16_t flags; /* MB_FLAG_COMMITTED, ... */
|
||||
uint32_t image_size; /* bytes, <= MB_INT_APP_SIZE */
|
||||
uint32_t image_crc32; /* CRC-32 (zlib) over image_size B */
|
||||
char name[MB_NAME_LEN]; /* human-readable, NUL-terminated */
|
||||
char fw_version[MB_VERSION_LEN]; /* firmware version string */
|
||||
uint8_t reserved[16]; /* pad to 64 bytes, future use */
|
||||
} mb_slot_header_t;
|
||||
|
||||
/* Multiboot operation result (a successful restore resets before returning). */
|
||||
enum {
|
||||
MB_OK = 0,
|
||||
MB_ERR_MAGIC, /* no/invalid slot header */
|
||||
MB_ERR_VERSION, /* header format too new */
|
||||
MB_ERR_NOT_COMMITTED,/* image not marked complete */
|
||||
MB_ERR_SIZE, /* image_size out of range */
|
||||
MB_ERR_CRC, /* image CRC32 mismatch */
|
||||
MB_ERR_SPI, /* external flash read/write timed out*/
|
||||
MB_ERR_SLOT, /* slot or bank index out of range */
|
||||
MB_ERR_AUTH, /* write refused: timestamp mismatch */
|
||||
MB_ERR_RAM_LOAD /* restore stub RAM copy mismatch */
|
||||
};
|
||||
|
||||
/* CRC-32 (zlib) over a resident memory buffer. Multiboot and overlay apps
|
||||
* share this implementation; keeping it here avoids carrying two identical
|
||||
* bitwise CRC loops in the MCU flash. */
|
||||
uint32_t MB_Crc32Bytes(const uint8_t *data, uint32_t len);
|
||||
|
||||
/* Multi-slot API used by the boot selector. Validation always covers the full
|
||||
* image CRC before restore. progress_line may point to a 128-byte LCD page; the
|
||||
* RAM copier then fills it while reflashing. Pass NULL to disable LCD updates.
|
||||
* With ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY, the copier is loaded over the
|
||||
* PY25Q16 sector cache only after validation and immediately before this call. */
|
||||
uint8_t MB_ValidateSlot(uint8_t slot, mb_slot_header_t *out_header, uint32_t *out_crc);
|
||||
uint8_t MB_RestoreSlot(uint8_t slot, uint8_t *progress_line);
|
||||
|
||||
/*
|
||||
* Host-tool slot management (M4, "Firmware Slots" in UV Studio). Everything is
|
||||
* bounds-checked (slot < MB_SLOT_COUNT, offset+len <= MB_SLOT_STRIDE) and writes
|
||||
* touch the EXTERNAL flash only, so none of this is brick-critical: a bad slot is
|
||||
* simply refused at restore by the CRC validation above.
|
||||
*
|
||||
* - MB_SlotInfo reads the 64-byte header only (fast, no CRC recompute) and
|
||||
* returns MB_OK / MB_ERR_* describing the header state.
|
||||
* - MB_SlotErase erases the whole 128 KiB slot region (header + image).
|
||||
* - MB_SlotWrite programs `len` bytes at slot_base+offset. The slot MUST have
|
||||
* been erased first (NOR flash only clears 1->0 bits); the host
|
||||
* writes the image, then the COMMITTED header last.
|
||||
* Use MB_ValidateSlot afterwards to confirm the full image CRC.
|
||||
*/
|
||||
uint8_t MB_SlotInfo(uint8_t slot, mb_slot_header_t *out_header);
|
||||
uint8_t MB_SlotErase(uint8_t slot);
|
||||
uint8_t MB_SlotWrite(uint8_t slot, uint32_t offset, const uint8_t *data, uint32_t len);
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Config banks (one per slot by default, switchable via SetCfg). */
|
||||
/* */
|
||||
/* Each firmware slot gets its own config bank by default (memory channels, */
|
||||
/* names, VFOs, settings) so switching firmware does not implicitly share or */
|
||||
/* clobber settings between editions. SetCfg deliberately lets the user pick */
|
||||
/* another bank after confirmation; compatibility is then the user's concern. */
|
||||
/* The banking itself is a single address offset applied in the flash driver */
|
||||
/* (PY25Q16_SetBankBase): everything below PY25Q16_BANK_SHARED_FROM */
|
||||
/* (0x010000, the calibration boundary) is per-bank, at/above stays shared. */
|
||||
/* Restoring slot N selects bank N initially, but slot and bank are tracked */
|
||||
/* independently afterwards. Bank 0 reuses the historical config region at */
|
||||
/* 0x000000 - no migration. */
|
||||
/* */
|
||||
/* External SPI flash (PY25Q16, 2 MiB) map: */
|
||||
/* 0x000000 bank 0 config (channels/settings) ] per-bank */
|
||||
/* 0x010000 calibration (512 B) ] shared */
|
||||
/* 0x011000 boot logo (4 KiB) ] shared */
|
||||
/* 0x020000 slot 0 firmware (BACKUP, 128 KiB) ] firmware-managed */
|
||||
/* 0x040000 slot 1 firmware (128 KiB) ] */
|
||||
/* 0x060000 slot 2 firmware ] user (UV Studio) */
|
||||
/* 0x080000 slot 3 firmware ] */
|
||||
/* 0x0A0000 slot 4 firmware ] */
|
||||
/* 0x0C0000 bank 1 config (64 KiB) ] */
|
||||
/* 0x0D0000 bank 2 config (64 KiB) ] per-bank */
|
||||
/* 0x0E0000 bank 3 config (64 KiB) ] */
|
||||
/* 0x0F0000 bank 4 config (64 KiB) ] */
|
||||
/* 0x100000 multiboot state A (4 KiB marker) ] shared */
|
||||
/* 0x101000 multiboot state B (4 KiB marker) ] redundant */
|
||||
/* 0x102000 -- free ~888 KiB -- */
|
||||
/* 0x1E0000 RX/TX log (32 KiB) ] shared */
|
||||
/* */
|
||||
/* Banks are 64 KiB for headroom; the live config footprint is ~44 KiB (max */
|
||||
/* physical config address 0x00A170). Keep the config banks past the last */
|
||||
/* slot if MB_SLOT_COUNT ever grows. */
|
||||
|
||||
#define MB_BANK_COUNT MB_SLOT_COUNT /* selectable config banks (0..4) */
|
||||
#define MB_BANK_SIZE 0x00010000u /* 64 KiB per config bank */
|
||||
#define MB_BANK1_EXT_BASE 0x000C0000u /* banks 1..4, right after slot 4 */
|
||||
#define MB_STATE_A_BASE 0x00100000u /* redundant marker sector A */
|
||||
#define MB_STATE_B_BASE 0x00101000u /* redundant marker sector B */
|
||||
|
||||
/* Active-state marker: two alternating external-flash sectors, never banked,
|
||||
* outside the EEPROM logical map. A new record is verified in the inactive
|
||||
* sector before it supersedes the previous one, so a power loss always leaves
|
||||
* at least one usable state. The expected firmware identity is stored here as
|
||||
* well: boot resolution never depends on the slot header remaining readable. */
|
||||
#define MB_STATE_LEGACY_MAGIC 0x31504D46u /* "FMP1" (single 8-byte record) */
|
||||
#define MB_STATE_V2_MAGIC 0x32504D46u /* "FMP2" (slot == config bank) */
|
||||
#define MB_STATE_MAGIC 0x33504D46u /* "FMP3" (slot + bank separated) */
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint32_t magic; /* MB_STATE_MAGIC */
|
||||
uint32_t generation; /* monotonically increasing record version */
|
||||
uint32_t image_size; /* expected internal image size */
|
||||
uint32_t image_crc32; /* expected internal image CRC-32 */
|
||||
uint8_t firmware_slot; /* exact source slot of the running image */
|
||||
uint8_t slot_inv; /* ~firmware_slot, quick integrity check */
|
||||
uint8_t config_bank; /* active config bank 0..MB_BANK_COUNT-1 */
|
||||
uint8_t bank_inv; /* ~config_bank, quick integrity check */
|
||||
uint32_t state_crc32; /* CRC-32 over all preceding fields */
|
||||
} mb_state_t;
|
||||
|
||||
/* External-flash base of a config bank. Bank 0 -> 0 (historical
|
||||
* region, identity map); banks 1..N -> past the slots. Out-of-range -> 0. */
|
||||
uint32_t MB_BankBase(uint8_t bank);
|
||||
|
||||
/* Result of reading the active-state marker. A boot must treat these very
|
||||
* differently: MISSING = fresh radio (adopting the running firmware as Main is
|
||||
* fine); IO / CORRUPT = uncertain (must NOT overwrite Main). */
|
||||
typedef enum {
|
||||
MB_MARK_VALID = 0, /* valid FMP2/FMP3; *state normalized to FMP3 */
|
||||
MB_MARK_LEGACY, /* valid FMP1 index; identity not stored */
|
||||
MB_MARK_MISSING, /* read OK but both sectors erased */
|
||||
MB_MARK_CORRUPT, /* read OK but magic/integrity bad */
|
||||
MB_MARK_IO, /* could not be read (SPI error) */
|
||||
} mb_mark_status_t;
|
||||
|
||||
/* Read the newest valid active-state marker, distinguishing the states above. */
|
||||
mb_mark_status_t MB_ReadActiveState(mb_state_t *state);
|
||||
|
||||
/* Write the active-state marker into the inactive redundant sector and read it
|
||||
* back to confirm it landed. The previous valid record is kept intact. The slot
|
||||
* header supplies the expected internal firmware identity. Use this right before
|
||||
* reflashing to `slot`: both the exact firmware slot and the initial config
|
||||
* bank become `slot`. */
|
||||
uint8_t MB_SetActiveSlot(uint8_t slot);
|
||||
|
||||
/* Switch ONLY the active settings bank, keeping the firmware that is running.
|
||||
* Unlike MB_SetActiveSlot (which records that slot's image as the
|
||||
* expected identity, for the imminent reflash to that slot), this preserves the
|
||||
* running firmware's identity taken from the current marker and changes only the
|
||||
* config bank. The next boot therefore maps a different bank with no
|
||||
* reflash and is never mistaken for an out-of-multiboot firmware change (which
|
||||
* would self-backup + reset to bank 0). Requires a currently valid marker -
|
||||
* what every normal boot leaves behind - else MB_ERR_SPI / MB_ERR_MAGIC. The
|
||||
* caller resets the MCU afterwards; the new bank takes effect at the next boot. */
|
||||
uint8_t MB_SetActiveBank(uint8_t bank);
|
||||
|
||||
/* Erase a whole config bank (host "Reset config" for a user slot):
|
||||
* the next boot using that bank reads 0xFF and re-seeds factory defaults. Bank 0
|
||||
* (the base) is refused - reset it by factory-resetting the base firmware.
|
||||
* External flash only, never brick-critical. */
|
||||
uint8_t MB_BankErase(uint8_t bank);
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Slot 0 self-backup (base firmware). */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
/* Progress callback for the (slow) slot-0 self-backup; may be NULL. */
|
||||
typedef void (*mb_progress_fn)(uint32_t done, uint32_t total);
|
||||
|
||||
/* Does the running internal firmware carry the DECLARED identity of `slot`
|
||||
* (CRC32 over image_size == the slot header's image_crc32)? This checks the
|
||||
* header's claim, not the stored image itself (that is validated once, right
|
||||
* after a backup). IO is reported separately so a transient SPI error is never
|
||||
* mistaken for a mismatch - which would wrongly trigger a self-backup over Main. */
|
||||
typedef enum {
|
||||
MB_FW_MATCH = 0, /* internal carries this slot's declared identity */
|
||||
MB_FW_MISMATCH, /* reliably not this slot (or slot has no header) */
|
||||
MB_FW_IO, /* slot header unreadable: uncertain */
|
||||
} mb_fw_match_t;
|
||||
|
||||
mb_fw_match_t MB_InternalMatchesSlot(uint8_t slot);
|
||||
|
||||
/* Compare internal flash with the identity stored in a validated marker. */
|
||||
bool MB_InternalMatchesState(const mb_state_t *state);
|
||||
|
||||
/* Back up the running internal firmware into slot 0 (erase + full image +
|
||||
* COMMITTED header, name=edition, version) and validate the stored image by a
|
||||
* full external CRC read-back. External flash only, never brick-critical; a
|
||||
* failure (SPI or CRC) leaves the slot uncommitted and does not advance the
|
||||
* marker, so boot resolution retries it. progress may be NULL. */
|
||||
uint8_t MB_BackupInternalToSlot0(mb_progress_fn progress);
|
||||
|
||||
#endif /* DRIVER_MB_FLASH_H */
|
||||
+83
-2
@@ -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));
|
||||
|
||||
@@ -22,7 +22,45 @@
|
||||
|
||||
void PY25Q16_Init();
|
||||
void PY25Q16_ReadBuffer(uint32_t Address, void *pBuffer, uint32_t Size);
|
||||
void PY25Q16_ReadBufferSafe(uint32_t Address, void *pBuffer, uint32_t Size);
|
||||
void PY25Q16_WriteBuffer(uint32_t Address, const void *pBuffer, uint32_t Size, bool Append);
|
||||
void PY25Q16_SectorErase(uint32_t Address);
|
||||
|
||||
/* Drop the internal single-sector write cache. Call after erasing/programming
|
||||
* flash behind the driver's back (e.g. the raw multiboot slot/bank ops) so a
|
||||
* later write cannot skip or resurrect data based on a stale cached sector. It
|
||||
* is also called before multiboot reuses the cache storage as a RAM overlay. */
|
||||
void PY25Q16_InvalidateCache(void);
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_APPS
|
||||
/* The 4 KiB sector cache, reused as the overlay-app execution workspace. */
|
||||
uint8_t *PY25Q16_OverlayBuffer(void);
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
|
||||
/*
|
||||
* Multiboot per-bank config banking.
|
||||
*
|
||||
* Each firmware slot gets its own config bank by default (memory channels,
|
||||
* names, VFOs, settings), though SetCfg can point the running firmware at a
|
||||
* different bank. A non-zero bank base transparently shifts every flash access
|
||||
* BELOW PY25Q16_BANK_SHARED_FROM into the active bank; calibration, boot logo,
|
||||
* firmware slots and the multiboot marker all live at/above that boundary and
|
||||
* stay shared across every bank.
|
||||
*
|
||||
* This is the single choke point: both the EEPROM emulation (eeprom_compat.c)
|
||||
* and the firmware's direct config reads/writes (settings.c) end up here, so
|
||||
* one offset covers them all - no per-call-site patching.
|
||||
*
|
||||
* Set once at boot, before any settings read, from
|
||||
* PY25Q16_SetBankBase(MB_BankBase(MB_BootResolveState()));
|
||||
* and never changed again during a session (a slot restore or SetCfg selection
|
||||
* records the next bank and resets first), so the banking itself never needs a
|
||||
* cache flush. Raw bank erases behind the driver explicitly call
|
||||
* PY25Q16_InvalidateCache().
|
||||
*/
|
||||
#define PY25Q16_BANK_SHARED_FROM 0x00010000u /* calibration boundary (see flash map) */
|
||||
void PY25Q16_SetBankBase(uint32_t Base);
|
||||
#endif
|
||||
|
||||
#endif
|
||||
+15
-6
@@ -204,9 +204,11 @@ void ST7565_DrawLine(const unsigned int Column, const unsigned int Line, const u
|
||||
void ST7565_FillScreen(uint8_t value)
|
||||
{
|
||||
CS_Assert();
|
||||
for (unsigned i = 0; i < 8; i++) {
|
||||
// TODO: This is wrong
|
||||
DrawLine(0, i, NULL, value);
|
||||
for (uint8_t line = 0; line < 8u; line++) {
|
||||
ST7565_SelectColumnAndLine(4u, line);
|
||||
A0_Set();
|
||||
for (uint8_t column = 0; column < LCD_WIDTH; column++)
|
||||
SPI_WriteByte(value);
|
||||
}
|
||||
CS_Release();
|
||||
}
|
||||
@@ -333,6 +335,10 @@ void ST7565_Init(void)
|
||||
SPI_Init();
|
||||
ST7565_HardwareReset();
|
||||
CS_Assert();
|
||||
|
||||
/* Hide the controller RAM immediately. On K1 there is no usable hardware
|
||||
* reset pin, so its power-on contents can otherwise briefly reach the LCD. */
|
||||
ST7565_WriteByte(ST7565_CMD_DISPLAY_ON_OFF | 0);
|
||||
ST7565_WriteByte(ST7565_CMD_SOFTWARE_RESET); // software reset
|
||||
SYSTEM_DelayMs(120);
|
||||
|
||||
@@ -354,13 +360,16 @@ void ST7565_Init(void)
|
||||
ST7565_WriteByte(ST7565_CMD_POWER_CIRCUIT | 0b111); // VB=1 VR=1 VF=1
|
||||
|
||||
SYSTEM_DelayMs(40);
|
||||
|
||||
ST7565_WriteByte(ST7565_CMD_SET_START_LINE | 0); // line 0
|
||||
ST7565_WriteByte(ST7565_CMD_DISPLAY_ON_OFF | 1); // D=1
|
||||
|
||||
ST7565_WriteByte(ST7565_CMD_SET_START_LINE | 0); // line 0
|
||||
CS_Release();
|
||||
|
||||
/* Clear all eight LCD RAM pages while the display is still disabled. */
|
||||
ST7565_FillScreen(0x00);
|
||||
|
||||
CS_Assert();
|
||||
ST7565_WriteByte(ST7565_CMD_DISPLAY_ON_OFF | 1); // D=1
|
||||
CS_Release();
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_SLEEP
|
||||
|
||||
+8
-42
@@ -17,13 +17,13 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "app/dtmf.h"
|
||||
#if defined(ENABLE_FMRADIO)
|
||||
#if defined(ENABLE_FMRADIO_EMBEDDED)
|
||||
#include "app/fm.h"
|
||||
#endif
|
||||
#include "audio.h"
|
||||
#include "dcs.h"
|
||||
#include "driver/backlight.h"
|
||||
#if defined(ENABLE_FMRADIO)
|
||||
#if defined(ENABLE_FMRADIO_EMBEDDED)
|
||||
#include "driver/bk1080.h"
|
||||
#endif
|
||||
#include "driver/bk4819.h"
|
||||
@@ -103,7 +103,7 @@ void FUNCTION_Foreground(const FUNCTION_Type_t PreviousFunction)
|
||||
return;
|
||||
}
|
||||
|
||||
#if defined(ENABLE_FMRADIO)
|
||||
#if defined(ENABLE_FMRADIO_EMBEDDED)
|
||||
if (gFmRadioMode)
|
||||
gFM_RestoreCountdown_10ms = fm_restore_countdown_10ms;
|
||||
#endif
|
||||
@@ -156,34 +156,11 @@ void FUNCTION_Transmit()
|
||||
gDTMF_RX_live_timeout = 0;
|
||||
DTMF_clear_input_box_memory();
|
||||
|
||||
#if defined(ENABLE_FMRADIO)
|
||||
#if defined(ENABLE_FMRADIO_EMBEDDED)
|
||||
if (gFmRadioMode)
|
||||
BK1080_Init0();
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_ALARM
|
||||
if (gAlarmState == ALARM_STATE_SITE_ALARM)
|
||||
{
|
||||
GUI_DisplayScreen();
|
||||
|
||||
AUDIO_AudioPathOff();
|
||||
|
||||
SYSTEM_DelayMs(20);
|
||||
BK4819_PlayTone(500, 0);
|
||||
SYSTEM_DelayMs(2);
|
||||
|
||||
AUDIO_AudioPathOn();
|
||||
|
||||
gEnableSpeaker = true;
|
||||
|
||||
SYSTEM_DelayMs(60);
|
||||
BK4819_ExitTxMute();
|
||||
|
||||
gAlarmToneCounter = 0;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
gUpdateStatus = true;
|
||||
|
||||
GUI_DisplayScreen();
|
||||
@@ -198,20 +175,9 @@ void FUNCTION_Transmit()
|
||||
if (gCurrentVfo->DTMF_PTT_ID_TX_MODE == PTT_ID_APOLLO)
|
||||
BK4819_PlaySingleTone(2525, 250, 0, gEeprom.DTMF_SIDE_TONE);
|
||||
|
||||
#if defined(ENABLE_ALARM) || defined(ENABLE_TX1750)
|
||||
if (gAlarmState != ALARM_STATE_OFF) {
|
||||
#ifdef ENABLE_TX1750
|
||||
if (gAlarmState == ALARM_STATE_TX1750)
|
||||
BK4819_TransmitTone(true, 1750);
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_ALARM
|
||||
if (gAlarmState == ALARM_STATE_TXALARM)
|
||||
BK4819_TransmitTone(true, 500);
|
||||
|
||||
gAlarmToneCounter = 0;
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_TX1750
|
||||
if (gTx1750Active) {
|
||||
BK4819_TransmitTone(true, 1750);
|
||||
SYSTEM_DelayMs(2);
|
||||
AUDIO_AudioPathOn();
|
||||
gEnableSpeaker = true;
|
||||
@@ -318,7 +284,7 @@ void FUNCTION_Select(FUNCTION_Type_t Function)
|
||||
gBatterySaveCountdown_10ms = battery_save_count_10ms;
|
||||
gSchedulePowerSave = false;
|
||||
|
||||
#if defined(ENABLE_FMRADIO)
|
||||
#if defined(ENABLE_FMRADIO_EMBEDDED)
|
||||
if(Function != FUNCTION_INCOMING)
|
||||
gFM_RestoreCountdown_10ms = 0;
|
||||
#endif
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <assert.h>
|
||||
|
||||
#include "battery.h"
|
||||
#include "board.h"
|
||||
#include "driver/backlight.h"
|
||||
#include "driver/st7565.h"
|
||||
#include "functions.h"
|
||||
@@ -129,6 +130,16 @@ unsigned int BATTERY_VoltsToPercent(const unsigned int voltage_10mV)
|
||||
return 0;
|
||||
}
|
||||
|
||||
#if defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) || defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON)
|
||||
void BATTERY_Sample(const bool bDisplayBatteryLevel)
|
||||
{
|
||||
BOARD_ADC_GetBatteryInfo(&gBatteryVoltages[gBatteryVoltageIndex++], &gBatteryCurrent);
|
||||
if (gBatteryVoltageIndex > 3u)
|
||||
gBatteryVoltageIndex = 0u;
|
||||
BATTERY_GetReadings(bDisplayBatteryLevel);
|
||||
}
|
||||
#endif
|
||||
|
||||
void BATTERY_GetReadings(const bool bDisplayBatteryLevel)
|
||||
{
|
||||
const uint8_t PreviousBatteryLevel = gBatteryDisplayLevel;
|
||||
|
||||
@@ -45,6 +45,9 @@ typedef enum {
|
||||
|
||||
|
||||
unsigned int BATTERY_VoltsToPercent(unsigned int voltage_10mV);
|
||||
#if defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) || defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON)
|
||||
void BATTERY_Sample(bool bDisplayBatteryLevel);
|
||||
#endif
|
||||
void BATTERY_GetReadings(bool bDisplayBatteryLevel);
|
||||
void BATTERY_TimeSlice500ms(void);
|
||||
|
||||
|
||||
+27
-2
@@ -29,20 +29,40 @@
|
||||
#include "settings.h"
|
||||
#include "ui/menu.h"
|
||||
#include "ui/ui.h"
|
||||
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
|
||||
#include "ui/multiboot.h"
|
||||
#endif
|
||||
|
||||
BOOT_Mode_t BOOT_GetMode(void)
|
||||
{
|
||||
unsigned int i;
|
||||
KEY_Code_t Keys[2];
|
||||
bool PttPressed[2];
|
||||
|
||||
/* Poll the keypad even without PTT: holding MENU alone enters multiboot.
|
||||
* The two samples keep the same debounce rule as the legacy boot modes. */
|
||||
for (i = 0; i < 2; i++)
|
||||
{
|
||||
if (!GPIO_IsPttPressed())
|
||||
return BOOT_MODE_NORMAL; // PTT not pressed
|
||||
PttPressed[i] = GPIO_IsPttPressed();
|
||||
Keys[i] = KEYBOARD_Poll();
|
||||
SYSTEM_DelayMs(20);
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
|
||||
if (!PttPressed[0] && !PttPressed[1] &&
|
||||
Keys[0] == KEY_MENU && Keys[1] == KEY_MENU)
|
||||
{
|
||||
gKeyReading0 = Keys[0];
|
||||
gKeyReading1 = Keys[0];
|
||||
gDebounceCounter = 2;
|
||||
return BOOT_MODE_MULTIBOOT;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* All historical special modes still require PTT for both samples. */
|
||||
if (!PttPressed[0] || !PttPressed[1])
|
||||
return BOOT_MODE_NORMAL;
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
|
||||
if (Keys[0] == (10 + gEeprom.SET_KEY))
|
||||
{
|
||||
@@ -125,5 +145,10 @@ void BOOT_ProcessMode(BOOT_Mode_t Mode)
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
|
||||
else if (Mode == BOOT_MODE_MULTIBOOT)
|
||||
UI_MultibootSelector();
|
||||
#endif
|
||||
|
||||
GUI_SelectNextDisplay(display);
|
||||
}
|
||||
+4
-1
@@ -28,7 +28,10 @@ enum BOOT_Mode_t
|
||||
BOOT_MODE_RESCUE_OPS,
|
||||
#endif
|
||||
#ifdef ENABLE_AIRCOPY
|
||||
BOOT_MODE_AIRCOPY
|
||||
BOOT_MODE_AIRCOPY,
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
|
||||
BOOT_MODE_MULTIBOOT,
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
+21
-1
@@ -18,7 +18,12 @@
|
||||
#include "driver/st7565.h"
|
||||
#include "k5viewer.h"
|
||||
#include "misc.h"
|
||||
#ifdef ENABLE_UART
|
||||
#include "driver/uart.h"
|
||||
#endif
|
||||
#ifdef ENABLE_USB
|
||||
#include "driver/vcp.h"
|
||||
#endif
|
||||
#include "driver/keyboard.h"
|
||||
#include "driver/bk4819.h"
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
|
||||
@@ -67,26 +72,41 @@ void K5VIEWER_ParseInput(void)
|
||||
if (K5VIEWER_IsLocked())
|
||||
return;
|
||||
|
||||
#ifdef ENABLE_UART
|
||||
if (UART_IsCableConnected()) {
|
||||
keepAlive = 15;
|
||||
hasConnectionPing = true;
|
||||
gUSB_K5ViewerEnabled = false;
|
||||
return;
|
||||
}
|
||||
else if (VCP_K5ViewerPing()) {
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_USB
|
||||
if (VCP_K5ViewerPing()) {
|
||||
keepAlive = 15;
|
||||
hasConnectionPing = true;
|
||||
gUSB_K5ViewerEnabled = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
static void K5VIEWER_Send(const uint8_t *buf, uint16_t len)
|
||||
{
|
||||
#if defined(ENABLE_UART) && defined(ENABLE_USB)
|
||||
if (gUSB_K5ViewerEnabled) {
|
||||
cdc_acm_data_send_with_dtr(buf, len);
|
||||
} else {
|
||||
UART_Send(buf, len);
|
||||
}
|
||||
#elif defined(ENABLE_USB)
|
||||
cdc_acm_data_send_with_dtr(buf, len);
|
||||
#elif defined(ENABLE_UART)
|
||||
UART_Send(buf, len);
|
||||
#else
|
||||
(void)buf;
|
||||
(void)len;
|
||||
#endif
|
||||
}
|
||||
|
||||
enum {
|
||||
|
||||
+22
-9
@@ -18,10 +18,6 @@
|
||||
#include <string.h>
|
||||
#include <stdio.h> // NULL
|
||||
|
||||
#ifdef ENABLE_AM_FIX
|
||||
#include "am_fix.h"
|
||||
#endif
|
||||
|
||||
#include "audio.h"
|
||||
#include "board.h"
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
|
||||
@@ -52,6 +48,10 @@
|
||||
#include "driver/system.h"
|
||||
#include "driver/systick.h"
|
||||
#include "driver/py25q16.h"
|
||||
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
|
||||
#include "driver/mb_flash.h"
|
||||
#include "ui/multiboot.h"
|
||||
#endif
|
||||
#ifdef ENABLE_UART
|
||||
#include "driver/uart.h"
|
||||
#endif
|
||||
@@ -81,6 +81,14 @@ void Main(void)
|
||||
SYSTICK_Init();
|
||||
BOARD_Init();
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
|
||||
/* Resolve the active settings bank BEFORE any EEPROM/settings access
|
||||
* below. This also adopts a normally-flashed firmware as slot 0 (discreet
|
||||
* self-backup) when the running image isn't the slot the marker points to.
|
||||
* Calibration stays shared regardless of the selected bank. */
|
||||
PY25Q16_SetBankBase(MB_BankBase(MB_BootResolveState()));
|
||||
#endif
|
||||
|
||||
boot_counter_10ms = 250; // 2.5 sec
|
||||
|
||||
#ifdef ENABLE_UART
|
||||
@@ -111,7 +119,6 @@ void Main(void)
|
||||
gCB = gEeprom.CROSS_BAND_RX_TX;
|
||||
#endif
|
||||
|
||||
SETTINGS_WriteBuildOptions();
|
||||
SETTINGS_LoadCalibration();
|
||||
|
||||
RADIO_ConfigureChannel(0, VFO_CONFIGURE_RELOAD);
|
||||
@@ -126,12 +133,18 @@ void Main(void)
|
||||
|
||||
BATTERY_GetReadings(false);
|
||||
|
||||
#ifdef ENABLE_AM_FIX
|
||||
AM_fix_init();
|
||||
#endif
|
||||
|
||||
BOOT_Mode_t BootMode = BOOT_GetMode();
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
|
||||
/* Run before the welcome screen and the normal application UI. EXIT from
|
||||
* the selector simply resumes this boot as if no special mode was held. */
|
||||
if (BootMode == BOOT_MODE_MULTIBOOT)
|
||||
{
|
||||
BOOT_ProcessMode(BootMode);
|
||||
BootMode = BOOT_MODE_NORMAL;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
|
||||
if (BootMode == BOOT_MODE_RESCUE_OPS)
|
||||
{
|
||||
|
||||
+5
-16
@@ -107,10 +107,6 @@ bool gSetting_ScrambleEnable;
|
||||
|
||||
enum BacklightOnRxTx_t gSetting_backlight_on_tx_rx;
|
||||
|
||||
#ifdef ENABLE_AM_FIX
|
||||
bool gSetting_AM_fix = true;
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_SLEEP
|
||||
uint8_t gSetting_set_off = 1;
|
||||
bool gWakeUp = false;
|
||||
@@ -148,9 +144,6 @@ enum BacklightOnRxTx_t gSetting_backlight_on_tx_rx;
|
||||
uint8_t gDW = 0;
|
||||
uint8_t gCB = 0;
|
||||
bool gSaveRxMode = false;
|
||||
uint8_t crc[15] = { 0 };
|
||||
uint8_t lErrorsDuringAirCopy = 0;
|
||||
uint8_t gAircopyStep = 0;
|
||||
uint8_t gAircopyCurrentMapIndex = 0;
|
||||
bool gAirCopyBootMode = 0;
|
||||
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
|
||||
@@ -244,8 +237,8 @@ bool gCssBackgroundScan;
|
||||
volatile bool gScheduleScanListen = true;
|
||||
volatile uint16_t gScanPauseDelayIn_10ms;
|
||||
|
||||
#if defined(ENABLE_ALARM) || defined(ENABLE_TX1750)
|
||||
AlarmState_t gAlarmState;
|
||||
#ifdef ENABLE_TX1750
|
||||
bool gTx1750Active;
|
||||
#endif
|
||||
uint16_t gMenuCountdown;
|
||||
bool gPttWasReleased;
|
||||
@@ -258,7 +251,7 @@ bool gFlagResetVfos;
|
||||
bool gRequestSaveVFO;
|
||||
uint16_t gRequestSaveChannel;
|
||||
bool gRequestSaveSettings;
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
bool gRequestSaveFM;
|
||||
#endif
|
||||
bool gFlagPrepareTX;
|
||||
@@ -266,7 +259,7 @@ bool gFlagPrepareTX;
|
||||
bool gFlagAcceptSetting;
|
||||
bool gFlagRefreshSetting;
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
bool gFlagSaveFM;
|
||||
#endif
|
||||
bool g_CDCSS_Lost;
|
||||
@@ -288,10 +281,6 @@ uint16_t gNextMrChannel;
|
||||
ReceptionMode_t gRxReceptionMode;
|
||||
|
||||
bool gRxVfoIsActive;
|
||||
#ifdef ENABLE_ALARM
|
||||
uint8_t gAlarmToneCounter;
|
||||
uint16_t gAlarmRunningCounter;
|
||||
#endif
|
||||
bool gKeyBeingHeld;
|
||||
bool gPttIsPressed;
|
||||
uint8_t gPttDebounceCounter;
|
||||
@@ -323,7 +312,7 @@ volatile bool gNextTimeslice40ms;
|
||||
volatile bool gScheduleNOAA = true;
|
||||
#endif
|
||||
volatile bool gFlagTailNoteEliminationComplete;
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
volatile bool gScheduleFM;
|
||||
#endif
|
||||
|
||||
|
||||
+5
-22
@@ -65,14 +65,6 @@ enum {
|
||||
VFO_CONFIGURE_RELOAD
|
||||
};
|
||||
|
||||
enum AlarmState_t {
|
||||
ALARM_STATE_OFF = 0,
|
||||
ALARM_STATE_TXALARM,
|
||||
ALARM_STATE_SITE_ALARM,
|
||||
ALARM_STATE_TX1750
|
||||
};
|
||||
typedef enum AlarmState_t AlarmState_t;
|
||||
|
||||
enum ReceptionMode_t {
|
||||
RX_MODE_NONE = 0, // squelch close ?
|
||||
RX_MODE_DETECTED, // signal detected
|
||||
@@ -167,10 +159,6 @@ extern bool gSetting_ScrambleEnable;
|
||||
|
||||
extern enum BacklightOnRxTx_t gSetting_backlight_on_tx_rx;
|
||||
|
||||
#ifdef ENABLE_AM_FIX
|
||||
extern bool gSetting_AM_fix;
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_SLEEP
|
||||
extern uint8_t gSetting_set_off;
|
||||
extern bool gWakeUp;
|
||||
@@ -231,9 +219,6 @@ extern enum BacklightOnRxTx_t gSetting_backlight_on_tx_rx;
|
||||
extern uint8_t gDW;
|
||||
extern uint8_t gCB;
|
||||
extern bool gSaveRxMode;
|
||||
extern uint8_t crc[15];
|
||||
extern uint8_t lErrorsDuringAirCopy;
|
||||
extern uint8_t gAircopyStep;
|
||||
extern uint8_t gAircopyCurrentMapIndex;
|
||||
extern bool gAirCopyBootMode;
|
||||
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
|
||||
@@ -393,7 +378,7 @@ enum
|
||||
extern volatile bool gScheduleScanListen;
|
||||
extern volatile uint16_t gScanPauseDelayIn_10ms;
|
||||
|
||||
extern AlarmState_t gAlarmState;
|
||||
extern bool gTx1750Active;
|
||||
extern uint16_t gMenuCountdown;
|
||||
extern bool gPttWasReleased;
|
||||
extern bool gPttWasPressed;
|
||||
@@ -404,7 +389,7 @@ extern bool gFlagResetVfos;
|
||||
extern bool gRequestSaveVFO;
|
||||
extern uint16_t gRequestSaveChannel;
|
||||
extern bool gRequestSaveSettings;
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
extern bool gRequestSaveFM;
|
||||
#endif
|
||||
extern uint8_t gKeypadLocked;
|
||||
@@ -413,7 +398,7 @@ extern bool gFlagPrepareTX;
|
||||
extern bool gFlagAcceptSetting; // accept menu setting
|
||||
extern bool gFlagRefreshSetting; // refresh menu display
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
extern bool gFlagSaveFM;
|
||||
#endif
|
||||
extern bool g_CDCSS_Lost;
|
||||
@@ -438,8 +423,6 @@ extern ReceptionMode_t gRxReceptionMode;
|
||||
|
||||
//TRUE when dual watch is momentarly suspended and RX_VFO is locked to either last TX or RX
|
||||
extern bool gRxVfoIsActive;
|
||||
extern uint8_t gAlarmToneCounter;
|
||||
extern uint16_t gAlarmRunningCounter;
|
||||
extern bool gKeyBeingHeld;
|
||||
extern bool gPttIsPressed;
|
||||
extern uint8_t gPttDebounceCounter;
|
||||
@@ -454,7 +437,7 @@ extern uint8_t gFSKWriteIndex;
|
||||
extern volatile bool gNextTimeslice;
|
||||
extern bool gUpdateDisplay;
|
||||
extern bool gF_LOCK;
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
extern uint8_t gFM_ChannelPosition;
|
||||
#endif
|
||||
extern uint8_t gShowChPrefix;
|
||||
@@ -470,7 +453,7 @@ extern volatile bool gNextTimeslice40ms;
|
||||
#endif
|
||||
extern volatile bool gFlagTailNoteEliminationComplete;
|
||||
extern volatile uint8_t gVFOStateResumeCountdown_500ms;
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
extern volatile bool gScheduleFM;
|
||||
#endif
|
||||
extern uint8_t gIsLocked;
|
||||
|
||||
+9
-33
@@ -17,12 +17,11 @@
|
||||
#include "driver/bk4819-regs.h"
|
||||
#include <string.h>
|
||||
|
||||
#include "am_fix.h"
|
||||
#include "app/dtmf.h"
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
|
||||
#include "app/rxtx_log.h"
|
||||
#endif
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
#include "app/fm.h"
|
||||
#endif
|
||||
#include "audio.h"
|
||||
@@ -767,12 +766,7 @@ void RADIO_SetupRegisters(bool switchToForeground)
|
||||
case BK4819_FILTER_BW_WIDE:
|
||||
case BK4819_FILTER_BW_NARROW:
|
||||
case BK4819_FILTER_BW_NARROWER:
|
||||
#ifdef ENABLE_AM_FIX
|
||||
// BK4819_SetFilterBandwidth(Bandwidth, gRxVfo->Modulation == MODULATION_AM && gSetting_AM_fix);
|
||||
BK4819_SetFilterBandwidth(Bandwidth, true);
|
||||
#else
|
||||
BK4819_SetFilterBandwidth(Bandwidth, false);
|
||||
#endif
|
||||
BK4819_SetFilterBandwidth(Bandwidth, false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -893,7 +887,7 @@ void RADIO_SetupRegisters(bool switchToForeground)
|
||||
#ifdef ENABLE_NOAA
|
||||
&& !IS_NOAA_CHANNEL(gCurrentVfo->CHANNEL_SAVE)
|
||||
#endif
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
&& !gFmRadioMode
|
||||
#endif
|
||||
){
|
||||
@@ -994,12 +988,7 @@ void RADIO_SetTxParameters(void)
|
||||
case BK4819_FILTER_BW_WIDE:
|
||||
case BK4819_FILTER_BW_NARROW:
|
||||
case BK4819_FILTER_BW_NARROWER:
|
||||
#ifdef ENABLE_AM_FIX
|
||||
// BK4819_SetFilterBandwidth(Bandwidth, gCurrentVfo->Modulation == MODULATION_AM && gSetting_AM_fix);
|
||||
BK4819_SetFilterBandwidth(Bandwidth, true);
|
||||
#else
|
||||
BK4819_SetFilterBandwidth(Bandwidth, false);
|
||||
#endif
|
||||
BK4819_SetFilterBandwidth(Bandwidth, false);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1150,14 +1139,6 @@ void RADIO_SetupAGC(bool listeningAM, bool disable)
|
||||
return;
|
||||
lastSettings = newSettings;
|
||||
|
||||
#ifdef ENABLE_AM_FIX
|
||||
if (listeningAM && gSetting_AM_fix) {
|
||||
BK4819_SetAGC(0);
|
||||
AM_fix_enable(!disable);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
BK4819_SetAGC(!disable);
|
||||
BK4819_InitAGC(listeningAM);
|
||||
}
|
||||
@@ -1208,9 +1189,6 @@ void RADIO_PrepareTX(void)
|
||||
if(TX_freq_check(gCurrentVfo->pTX->Frequency) != 0
|
||||
#ifdef ENABLE_FEAT_F4HWN
|
||||
&& gCurrentVfo->TX_LOCK == true
|
||||
#endif
|
||||
#if defined(ENABLE_ALARM) || defined(ENABLE_TX1750)
|
||||
&& gAlarmState != ALARM_STATE_SITE_ALARM
|
||||
#endif
|
||||
){
|
||||
// TX frequency not allowed
|
||||
@@ -1235,19 +1213,17 @@ void RADIO_PrepareTX(void)
|
||||
State = VFO_STATE_TX_DISABLE;
|
||||
}
|
||||
#endif
|
||||
#ifndef ENABLE_TX_WHEN_AM
|
||||
else if (gCurrentVfo->Modulation != MODULATION_FM) {
|
||||
// not allowed to TX if in AM mode
|
||||
// AM and other non-FM modes are receive-only.
|
||||
State = VFO_STATE_TX_DISABLE;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (State != VFO_STATE_NORMAL) {
|
||||
// TX not allowed
|
||||
RADIO_SetVfoState(State);
|
||||
|
||||
#if defined(ENABLE_ALARM) || defined(ENABLE_TX1750)
|
||||
gAlarmState = ALARM_STATE_OFF;
|
||||
#ifdef ENABLE_TX1750
|
||||
gTx1750Active = false;
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_DTMF_CALLING
|
||||
@@ -1281,8 +1257,8 @@ void RADIO_PrepareTX(void)
|
||||
|
||||
gTxTimerCountdown_500ms = 0; // no timeout
|
||||
|
||||
#if defined(ENABLE_ALARM) || defined(ENABLE_TX1750)
|
||||
if (gAlarmState == ALARM_STATE_OFF)
|
||||
#ifdef ENABLE_TX1750
|
||||
if (!gTx1750Active)
|
||||
#endif
|
||||
{
|
||||
|
||||
|
||||
@@ -50,7 +50,6 @@ enum VfoState_t
|
||||
VFO_STATE_BAT_LOW,
|
||||
VFO_STATE_TX_DISABLE,
|
||||
VFO_STATE_TIMEOUT,
|
||||
VFO_STATE_ALARM,
|
||||
VFO_STATE_VOLTAGE_HIGH,
|
||||
_VFO_STATE_LAST_ELEMENT
|
||||
};
|
||||
|
||||
+2
-2
@@ -16,7 +16,7 @@
|
||||
|
||||
#include "scheduler.h"
|
||||
#include "app/chFrScanner.h"
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
#include "app/fm.h"
|
||||
#endif
|
||||
#include "app/scanner.h"
|
||||
@@ -136,7 +136,7 @@ void SysTick_Handler(void)
|
||||
DECREMENT_AND_TRIGGER(gCountdownToPlayNextVoice_10ms, gFlagPlayQueuedVoice);
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (gFM_ScanState != FM_SCAN_OFF && gCurrentFunction != FUNCTION_MONITOR)
|
||||
if (gCurrentFunction != FUNCTION_TRANSMIT && gCurrentFunction != FUNCTION_RECEIVE)
|
||||
DECREMENT_AND_TRIGGER(gFmPlayCountdown_10ms, gScheduleFM);
|
||||
|
||||
+19
-90
@@ -90,8 +90,10 @@ void SETTINGS_InitEEPROM(void)
|
||||
|
||||
// 4. Reset logo lines (clear to null for strlen() == 0)
|
||||
|
||||
char logoLines[32];
|
||||
PY25Q16_ReadBuffer(0x00A0C8, logoLines, sizeof(logoLines));
|
||||
/* The two boot-message lines are contiguous in external flash. */
|
||||
char bootMessageLines[32];
|
||||
PY25Q16_ReadBuffer(SETTINGS_BOOT_MESSAGE_LINE1_ADDR,
|
||||
bootMessageLines, sizeof(bootMessageLines));
|
||||
|
||||
bool needsWrite = false;
|
||||
|
||||
@@ -99,12 +101,12 @@ void SETTINGS_InitEEPROM(void)
|
||||
int offset = line * 16;
|
||||
|
||||
for (int i = 0; i < 16; i++) {
|
||||
char c = logoLines[offset + i];
|
||||
char c = bootMessageLines[offset + i];
|
||||
if (c == 0) {
|
||||
break;
|
||||
}
|
||||
if (c < 0x20 || c > 0x7E) {
|
||||
memset(logoLines + offset, 0, 16);
|
||||
memset(bootMessageLines + offset, 0, 16);
|
||||
needsWrite = true;
|
||||
break;
|
||||
}
|
||||
@@ -112,7 +114,8 @@ void SETTINGS_InitEEPROM(void)
|
||||
}
|
||||
|
||||
if (needsWrite) {
|
||||
PY25Q16_WriteBuffer(0x00A0C8, logoLines, sizeof(logoLines), false);
|
||||
PY25Q16_WriteBuffer(SETTINGS_BOOT_MESSAGE_LINE1_ADDR,
|
||||
bootMessageLines, sizeof(bootMessageLines), false);
|
||||
}
|
||||
|
||||
// 5. Reset dBmCorrTable
|
||||
@@ -152,7 +155,9 @@ void SETTINGS_InitEEPROM(void)
|
||||
gEeprom.SET_KEY = ((Data[4] >> 2) & 0x0F) > 4 ? 0 : (Data[4] >> 2) & 0x0F;
|
||||
gEeprom.SET_NAV = (Data[4] & 0x40) != 0;
|
||||
#else
|
||||
gEeprom.KEY_LOCK = (Data[4] < 2) ? Data[4] : false;
|
||||
// RescueOps fields can be present in a config bank shared with another
|
||||
// preset. Read only KEY_LOCK here and leave the other bits untouched.
|
||||
gEeprom.KEY_LOCK = (Data[4] & 0x01) != 0;
|
||||
#endif
|
||||
#ifdef ENABLE_VOX
|
||||
gEeprom.VOX_SWITCH = (Data[5] < 2) ? Data[5] : false;
|
||||
@@ -164,9 +169,6 @@ void SETTINGS_InitEEPROM(void)
|
||||
PY25Q16_ReadBuffer(0x00A008, Data, 8);
|
||||
gEeprom.BACKLIGHT_MAX = (Data[0] & 0xF) <= 10 ? (Data[0] & 0xF) : 10;
|
||||
gEeprom.BACKLIGHT_MIN = (Data[0] >> 4) < gEeprom.BACKLIGHT_MAX ? (Data[0] >> 4) : 0;
|
||||
#ifdef ENABLE_BLMIN_TMP_OFF
|
||||
gEeprom.BACKLIGHT_MIN_STAT = BLMIN_STAT_ON;
|
||||
#endif
|
||||
gEeprom.CHANNEL_DISPLAY_MODE = (Data[1] < 4) ? Data[1] : MDF_FREQUENCY; // 4 instead of 3 - extra display mode
|
||||
gEeprom.CROSS_BAND_RX_TX = (Data[2] < 3) ? Data[2] : CROSS_BAND_OFF;
|
||||
gEeprom.BATTERY_SAVE = (Data[3] < 6) ? Data[3] : 4;
|
||||
@@ -288,9 +290,6 @@ gEeprom.FreqChannel[1] = IS_FREQ_CHANNEL(Data16[5]) ? Data16[5] : (FREQ_CHANNE
|
||||
|
||||
// 0EA8..0EAF
|
||||
PY25Q16_ReadBuffer(0x00A0A8 + 0x18, Data, 8);
|
||||
#ifdef ENABLE_ALARM
|
||||
gEeprom.ALARM_MODE = (Data[0] < 2) ? Data[0] : true;
|
||||
#endif
|
||||
gEeprom.ROGER = (Data[1] < 3) ? Data[1] : ROGER_MODE_OFF;
|
||||
gEeprom.REPEATER_TAIL_TONE_ELIMINATION = (Data[2] < 11) ? Data[2] : 0;
|
||||
gEeprom.TX_VFO = (Data[3] < 2) ? Data[3] : 0;
|
||||
@@ -381,11 +380,7 @@ gEeprom.FreqChannel[1] = IS_FREQ_CHANNEL(Data16[5]) ? Data16[5] : (FREQ_CHANNE
|
||||
#ifdef ENABLE_AUDIO_BAR
|
||||
gSetting_mic_bar = !!(Data[7] & (1u << 4));
|
||||
#endif
|
||||
#ifndef ENABLE_FEAT_F4HWN
|
||||
#ifdef ENABLE_AM_FIX
|
||||
gSetting_AM_fix = !!(Data[7] & (1u << 5));
|
||||
#endif
|
||||
#endif
|
||||
// Data[7] bit 5 is reserved (legacy ENABLE_AM_FIX).
|
||||
gSetting_backlight_on_tx_rx = (Data[7] >> 6) & 3u;
|
||||
|
||||
if (!gEeprom.VFO_OPEN)
|
||||
@@ -906,7 +901,11 @@ void SETTINGS_SaveSettings(void)
|
||||
((gEeprom.SET_KEY & 0x0F) << 2) |
|
||||
(gEeprom.SET_NAV ? 0x40 : 0);
|
||||
#else
|
||||
State[4] = gEeprom.KEY_LOCK;
|
||||
// A non-RescueOps preset owns KEY_LOCK only. Preserve Set RescueOps,
|
||||
// SetKEY, SetNav and reserved bits from a config created by another
|
||||
// preset while updating bit 0.
|
||||
PY25Q16_ReadBuffer(0x00A004, &State[4], 1);
|
||||
State[4] = (State[4] & 0xFEu) | (gEeprom.KEY_LOCK ? 0x01u : 0u);
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_VOX
|
||||
@@ -1000,11 +999,7 @@ void SETTINGS_SaveSettings(void)
|
||||
|
||||
// 0x0EA8
|
||||
State = SecBuf + 0x18;
|
||||
#if defined(ENABLE_ALARM) || defined(ENABLE_TX1750)
|
||||
State[0] = gEeprom.ALARM_MODE;
|
||||
#else
|
||||
State[0] = false;
|
||||
#endif
|
||||
State[0] = false;
|
||||
State[1] = gEeprom.ROGER;
|
||||
State[2] = gEeprom.REPEATER_TAIL_TONE_ELIMINATION;
|
||||
State[3] = gEeprom.TX_VFO;
|
||||
@@ -1086,11 +1081,7 @@ void SETTINGS_SaveSettings(void)
|
||||
#ifdef ENABLE_AUDIO_BAR
|
||||
if (!gSetting_mic_bar) State[7] &= ~(1u << 4);
|
||||
#endif
|
||||
#ifndef ENABLE_FEAT_F4HWN
|
||||
#ifdef ENABLE_AM_FIX
|
||||
if (!gSetting_AM_fix) State[7] &= ~(1u << 5);
|
||||
#endif
|
||||
#endif
|
||||
// State[7] bit 5 is preserved (legacy ENABLE_AM_FIX).
|
||||
State[7] = (State[7] & ~(3u << 6)) | ((gSetting_backlight_on_tx_rx & 3u) << 6);
|
||||
|
||||
PY25Q16_WriteBuffer(0x00A150, SecBuf, 8, false);
|
||||
@@ -1269,68 +1260,6 @@ void SETTINGS_UpdateChannel(uint16_t channel, const VFO_Info_t *pVFO, bool keep)
|
||||
SETTINGS_SaveChannelName(channel, "");
|
||||
}
|
||||
|
||||
void SETTINGS_WriteBuildOptions(void)
|
||||
{
|
||||
uint8_t State[8];
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN
|
||||
// 0x1FF0
|
||||
PY25Q16_ReadBuffer(0x00A158, State, sizeof(State));
|
||||
#endif
|
||||
|
||||
State[0] = 0
|
||||
#ifdef ENABLE_FMRADIO
|
||||
| (1 << 0)
|
||||
#endif
|
||||
#ifdef ENABLE_NOAA
|
||||
| (1 << 1)
|
||||
#endif
|
||||
#ifdef ENABLE_VOICE
|
||||
| (1 << 2)
|
||||
#endif
|
||||
#ifdef ENABLE_VOX
|
||||
| (1 << 3)
|
||||
#endif
|
||||
#ifdef ENABLE_ALARM
|
||||
| (1 << 4)
|
||||
#endif
|
||||
#ifdef ENABLE_TX1750
|
||||
| (1 << 5)
|
||||
#endif
|
||||
#ifdef ENABLE_PWRON_PASSWORD
|
||||
| (1 << 6)
|
||||
#endif
|
||||
#ifdef ENABLE_DTMF_CALLING
|
||||
| (1 << 7)
|
||||
#endif
|
||||
;
|
||||
|
||||
State[1] = 0
|
||||
#ifdef ENABLE_FLASHLIGHT
|
||||
| (1 << 0)
|
||||
#endif
|
||||
#ifdef ENABLE_WIDE_RX
|
||||
| (1 << 1)
|
||||
#endif
|
||||
#ifdef ENABLE_BYP_RAW_DEMODULATORS
|
||||
| (1 << 2)
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_GAME
|
||||
| (1 << 3)
|
||||
#endif
|
||||
#ifdef ENABLE_AM_FIX
|
||||
| (1 << 4)
|
||||
#endif
|
||||
#ifdef ENABLE_SPECTRUM
|
||||
| (1 << 5)
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
|
||||
| (1 << 6)
|
||||
#endif
|
||||
;
|
||||
PY25Q16_WriteBuffer(0x00A158, State, sizeof(State), false);
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_RESUME_STATE
|
||||
void SETTINGS_WriteCurrentState(void)
|
||||
{
|
||||
|
||||
+33
-50
@@ -25,6 +25,10 @@
|
||||
#include "radio.h"
|
||||
#include <driver/backlight.h>
|
||||
|
||||
/* Shared PY25Q16 locations for the two configurable boot-message lines. */
|
||||
#define SETTINGS_BOOT_MESSAGE_LINE1_ADDR 0x00A0C8u
|
||||
#define SETTINGS_BOOT_MESSAGE_LINE2_ADDR 0x00A0D8u
|
||||
|
||||
enum POWER_OnDisplayMode_t {
|
||||
#ifdef ENABLE_FEAT_F4HWN
|
||||
POWER_ON_DISPLAY_MODE_ALL,
|
||||
@@ -100,43 +104,35 @@ enum {
|
||||
};
|
||||
|
||||
enum ACTION_OPT_t {
|
||||
ACTION_OPT_NONE = 0,
|
||||
ACTION_OPT_FLASHLIGHT,
|
||||
ACTION_OPT_POWER,
|
||||
ACTION_OPT_MONITOR,
|
||||
ACTION_OPT_SCAN,
|
||||
ACTION_OPT_VOX,
|
||||
ACTION_OPT_ALARM,
|
||||
ACTION_OPT_FM,
|
||||
ACTION_OPT_1750,
|
||||
ACTION_OPT_KEYLOCK,
|
||||
ACTION_OPT_A_B,
|
||||
ACTION_OPT_VFO_MR,
|
||||
ACTION_OPT_SWITCH_DEMODUL,
|
||||
ACTION_OPT_BLMIN_TMP_OFF, //BackLight Minimum Temporay OFF
|
||||
#ifdef ENABLE_FEAT_F4HWN
|
||||
ACTION_OPT_RXMODE,
|
||||
ACTION_OPT_MAINONLY,
|
||||
ACTION_OPT_PTT,
|
||||
ACTION_OPT_WN,
|
||||
ACTION_OPT_BACKLIGHT,
|
||||
ACTION_OPT_MUTE,
|
||||
ACTION_OPT_RXA,
|
||||
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
|
||||
ACTION_OPT_POWER_HIGH,
|
||||
ACTION_OPT_REMOVE_OFFSET,
|
||||
#endif
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_BEAM
|
||||
ACTION_OPT_BEAM,
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
|
||||
ACTION_OPT_RXTX_LOG,
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_FOXHUNT
|
||||
ACTION_OPT_FOXHUNT,
|
||||
#endif
|
||||
ACTION_OPT_LEN
|
||||
/* Persisted in EEPROM: never renumber or make these values conditional. */
|
||||
ACTION_OPT_NONE = 0,
|
||||
ACTION_OPT_FLASHLIGHT = 1,
|
||||
ACTION_OPT_POWER = 2,
|
||||
ACTION_OPT_MONITOR = 3,
|
||||
ACTION_OPT_SCAN = 4,
|
||||
ACTION_OPT_VOX = 5,
|
||||
ACTION_OPT_FM = 6,
|
||||
ACTION_OPT_1750 = 7,
|
||||
ACTION_OPT_KEYLOCK = 8,
|
||||
ACTION_OPT_A_B = 9,
|
||||
ACTION_OPT_VFO_MR = 10,
|
||||
ACTION_OPT_SWITCH_DEMODUL = 11,
|
||||
ACTION_OPT_RXMODE = 12,
|
||||
ACTION_OPT_MAINONLY = 13,
|
||||
ACTION_OPT_PTT = 14,
|
||||
ACTION_OPT_WN = 15,
|
||||
ACTION_OPT_MUTE = 16,
|
||||
ACTION_OPT_RXA = 17,
|
||||
|
||||
/* Preset-specific actions keep their IDs even when not compiled. */
|
||||
ACTION_OPT_RXTX_LOG = 18,
|
||||
ACTION_OPT_BEAM = 19,
|
||||
ACTION_OPT_POWER_HIGH = 20,
|
||||
ACTION_OPT_REMOVE_OFFSET = 21,
|
||||
ACTION_OPT_FOXHUNT = 22,
|
||||
ACTION_OPT_BEACON = 23,
|
||||
|
||||
ACTION_OPT_LEN = 24
|
||||
};
|
||||
|
||||
#ifdef ENABLE_VOICE
|
||||
@@ -149,12 +145,6 @@ enum ACTION_OPT_t {
|
||||
typedef enum VOICE_Prompt_t VOICE_Prompt_t;
|
||||
#endif
|
||||
|
||||
enum ALARM_Mode_t {
|
||||
ALARM_MODE_SITE = 0,
|
||||
ALARM_MODE_TONE
|
||||
};
|
||||
typedef enum ALARM_Mode_t ALARM_Mode_t;
|
||||
|
||||
enum ROGER_Mode_t {
|
||||
ROGER_MODE_OFF = 0,
|
||||
ROGER_MODE_ROGER,
|
||||
@@ -237,9 +227,6 @@ typedef struct {
|
||||
uint8_t field38_0x33;
|
||||
|
||||
uint8_t AUTO_KEYPAD_LOCK;
|
||||
#if defined(ENABLE_ALARM) || defined(ENABLE_TX1750)
|
||||
ALARM_Mode_t ALARM_MODE;
|
||||
#endif
|
||||
POWER_OnDisplayMode_t POWER_ON_DISPLAY_MODE;
|
||||
ROGER_Mode_t ROGER;
|
||||
uint8_t REPEATER_TAIL_TONE_ELIMINATION;
|
||||
@@ -301,9 +288,6 @@ typedef struct {
|
||||
|
||||
uint8_t KEY_M_LONG_PRESS_ACTION;
|
||||
uint8_t BACKLIGHT_MIN;
|
||||
#ifdef ENABLE_BLMIN_TMP_OFF
|
||||
BLMIN_STAT_t BACKLIGHT_MIN_STAT;
|
||||
#endif
|
||||
uint8_t BACKLIGHT_MAX;
|
||||
BATTERY_Type_t BATTERY_TYPE;
|
||||
#ifdef ENABLE_RSSI_BAR
|
||||
@@ -350,7 +334,6 @@ void SETTINGS_SaveChannelName(uint16_t channel, const char * name);
|
||||
void SETTINGS_SaveChannel(uint16_t Channel, uint8_t VFO, const VFO_Info_t *pVFO, uint8_t Mode);
|
||||
void SETTINGS_SaveBatteryCalibration(const uint16_t * batteryCalibration);
|
||||
void SETTINGS_UpdateChannel(uint16_t channel, const VFO_Info_t *pVFO, bool keep);
|
||||
void SETTINGS_WriteBuildOptions(void);
|
||||
#ifdef ENABLE_FEAT_F4HWN_RESUME_STATE
|
||||
void SETTINGS_WriteCurrentState(void);
|
||||
#endif
|
||||
|
||||
+49
-74
@@ -27,14 +27,6 @@
|
||||
#include "ui/helper.h"
|
||||
#include "ui/inputbox.h"
|
||||
|
||||
static void set_bit(uint8_t* array, int bit_index) {
|
||||
array[bit_index / 8] |= (1 << (bit_index % 8));
|
||||
}
|
||||
|
||||
static int get_bit(uint8_t* array, int bit_index) {
|
||||
return (array[bit_index / 8] >> (bit_index % 8)) & 1;
|
||||
}
|
||||
|
||||
void UI_DisplayAircopy(void)
|
||||
{
|
||||
char String[16];
|
||||
@@ -45,10 +37,21 @@ void UI_DisplayAircopy(void)
|
||||
if (gAircopyState == AIRCOPY_READY) {
|
||||
pPrintStr = "AIR COPY(RDY)";
|
||||
} else if (gAircopyState == AIRCOPY_TRANSFER) {
|
||||
pPrintStr = "AIR COPY";
|
||||
if (gAircopyAll) {
|
||||
// All mode: show the slice being replicated in place of the title.
|
||||
const uint8_t m = AIRCOPY_CurrentSliceMap();
|
||||
if (m < AIRCOPY_NUM_BANKS)
|
||||
sprintf(String, "MEM %03u-%03u", (m * 128) + 1, (m + 1) * 128);
|
||||
else
|
||||
strcpy(String, "SETTINGS");
|
||||
pPrintStr = String;
|
||||
} else {
|
||||
pPrintStr = "AIR COPY";
|
||||
}
|
||||
} else if (gAircopyState == AIRCOPY_COMPLETE) {
|
||||
pPrintStr = "AIR COPY OK";
|
||||
} else {
|
||||
pPrintStr = "AIR COPY(CMP)";
|
||||
gAircopyState = AIRCOPY_READY;
|
||||
pPrintStr = "AIR COPY FAIL";
|
||||
}
|
||||
|
||||
UI_PrintString(pPrintStr, 2, 127, 0, 8);
|
||||
@@ -67,89 +70,61 @@ void UI_DisplayAircopy(void)
|
||||
// show the main large frequency digits
|
||||
UI_DisplayFrequency(String, 16, 2, false);
|
||||
|
||||
// Get the current map and calculate percentage based on its total blocks
|
||||
const AIRCOPY_TransferMap_t *currentMap = AIRCOPY_GetCurrentMap();
|
||||
const uint16_t totalBlocks = AIRCOPY_GetTotalBlocks();
|
||||
uint16_t doneBlocks = gAirCopyBlockNumber;
|
||||
|
||||
uint16_t doneBlocks = gAirCopyBlockNumber + gErrorsDuringAirCopy;
|
||||
|
||||
if (doneBlocks > currentMap->total_blocks)
|
||||
doneBlocks = currentMap->total_blocks;
|
||||
if (doneBlocks > totalBlocks)
|
||||
doneBlocks = totalBlocks;
|
||||
|
||||
// Draw memory selection
|
||||
if (gAircopyState == AIRCOPY_READY)
|
||||
{
|
||||
doneBlocks = 0;
|
||||
|
||||
if(gAircopyCurrentMapIndex < AIRCOPY_NUM_BANKS) {
|
||||
if(gAircopyCurrentMapIndex < AIRCOPY_NUM_BANKS) {
|
||||
sprintf(String, "MEM %03u - %03u", (gAircopyCurrentMapIndex * 128) + 1, (gAircopyCurrentMapIndex + 1) * 128);
|
||||
} else if(gAircopyCurrentMapIndex == AIRCOPY_NUM_BANKS) {
|
||||
strcpy(String, "Settings");
|
||||
} else {
|
||||
strcpy(String, "Settings");
|
||||
strcpy(String, "All (Mem+Set)");
|
||||
}
|
||||
UI_PrintString(String, 2, 127, 5, 8);
|
||||
}
|
||||
else
|
||||
{
|
||||
uint16_t percent = (doneBlocks * 10000) / currentMap->total_blocks;
|
||||
uint16_t percent = (doneBlocks * 10000) / totalBlocks;
|
||||
const unsigned displayedErrors = gErrorsDuringAirCopy > 99u
|
||||
? 99u
|
||||
: gErrorsDuringAirCopy;
|
||||
|
||||
if (gAirCopyIsSendMode == 0) {
|
||||
sprintf(String, "RCV:%02u.%02u%% E:%d", percent / 100, percent % 100, gErrorsDuringAirCopy);
|
||||
if (gAircopyState == AIRCOPY_COMPLETE || gAircopyState == AIRCOPY_FAILED) {
|
||||
sprintf(String, "%s %u/%u %s:%u",
|
||||
gAircopyState == AIRCOPY_COMPLETE ? "OK" : "KO",
|
||||
doneBlocks, totalBlocks,
|
||||
gAirCopyIsSendMode ? "RT" : "ER",
|
||||
displayedErrors);
|
||||
} else if (gAirCopyIsSendMode == 0) {
|
||||
sprintf(String, "RX:%02u.%02u ER:%u", percent / 100, percent % 100,
|
||||
displayedErrors);
|
||||
} else {
|
||||
sprintf(String, "SND:%02u.%02u%%", percent / 100, percent % 100);
|
||||
sprintf(String, "TX:%02u.%02u RT:%u", percent / 100, percent % 100,
|
||||
displayedErrors);
|
||||
}
|
||||
|
||||
// Draw gauge
|
||||
if(gAircopyStep != 0)
|
||||
{
|
||||
UI_PrintString(String, 2, 127, 5, 8);
|
||||
|
||||
gFrameBuffer[4][1] = 0x3c;
|
||||
gFrameBuffer[4][2] = 0x42;
|
||||
|
||||
for(uint8_t i = 1; i <= AIRCOPY_BAR_WIDTH + 2; i++)
|
||||
{
|
||||
gFrameBuffer[4][2 + i] = 0x81;
|
||||
}
|
||||
|
||||
gFrameBuffer[4][125] = 0x42;
|
||||
gFrameBuffer[4][126] = 0x3c;
|
||||
}
|
||||
}
|
||||
|
||||
if (doneBlocks > 0)
|
||||
{
|
||||
// Track CRC errors per real block index
|
||||
if (gErrorsDuringAirCopy != lErrorsDuringAirCopy)
|
||||
{
|
||||
// Mark the last processed block as faulty
|
||||
set_bit(crc, doneBlocks - 1);
|
||||
lErrorsDuringAirCopy = gErrorsDuringAirCopy;
|
||||
}
|
||||
|
||||
uint16_t b = 0;
|
||||
uint16_t fraction_accumulator = 0;
|
||||
UI_PrintString(String, 2, 127, 5, 8);
|
||||
|
||||
gFrameBuffer[4][1] = 0x3c;
|
||||
gFrameBuffer[4][2] = 0x42;
|
||||
gFrameBuffer[4][3] = 0x81;
|
||||
// Match the former DDA gauge exactly, including its partial first pixel.
|
||||
const uint8_t filled = (doneBlocks * AIRCOPY_BAR_WIDTH + totalBlocks - 1u)
|
||||
/ totalBlocks;
|
||||
for (uint8_t col = 0; col < AIRCOPY_BAR_WIDTH; col++)
|
||||
{
|
||||
bool processed = (b < doneBlocks);
|
||||
bool error = processed && get_bit(crc, b);
|
||||
|
||||
if (!processed)
|
||||
gFrameBuffer[4][col + 4] = 0x81; // not yet processed
|
||||
else if (error)
|
||||
gFrameBuffer[4][col + 4] = 0x81; // error gap (intentional hole)
|
||||
else
|
||||
gFrameBuffer[4][col + 4] = 0xBD; // ok filled
|
||||
|
||||
// DDA/Bresenham algorythm
|
||||
fraction_accumulator += currentMap->total_blocks;
|
||||
while (fraction_accumulator >= AIRCOPY_BAR_WIDTH) {
|
||||
fraction_accumulator -= AIRCOPY_BAR_WIDTH;
|
||||
b++;
|
||||
}
|
||||
}
|
||||
gFrameBuffer[4][col + 4] = col < filled ? 0xBD : 0x81;
|
||||
gFrameBuffer[4][124] = 0x81;
|
||||
gFrameBuffer[4][125] = 0x42;
|
||||
gFrameBuffer[4][126] = 0x3c;
|
||||
}
|
||||
|
||||
ST7565_BlitFullScreen();
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
+18
-8
@@ -31,6 +31,13 @@
|
||||
|
||||
void UI_DisplayFM(void)
|
||||
{
|
||||
// Keep these labels in sync with the limits in driver/bk1080.c.
|
||||
static const char BandNames[][10] = {
|
||||
"87.5-108M",
|
||||
"76-108M",
|
||||
"76-90M",
|
||||
"64-76M",
|
||||
};
|
||||
char String[16];
|
||||
char *pPrintStr = String;
|
||||
UI_DisplayClear();
|
||||
@@ -41,13 +48,7 @@ void UI_DisplayFM(void)
|
||||
|
||||
UI_PrintString("FM", 2, 0, 0, 8);
|
||||
|
||||
sprintf(String, "%d%s-%dM",
|
||||
BK1080_GetFreqLoLimit(gEeprom.FM_Band)/10,
|
||||
gEeprom.FM_Band == 0 ? ".5" : "",
|
||||
BK1080_GetFreqHiLimit(gEeprom.FM_Band)/10
|
||||
);
|
||||
|
||||
UI_PrintStringSmallNormal(String, 1, 0, 6);
|
||||
UI_PrintStringSmallNormal(BandNames[gEeprom.FM_Band], 1, 0, 6);
|
||||
|
||||
//uint8_t spacings[] = {20,10,5};
|
||||
//sprintf(String, "%d0k", spacings[gEeprom.FM_Space % 3]);
|
||||
@@ -81,7 +82,16 @@ void UI_DisplayFM(void)
|
||||
UI_PrintString(pPrintStr, 0, 127, 3, 10); // memory, vfo, scan
|
||||
|
||||
if (gAskToSave || (gEeprom.FM_IsMrMode && gInputBoxIndex > 0)) {
|
||||
UI_GenerateChannelString(String, gFM_ChannelPosition);
|
||||
if (gInputBoxIndex == 0) {
|
||||
sprintf(String, "CH-%02u", gFM_ChannelPosition + 1);
|
||||
} else {
|
||||
String[0] = 'C';
|
||||
String[1] = 'H';
|
||||
String[2] = '-';
|
||||
for (unsigned int i = 0; i < 2; i++)
|
||||
String[i + 3] = (gInputBox[i] == 10) ? '-' : gInputBox[i] + '0';
|
||||
String[5] = '\0';
|
||||
}
|
||||
} else if (gAskToDelete) {
|
||||
sprintf(String, "CH-%02u", gEeprom.FM_SelectedChannel + 1);
|
||||
} else {
|
||||
|
||||
+2
-21
@@ -25,25 +25,6 @@
|
||||
#include "settings.h"
|
||||
|
||||
|
||||
void UI_GenerateChannelString(char *pString, const uint16_t Channel)
|
||||
{
|
||||
unsigned int i;
|
||||
|
||||
if (gInputBoxIndex == 0)
|
||||
{
|
||||
sprintf(pString, "CH-%02u", Channel + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
pString[0] = 'C';
|
||||
pString[1] = 'H';
|
||||
pString[2] = '-';
|
||||
for (i = 0; i < 2; i++)
|
||||
pString[i + 3] = (gInputBox[i] == 10) ? '-' : gInputBox[i] + '0';
|
||||
|
||||
pString[5] = 0;
|
||||
}
|
||||
|
||||
void UI_GenerateChannelStringEx(char *pString, const bool bShowPrefix, const uint16_t ChannelNumber)
|
||||
{
|
||||
if (gInputBoxIndex > 0) {
|
||||
@@ -421,12 +402,12 @@ void UI_DisplayPopup(const char *string)
|
||||
UI_PrintStringSmallNormal("Press EXIT", 9, 118, 6);
|
||||
}
|
||||
|
||||
void UI_DisplayClear()
|
||||
void UI_DisplayClear(void)
|
||||
{
|
||||
memset(gFrameBuffer, 0, sizeof(gFrameBuffer));
|
||||
}
|
||||
|
||||
void UI_StatusClear()
|
||||
void UI_StatusClear(void)
|
||||
{
|
||||
memset(gStatusLine, 0, sizeof(gStatusLine));
|
||||
}
|
||||
+2
-3
@@ -20,7 +20,6 @@
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
void UI_GenerateChannelString(char *pString, const uint16_t Channel);
|
||||
void UI_GenerateChannelStringEx(char *pString, const bool bShowPrefix, const uint16_t ChannelNumber);
|
||||
void UI_PrintString(const char *pString, uint8_t Start, uint8_t End, uint8_t Line, uint8_t Width);
|
||||
void UI_PrintStringSmallNormal(const char *pString, uint8_t Start, uint8_t End, uint8_t Line);
|
||||
@@ -45,7 +44,7 @@ void UI_DrawPixelBuffer(uint8_t (*buffer)[128], uint8_t x, uint8_t y, bool black
|
||||
void UI_DrawLineBuffer(uint8_t (*buffer)[128], int16_t x1, int16_t y1, int16_t x2, int16_t y2, bool black);
|
||||
void UI_DrawRectangleBuffer(uint8_t (*buffer)[128], int16_t x1, int16_t y1, int16_t x2, int16_t y2, bool black);
|
||||
|
||||
void UI_DisplayClear();
|
||||
void UI_StatusClear();
|
||||
void UI_DisplayClear(void);
|
||||
void UI_StatusClear(void);
|
||||
|
||||
#endif
|
||||
+75
-100
@@ -28,9 +28,6 @@
|
||||
#include "app/beam.h"
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_AM_FIX
|
||||
#include "am_fix.h"
|
||||
#endif
|
||||
#include "bitmaps.h"
|
||||
#include "board.h"
|
||||
#include "driver/bk4819.h"
|
||||
@@ -71,7 +68,10 @@ center_line_t center_line = CENTER_LINE_NONE;
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCAN_PROGRESS
|
||||
#define SCAN_PROGRESS_MR_CHANNEL_BYTES ((MR_CHANNELS_MAX + 7u) / 8u)
|
||||
#define SCAN_LIST_NAME_HOLD_500MS (2000u / 500u)
|
||||
// Scan-list name hold, in 10 ms ticks. Counted down on the 10 ms timeslice (not the
|
||||
// 500 ms one) so the hold is accurate to a single tick instead of +/- 500 ms. Stored
|
||||
// in a uint8_t, so the practical ceiling is 255 ticks = 2.55 s.
|
||||
#define SCAN_LIST_NAME_HOLD_10MS (1000u / 10u)
|
||||
|
||||
static bool gScanProgressSessionActive;
|
||||
static bool gScanProgressSessionIsMemory;
|
||||
@@ -86,7 +86,7 @@ static bool gScanProgressPrevResetVfosFlag;
|
||||
static bool gScanProgressForceRebuild;
|
||||
static uint16_t gScanProgressLastMemoryIndex;
|
||||
static uint8_t gScanProgressPriorityState;
|
||||
static uint8_t gScanListNameCountdown_500ms;
|
||||
static uint8_t gScanListNameCountdown_10ms;
|
||||
#define SCAN_PROGRESS_PRIORITY_LABEL_MASK 0x03u
|
||||
#define SCAN_PROGRESS_PRIORITY_SEEN_SHIFT 2
|
||||
#define SCAN_PROGRESS_PRIORITY_SEEN_MASK 0x1cu
|
||||
@@ -149,7 +149,6 @@ const char *const VfoStateStr[] = {
|
||||
[VFO_STATE_BAT_LOW]="BAT LOW",
|
||||
[VFO_STATE_TX_DISABLE]="TX DISABLE",
|
||||
[VFO_STATE_TIMEOUT]="TIMEOUT",
|
||||
[VFO_STATE_ALARM]="ALARM",
|
||||
[VFO_STATE_VOLTAGE_HIGH]="VOLT HIGH"
|
||||
};
|
||||
|
||||
@@ -227,7 +226,7 @@ static void ScanProgress_ResetSession(void)
|
||||
gScanProgressForceRebuild = false;
|
||||
gScanProgressLastMemoryIndex = 0;
|
||||
gScanProgressPriorityState = 0;
|
||||
gScanListNameCountdown_500ms = 0;
|
||||
gScanListNameCountdown_10ms = 0;
|
||||
}
|
||||
|
||||
void UI_MAIN_NotifyScanProgressDataChanged(void)
|
||||
@@ -239,7 +238,7 @@ void UI_MAIN_NotifyScanProgressDataChanged(void)
|
||||
void UI_MAIN_NotifyScanListChanged(void)
|
||||
{
|
||||
UI_MAIN_NotifyScanProgressDataChanged();
|
||||
gScanListNameCountdown_500ms = SCAN_LIST_NAME_HOLD_500MS;
|
||||
gScanListNameCountdown_10ms = SCAN_LIST_NAME_HOLD_10MS;
|
||||
gUpdateDisplay = true;
|
||||
}
|
||||
|
||||
@@ -254,7 +253,7 @@ void UI_MAIN_NotifyScanListChanged(void)
|
||||
// stall ~2 s with nothing on screen to explain the pause.
|
||||
bool UI_MAIN_ShouldHoldScanResume(void)
|
||||
{
|
||||
return gScanListNameCountdown_500ms > 0 && IS_MR_CHANNEL(gNextMrChannel);
|
||||
return gScanListNameCountdown_10ms > 0 && IS_MR_CHANNEL(gNextMrChannel);
|
||||
}
|
||||
|
||||
static inline void ScanProgress_SetBit(uint8_t *map, uint16_t ch)
|
||||
@@ -553,7 +552,7 @@ static bool UI_DrawScanProgress(void)
|
||||
}
|
||||
|
||||
// Right after a scan-list change, briefly show its name instead of the progress bar
|
||||
if (show_memory && gScanListNameCountdown_500ms > 0) {
|
||||
if (show_memory && gScanListNameCountdown_10ms > 0) {
|
||||
UI_MAIN_DrawScanListName();
|
||||
return true;
|
||||
}
|
||||
@@ -818,8 +817,8 @@ void UI_DisplayAudioBar(void)
|
||||
return; // screen is in use
|
||||
}
|
||||
|
||||
#if defined(ENABLE_ALARM) || defined(ENABLE_TX1750)
|
||||
if (gAlarmState != ALARM_STATE_OFF)
|
||||
#ifdef ENABLE_TX1750
|
||||
if (gTx1750Active)
|
||||
return;
|
||||
#endif
|
||||
static uint8_t barsOld = 0;
|
||||
@@ -847,15 +846,14 @@ void UI_DisplayAudioBar(void)
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_AUDIO_SCOPE
|
||||
|
||||
#if defined(ENABLE_FEAT_F4HWN_AUDIO_SCOPE) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
|
||||
#define SCOPE_SAMPLES 43 // number of columns (43 × 3px = 128px wide)
|
||||
#define SCOPE_NOISE_GATE 50u // minimum range below which the display shows baseline
|
||||
#define SCOPE_FLOOR_RISE 2u // floor rise per frame (+100 units/s at 20ms/frame)
|
||||
#define SCOPE_FLOOR_DROP_SHR 3u // floor drop IIR shift: drop by (floor-min) >> N per frame (~160ms to halve)
|
||||
#define SCOPE_VOLUME_MIN 200u // let's assume that the sound level in silence is 200
|
||||
|
||||
void UI_DisplayAudioScope(void)
|
||||
void UI_DisplayAudioScopeOverlay(const uint8_t line, const bool active)
|
||||
{
|
||||
static uint16_t g_scope_buf[SCOPE_SAMPLES];
|
||||
static uint8_t g_scope_write = 0;
|
||||
@@ -869,22 +867,11 @@ void UI_DisplayAudioScope(void)
|
||||
|
||||
static bool s_was_tx = false;
|
||||
|
||||
if (gCurrentFunction != FUNCTION_TRANSMIT) {
|
||||
if (!active) {
|
||||
s_was_tx = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// This prevents a sudden spike on the bar caused by release the PTT button
|
||||
if (!GPIO_IsPttPressed()
|
||||
#ifdef ENABLE_VOX
|
||||
&& !gEeprom.VOX_SWITCH
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN
|
||||
&& !gSetting_set_ptt_session
|
||||
#endif
|
||||
)
|
||||
return;
|
||||
|
||||
if (!s_was_tx) {
|
||||
// TX entry: full reset so every new transmission starts from a clean state
|
||||
for (uint8_t i = 0; i < SCOPE_SAMPLES; i++) g_scope_buf[i] = SCOPE_VOLUME_MIN;
|
||||
@@ -907,32 +894,6 @@ void UI_DisplayAudioScope(void)
|
||||
|
||||
g_scope_write = (g_scope_write + 1u) % SCOPE_SAMPLES;
|
||||
|
||||
// --------------------------------- Refresh display ---------------------------------
|
||||
|
||||
if (gLowBattery && !gLowBatteryConfirmed)
|
||||
return;
|
||||
|
||||
if (gScreenToDisplay != DISPLAY_MAIN
|
||||
#ifdef ENABLE_DTMF_CALLING
|
||||
|| gDTMF_CallState != DTMF_CALL_STATE_NONE
|
||||
#endif
|
||||
)
|
||||
return;
|
||||
|
||||
#if defined(ENABLE_ALARM) || defined(ENABLE_TX1750)
|
||||
if (gAlarmState != ALARM_STATE_OFF)
|
||||
return;
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN
|
||||
RxBlinkLed = 0;
|
||||
RxBlinkLedCounter = 0;
|
||||
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, false);
|
||||
const unsigned int line = isMainOnly() ? 5 : 3;
|
||||
#else
|
||||
const unsigned int line = 3;
|
||||
#endif
|
||||
|
||||
uint8_t *p_line = gFrameBuffer[line];
|
||||
memset(p_line, 0, LCD_WIDTH);
|
||||
|
||||
@@ -972,9 +933,50 @@ void UI_DisplayAudioScope(void)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_AUDIO_SCOPE
|
||||
void UI_DisplayAudioScope(void)
|
||||
{
|
||||
const unsigned int line = isMainOnly() ? 5u : 3u;
|
||||
|
||||
/* Keep MAIN's original gating and side effects outside the shared renderer. */
|
||||
if (gCurrentFunction != FUNCTION_TRANSMIT) {
|
||||
UI_DisplayAudioScopeOverlay((uint8_t)line, false);
|
||||
return;
|
||||
}
|
||||
if (!GPIO_IsPttPressed()
|
||||
#ifdef ENABLE_VOX
|
||||
&& !gEeprom.VOX_SWITCH
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN
|
||||
&& !gSetting_set_ptt_session
|
||||
#endif
|
||||
)
|
||||
return;
|
||||
if (gLowBattery && !gLowBatteryConfirmed)
|
||||
return;
|
||||
if (gScreenToDisplay != DISPLAY_MAIN
|
||||
#ifdef ENABLE_DTMF_CALLING
|
||||
|| gDTMF_CallState != DTMF_CALL_STATE_NONE
|
||||
#endif
|
||||
)
|
||||
return;
|
||||
#ifdef ENABLE_TX1750
|
||||
if (gTx1750Active)
|
||||
return;
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN
|
||||
RxBlinkLed = 0;
|
||||
RxBlinkLedCounter = 0;
|
||||
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, false);
|
||||
#endif
|
||||
UI_DisplayAudioScopeOverlay((uint8_t)line, true);
|
||||
ST7565_BlitLine(line);
|
||||
}
|
||||
#endif // ENABLE_FEAT_F4HWN_AUDIO_SCOPE
|
||||
#endif
|
||||
#endif // ENABLE_FEAT_F4HWN_AUDIO_SCOPE || ENABLE_FEAT_F4HWN_OVERLAY_APPS
|
||||
|
||||
void DisplayRSSIBar(const bool now)
|
||||
{
|
||||
@@ -1075,9 +1077,6 @@ void DisplayRSSIBar(const bool now)
|
||||
#ifdef ENABLE_FEAT_F4HWN
|
||||
int16_t rssi_dBm =
|
||||
BK4819_GetRSSI_dBm()
|
||||
#ifdef ENABLE_AM_FIX
|
||||
+ ((gSetting_AM_fix && gRxVfo->Modulation == MODULATION_AM) ? AM_fix_get_gain_diff() : 0)
|
||||
#endif
|
||||
+ dBmCorrTable[gRxVfo->Band];
|
||||
|
||||
// IARU VHF/UHF S-meter: S9 = -93 dBm, 1 S-unit = 6 dB
|
||||
@@ -1114,9 +1113,6 @@ void DisplayRSSIBar(const bool now)
|
||||
const int16_t s0_dBm = -gEeprom.S0_LEVEL; // S0 .. base level
|
||||
const int16_t rssi_dBm =
|
||||
BK4819_GetRSSI_dBm()
|
||||
#ifdef ENABLE_AM_FIX
|
||||
+ ((gSetting_AM_fix && gRxVfo->Modulation == MODULATION_AM) ? AM_fix_get_gain_diff() : 0)
|
||||
#endif
|
||||
+ dBmCorrTable[gRxVfo->Band];
|
||||
|
||||
int s0_9 = gEeprom.S0_LEVEL - gEeprom.S9_LEVEL;
|
||||
@@ -1232,13 +1228,25 @@ void UI_MAIN_PrintAGC(bool now)
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCAN_PROGRESS
|
||||
// Count the scan-list name hold down on the 10 ms tick. It used to ride the 500 ms
|
||||
// tick, but the countdown is armed at an arbitrary instant, so the first interval was
|
||||
// anywhere from ~0 to 500 ms - the name could linger up to half a second short of, or
|
||||
// over, its nominal hold. At 10 ms resolution that error is one tick at most. Gated on
|
||||
// DISPLAY_MAIN exactly as the old 500 ms path was, so it only ticks while the name can
|
||||
// actually be on screen.
|
||||
void UI_MAIN_TimeSlice10ms(void)
|
||||
{
|
||||
if (gScreenToDisplay == DISPLAY_MAIN
|
||||
&& gScanListNameCountdown_10ms > 0
|
||||
&& --gScanListNameCountdown_10ms == 0)
|
||||
gUpdateDisplay = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
void UI_MAIN_TimeSlice500ms(void)
|
||||
{
|
||||
if(gScreenToDisplay==DISPLAY_MAIN) {
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCAN_PROGRESS
|
||||
if (gScanListNameCountdown_500ms > 0 && --gScanListNameCountdown_500ms == 0)
|
||||
gUpdateDisplay = true;
|
||||
#endif
|
||||
#ifdef ENABLE_AGC_SHOW_DATA
|
||||
UI_MAIN_PrintAGC(true);
|
||||
return;
|
||||
@@ -1376,6 +1384,8 @@ void UI_DisplayMain(void)
|
||||
UI_PrintActionPickerLabel(previous, 1, false);
|
||||
UI_PrintActionPickerLabel(selection, 2, true);
|
||||
UI_PrintActionPickerLabel(next, 4, false);
|
||||
if (!ACTION_IsAvailable(gSubMenu_SIDEFUNCTIONS[selection].id))
|
||||
UI_PrintStringSmallNormalInverse("N/A", 53, 0, 6);
|
||||
ST7565_BlitFullScreen();
|
||||
return;
|
||||
}
|
||||
@@ -1566,11 +1576,6 @@ void UI_DisplayMain(void)
|
||||
if (gCurrentFunction == FUNCTION_TRANSMIT)
|
||||
{ // transmitting
|
||||
|
||||
#ifdef ENABLE_ALARM
|
||||
if (gAlarmState == ALARM_STATE_SITE_ALARM)
|
||||
mode = VFO_MODE_RX;
|
||||
else
|
||||
#endif
|
||||
{
|
||||
if (activeTxVFO == vfo_num)
|
||||
{ // show the TX symbol
|
||||
@@ -1732,12 +1737,6 @@ void UI_DisplayMain(void)
|
||||
|
||||
enum VfoState_t state = VfoState[vfo_num];
|
||||
|
||||
#ifdef ENABLE_ALARM
|
||||
if (gCurrentFunction == FUNCTION_TRANSMIT && gAlarmState == ALARM_STATE_SITE_ALARM) {
|
||||
if (activeTxVFO == vfo_num)
|
||||
state = VFO_STATE_ALARM;
|
||||
}
|
||||
#endif
|
||||
if (state != VFO_STATE_NORMAL)
|
||||
{
|
||||
if (state < ARRAY_SIZE(VfoStateStr))
|
||||
@@ -2105,15 +2104,12 @@ void UI_DisplayMain(void)
|
||||
}
|
||||
|
||||
GUI_DisplaySmallest(String, 68 + shift, line == 0 ? 17 : 49, false, true);
|
||||
|
||||
//sprintf(String, "%d.%02u", vfoInfo->StepFrequency / 100, vfoInfo->StepFrequency % 100);
|
||||
//GUI_DisplaySmallest(String, 91, line == 0 ? 2 : 34, false, true);
|
||||
}
|
||||
#else
|
||||
UI_PrintStringSmallNormal(s, LCD_WIDTH + 24, 0, line + 1);
|
||||
#endif
|
||||
|
||||
if (state == VFO_STATE_NORMAL || state == VFO_STATE_ALARM)
|
||||
if (state == VFO_STATE_NORMAL)
|
||||
{ // show the TX power
|
||||
uint8_t currentPower = vfoInfo->OUTPUT_POWER % 8;
|
||||
uint8_t arrowPos = 19;
|
||||
@@ -2142,8 +2138,6 @@ void UI_DisplayMain(void)
|
||||
else
|
||||
{
|
||||
const char pwr_long[][5] = {"LOW1", "LOW2", "LOW3", "LOW4", "LOW5", "MID", "HIGH"};
|
||||
//sprintf(String, "%s", pwr_long[currentPower]);
|
||||
//GUI_DisplaySmallest(String, 24, line == 0 ? 17 : 49, false, true);
|
||||
GUI_DisplaySmallest(pwr_long[currentPower], 24, line == 0 ? 17 : 49, false, true);
|
||||
}
|
||||
|
||||
@@ -2181,9 +2175,7 @@ void UI_DisplayMain(void)
|
||||
{
|
||||
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
|
||||
if(i == 3)
|
||||
{
|
||||
GUI_DisplaySmallest(dir_list[i], 43, line == 0 ? 17 : 49, false, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
#endif
|
||||
@@ -2358,23 +2350,6 @@ void UI_DisplayMain(void)
|
||||
else
|
||||
#endif
|
||||
|
||||
#if defined(ENABLE_AM_FIX) && defined(ENABLE_AM_FIX_SHOW_DATA)
|
||||
if (rx && gEeprom.VfoInfo[gEeprom.RX_VFO].Modulation == MODULATION_AM && gSetting_AM_fix)
|
||||
{
|
||||
if (gScreenToDisplay != DISPLAY_MAIN
|
||||
#ifdef ENABLE_DTMF_CALLING
|
||||
|| gDTMF_CallState != DTMF_CALL_STATE_NONE
|
||||
#endif
|
||||
)
|
||||
return;
|
||||
|
||||
center_line = CENTER_LINE_AM_FIX_DATA;
|
||||
AM_fix_print_data(gEeprom.RX_VFO, String);
|
||||
UI_PrintStringSmallNormal(String, 2, 0, 3);
|
||||
}
|
||||
else
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_RSSI_BAR
|
||||
if (rx) {
|
||||
center_line = CENTER_LINE_RSSI;
|
||||
|
||||
+5
-1
@@ -27,7 +27,6 @@ enum center_line_t {
|
||||
CENTER_LINE_AUDIO_BAR,
|
||||
CENTER_LINE_AUDIO_SCOPE,
|
||||
CENTER_LINE_RSSI,
|
||||
CENTER_LINE_AM_FIX_DATA,
|
||||
CENTER_LINE_DTMF_DEC,
|
||||
CENTER_LINE_CHARGE_DATA,
|
||||
#ifdef ENABLE_FEAT_F4HWN_BEAM
|
||||
@@ -48,6 +47,9 @@ extern center_line_t center_line;
|
||||
#ifdef ENABLE_AUDIO_BAR
|
||||
void UI_DisplayAudioBar(void);
|
||||
#endif
|
||||
#if defined(ENABLE_FEAT_F4HWN_AUDIO_SCOPE) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
|
||||
void UI_DisplayAudioScopeOverlay(uint8_t line, bool active);
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_AUDIO_SCOPE
|
||||
void UI_DisplayAudioScope(void);
|
||||
#endif
|
||||
@@ -58,10 +60,12 @@ void UI_DisplayMain(void);
|
||||
void UI_MAIN_NotifyScanProgressDataChanged(void);
|
||||
void UI_MAIN_NotifyScanListChanged(void);
|
||||
bool UI_MAIN_ShouldHoldScanResume(void);
|
||||
void UI_MAIN_TimeSlice10ms(void);
|
||||
#else
|
||||
static inline void UI_MAIN_NotifyScanProgressDataChanged(void) {}
|
||||
static inline void UI_MAIN_NotifyScanListChanged(void) {}
|
||||
static inline bool UI_MAIN_ShouldHoldScanResume(void) { return false; }
|
||||
static inline void UI_MAIN_TimeSlice10ms(void) {}
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_AGC_SHOW_DATA
|
||||
|
||||
+97
-60
@@ -14,9 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "../app/action.h"
|
||||
#include "../app/dtmf.h"
|
||||
#include "../app/menu.h"
|
||||
#include "../bitmaps.h"
|
||||
@@ -42,6 +44,10 @@
|
||||
#include "menu.h"
|
||||
#include "ui.h"
|
||||
#include "welcome.h"
|
||||
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
|
||||
#include "driver/mb_flash.h"
|
||||
#include "multiboot.h"
|
||||
#endif
|
||||
|
||||
|
||||
const t_menu_item MenuList[] =
|
||||
@@ -106,9 +112,6 @@ const t_menu_item MenuList[] =
|
||||
{"STE", MENU_STE },
|
||||
{"RP STE", MENU_RP_STE },
|
||||
{"1 Call", MENU_1_CALL },
|
||||
#ifdef ENABLE_ALARM
|
||||
{"AlarmT", MENU_AL_MOD },
|
||||
#endif
|
||||
#ifdef ENABLE_DTMF_CALLING
|
||||
{"ANI ID", MENU_ANI_ID },
|
||||
#endif
|
||||
@@ -126,11 +129,6 @@ const t_menu_item MenuList[] =
|
||||
{"D List", MENU_D_LIST },
|
||||
#endif
|
||||
{"D Live", MENU_D_LIVE_DEC }, // live DTMF decoder
|
||||
#ifndef ENABLE_FEAT_F4HWN
|
||||
#ifdef ENABLE_AM_FIX
|
||||
{"AM Fix", MENU_AM_FIX },
|
||||
#endif
|
||||
#endif
|
||||
{"VOX", MENU_VOX },
|
||||
#ifdef ENABLE_FEAT_F4HWN
|
||||
{"SysInf", MENU_VOL }, // was "VOL"
|
||||
@@ -174,6 +172,9 @@ const t_menu_item MenuList[] =
|
||||
#ifdef ENABLE_FEAT_F4HWN_LOGO_SAV
|
||||
{"SetSav", MENU_SET_SAV },
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
|
||||
{"SetCfg", MENU_SET_CFG }, // load another settings bank (reboots)
|
||||
#endif
|
||||
#endif
|
||||
// hidden menu items from here on
|
||||
// enabled if pressing both the PTT and upper side button at power-on
|
||||
@@ -258,14 +259,6 @@ const char* const gSubMenu_MDF[] =
|
||||
"NAME\n+\nFREQ"
|
||||
};
|
||||
|
||||
#ifdef ENABLE_ALARM
|
||||
const char* const gSubMenu_AL_MOD[] =
|
||||
{
|
||||
"SITE",
|
||||
"TONE"
|
||||
};
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_DTMF_CALLING
|
||||
const char* const gSubMenu_D_RSP[] =
|
||||
{
|
||||
@@ -481,57 +474,33 @@ const char* const gSubMenu_SCRAMBLER[] =
|
||||
const t_sidefunction gSubMenu_SIDEFUNCTIONS[] =
|
||||
{
|
||||
{"NONE", ACTION_OPT_NONE},
|
||||
#ifdef ENABLE_FLASHLIGHT
|
||||
{"FLASH\nLIGHT", ACTION_OPT_FLASHLIGHT},
|
||||
#endif
|
||||
{"POWER", ACTION_OPT_POWER},
|
||||
{"MONITOR", ACTION_OPT_MONITOR},
|
||||
{"SCAN", ACTION_OPT_SCAN},
|
||||
#ifdef ENABLE_VOX
|
||||
{"VOX", ACTION_OPT_VOX},
|
||||
#endif
|
||||
#ifdef ENABLE_ALARM
|
||||
{"ALARM", ACTION_OPT_ALARM},
|
||||
#endif
|
||||
#ifdef ENABLE_FMRADIO
|
||||
{"FM RADIO", ACTION_OPT_FM},
|
||||
#endif
|
||||
#ifdef ENABLE_TX1750
|
||||
{"1750Hz", ACTION_OPT_1750},
|
||||
#endif
|
||||
{"LOCK\nKEYPAD", ACTION_OPT_KEYLOCK},
|
||||
{"VFO A\nVFO B", ACTION_OPT_A_B},
|
||||
{"VFO\nMEM", ACTION_OPT_VFO_MR},
|
||||
{"MODE", ACTION_OPT_SWITCH_DEMODUL},
|
||||
#ifdef ENABLE_BLMIN_TMP_OFF
|
||||
{"BLMIN\nTMP OFF", ACTION_OPT_BLMIN_TMP_OFF}, //BackLight Minimum Temporary OFF
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN
|
||||
{"RX MODE", ACTION_OPT_RXMODE},
|
||||
{"MAIN ONLY", ACTION_OPT_MAINONLY},
|
||||
{"PTT", ACTION_OPT_PTT},
|
||||
{"WIDE\nNARROW", ACTION_OPT_WN},
|
||||
{"MUTE", ACTION_OPT_MUTE},
|
||||
#ifdef ENABLE_FEAT_F4HWN_AUDIO
|
||||
{"RxA", ACTION_OPT_RXA},
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
|
||||
{"POWER\nHIGH", ACTION_OPT_POWER_HIGH},
|
||||
{"REMOVE\nOFFSET", ACTION_OPT_REMOVE_OFFSET},
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_BEAM
|
||||
{"BEAM", ACTION_OPT_BEAM},
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
|
||||
{"RF LOG", ACTION_OPT_RXTX_LOG},
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_FOXHUNT
|
||||
{"FOX HUNT\nBEACON", ACTION_OPT_FOXHUNT},
|
||||
#endif
|
||||
#endif
|
||||
{"RxA", ACTION_OPT_RXA},
|
||||
{"RF LOG", ACTION_OPT_RXTX_LOG},
|
||||
{"BEAM", ACTION_OPT_BEAM},
|
||||
{"POWER\nHIGH", ACTION_OPT_POWER_HIGH},
|
||||
{"REMOVE\nOFFSET", ACTION_OPT_REMOVE_OFFSET},
|
||||
{"FOX HUNT", ACTION_OPT_FOXHUNT},
|
||||
{"BEACON", ACTION_OPT_BEACON},
|
||||
};
|
||||
|
||||
const uint8_t gSubMenu_SIDEFUNCTIONS_size = ARRAY_SIZE(gSubMenu_SIDEFUNCTIONS);
|
||||
static_assert(ARRAY_SIZE(gSubMenu_SIDEFUNCTIONS) == ACTION_OPT_LEN);
|
||||
|
||||
bool gIsInSubMenu;
|
||||
uint8_t gMenuCursor;
|
||||
@@ -598,6 +567,9 @@ static const uint8_t CatChannels[] = {
|
||||
#endif
|
||||
MENU_BCL, MENU_COMPAND, MENU_AM, MENU_TX_LOCK, MENU_PTT_ID, MENU_LIST_CH,
|
||||
MENU_MEM_CH, MENU_DEL_CH, MENU_MEM_NAME,
|
||||
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
|
||||
MENU_SET_CFG,
|
||||
#endif
|
||||
};
|
||||
static const uint8_t CatScan[] = {
|
||||
MENU_S_LIST, MENU_S_PRI, MENU_S_PRI_CH_1, MENU_S_PRI_CH_2, MENU_SC_REV,
|
||||
@@ -836,6 +808,28 @@ static void UI_MENU_DrawTopRightRoundedBadge(const char *text, const uint8_t lin
|
||||
UI_PrintStringSmallNormalInverse(text, text_x, 0, line);
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
|
||||
/* Draw `text` (3x5 font) centred inside a fixed-width rounded inverse capsule:
|
||||
* left edge `cap_left`, inclusive width `cap_w`, on framebuffer page `line`. Same
|
||||
* capsule pattern as GUI_DisplaySmallestInverse (0x3E rounded ends, 0x7F body) but
|
||||
* with the width decoupled from the text length, so two labels of different
|
||||
* lengths (e.g. "SLOT 2" / "CFG 4") share one width and each stays centred. */
|
||||
static void UI_MENU_DrawFixedCapsule(const char *text, uint8_t cap_left,
|
||||
uint8_t cap_w, uint8_t line)
|
||||
{
|
||||
const uint8_t cap_right = (uint8_t)(cap_left + cap_w - 1u);
|
||||
const uint8_t text_w = (uint8_t)(strlen(text) * 4u - 1u); /* 3x5 glyphs: 4 px/char, last one 3 px wide */
|
||||
const uint8_t tx = (uint8_t)(cap_left + (cap_w - text_w) / 2u);
|
||||
|
||||
GUI_DisplaySmallest(text, tx, (uint8_t)(line * 8u + 1u), false, true);
|
||||
|
||||
gFrameBuffer[line][cap_left] ^= 0x3Eu;
|
||||
for (uint8_t x = (uint8_t)(cap_left + 1u); x < cap_right; x++)
|
||||
gFrameBuffer[line][x] ^= 0x7Fu;
|
||||
gFrameBuffer[line][cap_right] ^= 0x3Eu;
|
||||
}
|
||||
#endif
|
||||
|
||||
void UI_DisplayMenu(void)
|
||||
{
|
||||
const unsigned int menu_list_width = 6; // max no. of characters on the menu list (left side)
|
||||
@@ -844,6 +838,7 @@ void UI_DisplayMenu(void)
|
||||
unsigned int i;
|
||||
char String[64]; // bigger cuz we can now do multi-line in one string (use '\n' char)
|
||||
char top_right_badge[16];
|
||||
uint8_t top_right_badge_line = 1;
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_MENU_CAT
|
||||
if (gMenuLevel == MENU_LEVEL_CAT)
|
||||
@@ -1132,11 +1127,6 @@ void UI_DisplayMenu(void)
|
||||
strcpy(String, gSubMenu_RX_TX[gSubMenuSelection]);
|
||||
break;
|
||||
|
||||
#ifndef ENABLE_FEAT_F4HWN
|
||||
#ifdef ENABLE_AM_FIX
|
||||
case MENU_AM_FIX:
|
||||
#endif
|
||||
#endif
|
||||
case MENU_BCL:
|
||||
case MENU_BEEP:
|
||||
case MENU_STE:
|
||||
@@ -1338,12 +1328,6 @@ void UI_DisplayMenu(void)
|
||||
}
|
||||
break;
|
||||
|
||||
#ifdef ENABLE_ALARM
|
||||
case MENU_AL_MOD:
|
||||
sprintf(String, gSubMenu_AL_MOD[gSubMenuSelection]);
|
||||
break;
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_DTMF_CALLING
|
||||
case MENU_ANI_ID:
|
||||
strcpy(String, gEeprom.ANI_DTMF_ID);
|
||||
@@ -1421,6 +1405,41 @@ void UI_DisplayMenu(void)
|
||||
#ifdef ENABLE_FEAT_F4HWN
|
||||
sprintf(String, "%s\n%s", AUTHOR_STRING_2, DISPLAY_VERSION_STRING_2);
|
||||
UI_PrintStringSmallNormal(Edition, menu_item_x1 - 1, menu_item_x2, 6);
|
||||
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
|
||||
/* Two 3x5 inverse-capsule labels on one line (scan-list "label"
|
||||
* style): the running firmware slot (M = Main) and the active
|
||||
* config bank. They match unless SetCfg has pointed the bank at a
|
||||
* different bank (e.g. SLOT 2 / CFG 4). */
|
||||
const uint8_t fw_slot = MB_GetRunningSlot();
|
||||
const uint8_t bank = MB_GetActiveBank();
|
||||
char slot_lbl[8];
|
||||
char cfg_lbl[8];
|
||||
|
||||
/* Only the last glyph varies (M / digit / ?), so poke it in place
|
||||
* instead of pulling sprintf for a single character. */
|
||||
strcpy(slot_lbl, "SLOT ?");
|
||||
if (fw_slot == 0u)
|
||||
slot_lbl[5] = 'M';
|
||||
else if (fw_slot < MB_SLOT_COUNT)
|
||||
slot_lbl[5] = (char)('0' + fw_slot);
|
||||
strcpy(cfg_lbl, "CFG M"); /* bank 0 = base config, like SLOT M */
|
||||
if (bank != 0u)
|
||||
cfg_lbl[4] = (char)('0' + bank);
|
||||
|
||||
/* Both capsules share the wider label's width (6-char "SLOT x" ->
|
||||
* 4*6+3 = 27 px); the shorter CFG text is centred inside its own.
|
||||
* The two are drawn as one centred pair with a small gap, centred in
|
||||
* the space between the separator bar (x=48) and the right screen
|
||||
* edge, so they line up with the centred identity lines above. */
|
||||
const uint8_t cap_w = (uint8_t)(4u * 6u + 3u); /* 27 */
|
||||
const uint8_t cap_gap = 4u;
|
||||
const uint8_t pair_w = (uint8_t)(2u * cap_w + cap_gap); /* 58 */
|
||||
const uint8_t slot_left = (uint8_t)((48u + LCD_WIDTH - pair_w) / 2u); /* 59 */
|
||||
const uint8_t cfg_left = (uint8_t)(slot_left + cap_w + cap_gap); /* 90 */
|
||||
|
||||
UI_MENU_DrawFixedCapsule(slot_lbl, slot_left, cap_w, 5);
|
||||
UI_MENU_DrawFixedCapsule(cfg_lbl, cfg_left, cap_w, 5);
|
||||
#endif
|
||||
#else
|
||||
sprintf(String, "%u.%02uV\n%u%%",
|
||||
gBatteryVoltageAverage / 100, gBatteryVoltageAverage % 100,
|
||||
@@ -1542,13 +1561,28 @@ void UI_DisplayMenu(void)
|
||||
strcpy(String, gSubMenu_SET_NAV[gSubMenuSelection]);
|
||||
break;
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
|
||||
case MENU_SET_CFG:
|
||||
strcpy(String, "CFG M"); /* bank 0 = base config, like SysInfo */
|
||||
if (gSubMenuSelection != 0)
|
||||
String[4] = (char)('0' + gSubMenuSelection);
|
||||
break;
|
||||
#endif
|
||||
|
||||
case MENU_F1SHRT:
|
||||
case MENU_F1LONG:
|
||||
case MENU_F2SHRT:
|
||||
case MENU_F2LONG:
|
||||
case MENU_MLONG:
|
||||
{
|
||||
const uint8_t action = gSubMenu_SIDEFUNCTIONS[gSubMenuSelection].id;
|
||||
strcpy(String, gSubMenu_SIDEFUNCTIONS[gSubMenuSelection].name);
|
||||
if (!ACTION_IsAvailable(action)) {
|
||||
strcpy(top_right_badge, "N/A");
|
||||
top_right_badge_line = 5;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_SLEEP
|
||||
case MENU_SET_OFF:
|
||||
@@ -1791,12 +1825,15 @@ void UI_DisplayMenu(void)
|
||||
#endif
|
||||
|
||||
if (top_right_badge[0] != '\0') {
|
||||
UI_MENU_DrawTopRightRoundedBadge(top_right_badge, 1, true, menu_item_x1, menu_item_x2);
|
||||
UI_MENU_DrawTopRightRoundedBadge(top_right_badge, top_right_badge_line, true, menu_item_x1, menu_item_x2);
|
||||
}
|
||||
|
||||
if ((m == MENU_RESET ||
|
||||
m == MENU_MEM_CH ||
|
||||
m == MENU_MEM_NAME ||
|
||||
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
|
||||
m == MENU_SET_CFG ||
|
||||
#endif
|
||||
m == MENU_DEL_CH) && gAskForConfirmation)
|
||||
{ // display confirmation
|
||||
char *pPrintStr = (gAskForConfirmation == 1) ? "SURE?" : "WAIT!";
|
||||
|
||||
+3
-9
@@ -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
|
||||
|
||||
@@ -0,0 +1,576 @@
|
||||
/* Copyright 2026 Armel F4HWN
|
||||
* https://github.com/armel
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "driver/backlight.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "driver/keyboard.h"
|
||||
#include "driver/mb_flash.h"
|
||||
#include "driver/st7565.h"
|
||||
#include "driver/system.h"
|
||||
#include "ui/helper.h"
|
||||
#include "ui/multiboot.h"
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
#include "k5viewer.h"
|
||||
#endif
|
||||
|
||||
static uint8_t gRunningSlot = 0xFFu;
|
||||
static uint8_t gActiveBank = 0u;
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
static void mb_k5viewer_service(void)
|
||||
{
|
||||
/* The Multiboot selector is modal and does not return to APP_Update(). */
|
||||
K5VIEWER_ParseInput();
|
||||
K5VIEWER_Update(false);
|
||||
}
|
||||
#endif
|
||||
|
||||
static uint8_t mb_remember_boot_state(uint8_t slot, uint8_t bank)
|
||||
{
|
||||
gRunningSlot = slot;
|
||||
gActiveBank = bank;
|
||||
return bank;
|
||||
}
|
||||
|
||||
uint8_t MB_GetRunningSlot(void)
|
||||
{
|
||||
return gRunningSlot;
|
||||
}
|
||||
|
||||
uint8_t MB_GetActiveBank(void)
|
||||
{
|
||||
return gActiveBank;
|
||||
}
|
||||
|
||||
static const char *mb_error_text(uint8_t err)
|
||||
{
|
||||
switch (err)
|
||||
{
|
||||
case MB_OK: return "OK";
|
||||
case MB_ERR_MAGIC: return "empty";
|
||||
case MB_ERR_VERSION: return "new header";
|
||||
case MB_ERR_NOT_COMMITTED: return "incomplete";
|
||||
case MB_ERR_SIZE: return "bad size";
|
||||
case MB_ERR_CRC: return "CRC ERROR";
|
||||
case MB_ERR_SPI: return "SPI ERROR";
|
||||
case MB_ERR_SLOT: return "bad index";
|
||||
case MB_ERR_AUTH: return "auth";
|
||||
case MB_ERR_RAM_LOAD: return "RAM LOAD ERROR";
|
||||
default: return "error";
|
||||
}
|
||||
}
|
||||
|
||||
static void mb_copy_label(char *dst, uint8_t cap, const char *src, uint8_t src_cap)
|
||||
{
|
||||
uint8_t n = 0;
|
||||
while (n + 1u < cap && n < src_cap && src[n])
|
||||
{
|
||||
dst[n] = src[n];
|
||||
n++;
|
||||
}
|
||||
dst[n] = 0;
|
||||
}
|
||||
|
||||
static uint8_t mb_copy_slot_version(char *dst, uint8_t cap, const mb_slot_header_t *header)
|
||||
{
|
||||
uint8_t n = 0;
|
||||
|
||||
dst[0] = 0;
|
||||
for (uint8_t i = 0; i + 1u < MB_VERSION_LEN && header->fw_version[i]; i++)
|
||||
{
|
||||
if (header->fw_version[i] == 'v' &&
|
||||
header->fw_version[i + 1u] >= '0' &&
|
||||
header->fw_version[i + 1u] <= '9')
|
||||
{
|
||||
if (n + 1u < cap)
|
||||
dst[n++] = 'v';
|
||||
i++;
|
||||
while (n + 1u < cap && i < MB_VERSION_LEN)
|
||||
{
|
||||
const char c = header->fw_version[i];
|
||||
if ((c < '0' || c > '9') && c != '.')
|
||||
break;
|
||||
dst[n++] = c;
|
||||
i++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
dst[n] = 0;
|
||||
return n;
|
||||
}
|
||||
|
||||
/* "F4HWN MULTIBOOT" banner in the top status bar, shown on every screen - the
|
||||
* same way the firmware puts mode labels there (inverse 3x5 capsule). */
|
||||
static void mb_status_bar(void)
|
||||
{
|
||||
UI_StatusClear();
|
||||
GUI_DisplaySmallestInverse("F4HWN MULTIBOOT", 34, 0, true, true, 94);
|
||||
|
||||
/* Thin line dressing up the otherwise blank row between the status bar and
|
||||
* the first content row. Drawn on gFrameBuffer[0] (line 0), which every
|
||||
* multiboot screen leaves blank, so it shows on all of them. Bit 3 (~mid of
|
||||
* the row) stays clear of the selected-slot capsule's top edge (bit 7). */
|
||||
for (uint8_t x = 2u; x < LCD_WIDTH - 2u; x++)
|
||||
gFrameBuffer[0][x] |= 0x08u;
|
||||
}
|
||||
|
||||
/* Bottom key-hint line: each key name as an inverse 3x5 capsule label, its
|
||||
* action in plain 3x5 text beside it. Drawn on the bottom line
|
||||
* (gFrameBuffer[6] -> y = 6*8+1 = 49). MENU is pinned to the left and EXIT to
|
||||
* the right, leaving an airy gap in the middle. "MENU"/"EXIT" are 4 chars
|
||||
* (16 px); their capsule spans [x-2 .. x+16]. */
|
||||
static void mb_key_hints(const char *act_menu, const char *act_exit)
|
||||
{
|
||||
const uint8_t sp = 6u; /* label <-> action gap */
|
||||
const uint8_t ae = (uint8_t)strlen(act_exit);
|
||||
const uint8_t xm = 4u; /* MENU text; capsule at x=2 */
|
||||
const uint8_t xe = (uint8_t)(124u - ae * 4u - sp - 16u); /* EXIT action ends at x=124 */
|
||||
|
||||
GUI_DisplaySmallestInverse("MENU", xm, 6, false, true, (uint8_t)(xm + 16u));
|
||||
GUI_DisplaySmallest(act_menu, (uint8_t)(xm + 16u + sp), 49, false, true);
|
||||
|
||||
GUI_DisplaySmallestInverse("EXIT", xe, 6, false, true, (uint8_t)(xe + 16u));
|
||||
GUI_DisplaySmallest(act_exit, (uint8_t)(xe + 16u + sp), 49, false, true);
|
||||
}
|
||||
|
||||
/* Fixed selection capsule around the firmware name. The slot index stays in
|
||||
* the normal font while the version is plain 3x5 metadata. */
|
||||
#define MB_NAME_BOX_START 12u
|
||||
#define MB_NAME_BOX_END 96u
|
||||
#define MB_NAME_TEXT_X 14u
|
||||
|
||||
static void mb_invert_name(uint8_t line)
|
||||
{
|
||||
gFrameBuffer[line][MB_NAME_BOX_START] ^= 0x7Fu;
|
||||
for (uint8_t x = MB_NAME_BOX_START + 1u; x < MB_NAME_BOX_END; x++)
|
||||
{
|
||||
gFrameBuffer[line][x] ^= 0xFFu;
|
||||
gFrameBuffer[line - 1u][x] ^= 0x80u;
|
||||
}
|
||||
gFrameBuffer[line][MB_NAME_BOX_END] ^= 0x7Fu;
|
||||
}
|
||||
|
||||
static void mb_show_message(const char *line1, const char *line2, const char *line3)
|
||||
{
|
||||
UI_DisplayClear();
|
||||
mb_status_bar();
|
||||
if (line1) UI_PrintStringSmallNormal(line1, 2, 126, 2);
|
||||
if (line2) UI_PrintStringSmallNormal(line2, 2, 126, 4);
|
||||
if (line3) UI_PrintStringSmallNormal(line3, 2, 126, 6);
|
||||
ST7565_BlitStatusLine();
|
||||
ST7565_BlitFullScreen();
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
mb_k5viewer_service();
|
||||
#endif
|
||||
}
|
||||
|
||||
static void mb_wait_release(void)
|
||||
{
|
||||
uint8_t stable = 0;
|
||||
while (stable < 10u)
|
||||
{
|
||||
if (!GPIO_IsPttPressed() && KEYBOARD_Poll() == KEY_INVALID)
|
||||
stable++;
|
||||
else
|
||||
stable = 0;
|
||||
SYSTEM_DelayMs(10);
|
||||
}
|
||||
}
|
||||
|
||||
static KEY_Code_t mb_get_key(void)
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
mb_k5viewer_service();
|
||||
#endif
|
||||
KEY_Code_t key = KEYBOARD_Poll();
|
||||
if (key != KEY_INVALID)
|
||||
{
|
||||
SYSTEM_DelayMs(30);
|
||||
if (KEYBOARD_Poll() == key)
|
||||
{
|
||||
while (KEYBOARD_Poll() != KEY_INVALID)
|
||||
SYSTEM_DelayMs(10);
|
||||
return key;
|
||||
}
|
||||
}
|
||||
SYSTEM_DelayMs(10);
|
||||
}
|
||||
}
|
||||
|
||||
/* Shown from the normal settings menu (SetCfg), not the boot selector, so it does
|
||||
* NOT paint the "F4HWN MULTIBOOT" status banner - just a plain acknowledged message. */
|
||||
void UI_MultibootShowConfigError(uint8_t err)
|
||||
{
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
gKeyReading0 = KEY_INVALID;
|
||||
gKeyReading1 = KEY_INVALID;
|
||||
#endif
|
||||
UI_DisplayClear();
|
||||
UI_StatusClear();
|
||||
UI_PrintStringSmallNormal("CFG ERROR", 2, 126, 2);
|
||||
UI_PrintStringSmallNormal(mb_error_text(err), 2, 126, 4);
|
||||
UI_PrintStringSmallNormal("Press any key", 2, 126, 6);
|
||||
ST7565_BlitStatusLine();
|
||||
ST7565_BlitFullScreen();
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
mb_k5viewer_service();
|
||||
#endif
|
||||
/* The MENU press that confirmed SetCfg may still be down; wait for a clean
|
||||
* release first so it isn't consumed as the acknowledgement immediately. */
|
||||
mb_wait_release();
|
||||
(void)mb_get_key();
|
||||
}
|
||||
|
||||
static void mb_scan_slots(mb_slot_header_t headers[MB_SLOT_COUNT], uint8_t status[MB_SLOT_COUNT])
|
||||
{
|
||||
mb_show_message("Scanning slots...", NULL, "Please wait");
|
||||
for (uint8_t slot = 0; slot < MB_SLOT_COUNT; slot++)
|
||||
status[slot] = MB_ValidateSlot(slot, &headers[slot], NULL);
|
||||
}
|
||||
|
||||
static void mb_render_slots(uint8_t selected,
|
||||
const mb_slot_header_t headers[MB_SLOT_COUNT],
|
||||
const uint8_t status[MB_SLOT_COUNT])
|
||||
{
|
||||
char name[13];
|
||||
char version[8]; /* v + up to six version digits/dots in the 3x5 column. */
|
||||
|
||||
UI_DisplayClear();
|
||||
mb_status_bar();
|
||||
|
||||
for (uint8_t slot = 0; slot < MB_SLOT_COUNT; slot++)
|
||||
{
|
||||
const uint8_t fbLine = (uint8_t)(slot + 1u); /* page 1 stays blank */
|
||||
char index[2];
|
||||
uint8_t version_len = 0;
|
||||
uint8_t version_x = 0;
|
||||
|
||||
memset(name, 0, sizeof(name));
|
||||
memset(version, 0, sizeof(version));
|
||||
/* Slot 0 is the auto-backed-up main firmware: label it 'M' (Main) so it
|
||||
* reads apart from the numbered user slots 1..4. */
|
||||
index[0] = (slot == 0u) ? 'M' : (char)('0' + slot);
|
||||
index[1] = '\0';
|
||||
|
||||
if (status[slot] == MB_OK)
|
||||
{
|
||||
version_len = mb_copy_slot_version(version, sizeof(version), &headers[slot]);
|
||||
if (version_len)
|
||||
version_x = (uint8_t)(LCD_WIDTH - 2u - version_len * 4u);
|
||||
|
||||
if (headers[slot].name[0])
|
||||
mb_copy_label(name, sizeof(name), headers[slot].name, MB_NAME_LEN);
|
||||
else if (!version_len)
|
||||
mb_copy_label(name, sizeof(name), headers[slot].fw_version, MB_VERSION_LEN);
|
||||
}
|
||||
else
|
||||
mb_copy_label(name, sizeof(name), mb_error_text(status[slot]), 20u);
|
||||
|
||||
UI_PrintStringSmallNormal(index, 2u, 0, fbLine);
|
||||
UI_PrintStringSmallNormal(name, MB_NAME_TEXT_X, 0, fbLine);
|
||||
if (version_len)
|
||||
GUI_DisplaySmallest(version, version_x,
|
||||
(uint8_t)(fbLine * 8u + 1u), false, true);
|
||||
|
||||
/* Selected row: fixed rounded inverse capsule around the name only. */
|
||||
if (slot == selected)
|
||||
mb_invert_name(fbLine);
|
||||
}
|
||||
|
||||
mb_key_hints("SELECT", "QUIT");
|
||||
|
||||
ST7565_BlitStatusLine();
|
||||
ST7565_BlitFullScreen();
|
||||
}
|
||||
|
||||
static void mb_draw_progress_outline(void)
|
||||
{
|
||||
/* Same rounded outline and hatch pattern as the scan progress gauge. */
|
||||
gFrameBuffer[6][3] = 0x0Cu;
|
||||
gFrameBuffer[6][4] = 0x12u;
|
||||
gFrameBuffer[6][123] = 0x12u;
|
||||
gFrameBuffer[6][124] = 0x0Cu;
|
||||
for (uint8_t x = 5; x < 123u; x++)
|
||||
gFrameBuffer[6][x] = 0x21u;
|
||||
}
|
||||
|
||||
/* Full progress frame used while restoring a firmware slot. */
|
||||
__attribute__((noinline)) static void mb_prepare_progress_screen(const char *title,
|
||||
const char *detail)
|
||||
{
|
||||
UI_DisplayClear();
|
||||
mb_status_bar();
|
||||
UI_PrintStringSmallNormal(title, 2, 126, 1);
|
||||
UI_PrintStringSmallNormal("DO NOT POWER OFF", 2, 126, 3);
|
||||
UI_PrintStringSmallNormal(detail, 2, 126, 5);
|
||||
/* Empty gauge that the RAM copier fills as it reflashes. */
|
||||
mb_draw_progress_outline();
|
||||
ST7565_BlitStatusLine();
|
||||
ST7565_BlitFullScreen();
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
mb_k5viewer_service();
|
||||
#endif
|
||||
}
|
||||
|
||||
static void mb_prepare_progress(uint8_t slot)
|
||||
{
|
||||
char slot_title[] = "Restore slot 0";
|
||||
slot_title[13] = (char)('0' + slot);
|
||||
const char *title = (slot == 0u) ? "Restore Main" : slot_title;
|
||||
|
||||
mb_prepare_progress_screen(title, "Writing / Verify");
|
||||
}
|
||||
|
||||
/* Discreet "Main backup" screen shown once, at the first boot after a normal
|
||||
* Flash-Firmware install, while the running firmware is copied into slot 0. */
|
||||
static void mb_backup_prepare(void)
|
||||
{
|
||||
UI_DisplayClear();
|
||||
UI_StatusClear();
|
||||
UI_PrintStringSmallNormal("Init Main", 2, 126, 1);
|
||||
UI_PrintStringSmallNormal("DO NOT POWER OFF", 2, 126, 3);
|
||||
mb_draw_progress_outline();
|
||||
ST7565_BlitStatusLine();
|
||||
ST7565_BlitFullScreen();
|
||||
}
|
||||
|
||||
static void mb_backup_progress(uint32_t done, uint32_t total)
|
||||
{
|
||||
uint32_t cols = total ? (done * 118u / total) : 118u;
|
||||
if (cols > 118u)
|
||||
cols = 118u;
|
||||
for (uint32_t i = 0; i < cols; i++)
|
||||
gFrameBuffer[6][5u + i] = 0x2Du;
|
||||
ST7565_BlitFullScreen();
|
||||
}
|
||||
|
||||
/* With no trustworthy bank, continuing would let normal boot-time settings
|
||||
* writes modify an arbitrary bank. Keep the radio in a read-only error state;
|
||||
* a power cycle can recover from a transient SPI fault. */
|
||||
__attribute__((noreturn)) static void mb_state_error_halt(void)
|
||||
{
|
||||
BACKLIGHT_TurnOn();
|
||||
mb_show_message("STATE ERROR", "Flash state unknown", "Restart radio");
|
||||
for (;;)
|
||||
SYSTEM_DelayMs(100);
|
||||
}
|
||||
|
||||
static void mb_confirm_screen(uint8_t slot)
|
||||
{
|
||||
char slot_title[] = "Restore slot 0?";
|
||||
slot_title[13] = (char)('0' + slot);
|
||||
const char *title = (slot == 0u) ? "Restore Main?" : slot_title;
|
||||
|
||||
UI_DisplayClear();
|
||||
mb_status_bar();
|
||||
UI_PrintStringSmallNormal(title, 2, 126, 3);
|
||||
mb_key_hints("CONFIRM", "BACK");
|
||||
ST7565_BlitStatusLine();
|
||||
ST7565_BlitFullScreen();
|
||||
}
|
||||
|
||||
void UI_MultibootSelector(void)
|
||||
{
|
||||
mb_slot_header_t headers[MB_SLOT_COUNT];
|
||||
uint8_t status[MB_SLOT_COUNT];
|
||||
uint8_t selected = 0;
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
/* The selector is entered from a boot key event. Clear that stale key so
|
||||
* K5Viewer is allowed to publish the first selector frame immediately. */
|
||||
gKeyReading0 = KEY_INVALID;
|
||||
gKeyReading1 = KEY_INVALID;
|
||||
#endif
|
||||
|
||||
/* Clear + blit the LCD BEFORE the backlight comes on, otherwise it reveals
|
||||
* the random power-on contents of the display RAM for a moment. */
|
||||
mb_show_message("Release keys", NULL, NULL);
|
||||
BACKLIGHT_TurnOn();
|
||||
mb_wait_release();
|
||||
mb_scan_slots(headers, status);
|
||||
|
||||
/* Pre-select the exact firmware slot resolved at boot, independently of the
|
||||
* active config bank (SetCfg can point the bank elsewhere), so the cursor
|
||||
* lands on "where you are". An unknown or now-invalid slot falls back to the
|
||||
* first valid slot below. */
|
||||
selected = MB_GetRunningSlot();
|
||||
if (selected >= MB_SLOT_COUNT || status[selected] != MB_OK)
|
||||
{
|
||||
for (uint8_t slot = 0; slot < MB_SLOT_COUNT; slot++)
|
||||
{
|
||||
if (status[slot] == MB_OK)
|
||||
{
|
||||
selected = slot;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (;;)
|
||||
{
|
||||
mb_render_slots(selected, headers, status);
|
||||
KEY_Code_t key = mb_get_key();
|
||||
|
||||
if (key == KEY_EXIT)
|
||||
{
|
||||
/* MENU was latched by BOOT_GetMode(). Do not let that stale boot
|
||||
* key reach the normal application after leaving the selector. */
|
||||
gKeyReading0 = KEY_INVALID;
|
||||
gKeyReading1 = KEY_INVALID;
|
||||
gDebounceCounter = 0;
|
||||
return;
|
||||
}
|
||||
if (key == KEY_UP)
|
||||
{
|
||||
selected = (uint8_t)((selected + MB_SLOT_COUNT - 1u) % MB_SLOT_COUNT);
|
||||
continue;
|
||||
}
|
||||
if (key == KEY_DOWN)
|
||||
{
|
||||
selected = (uint8_t)((selected + 1u) % MB_SLOT_COUNT);
|
||||
continue;
|
||||
}
|
||||
if (key != KEY_MENU)
|
||||
continue;
|
||||
|
||||
if (status[selected] != MB_OK)
|
||||
{
|
||||
mb_show_message("SLOT NOT VALID", mb_error_text(status[selected]), "Press any key");
|
||||
(void)mb_get_key();
|
||||
continue;
|
||||
}
|
||||
|
||||
mb_confirm_screen(selected);
|
||||
key = mb_get_key();
|
||||
if (key != KEY_MENU)
|
||||
continue;
|
||||
|
||||
/* Bind this slot to its own settings bank BEFORE reflashing. The
|
||||
* write is verified (read-back); if it can't be confirmed we must NOT
|
||||
* reflash - otherwise the next boot could resolve to the wrong bank
|
||||
* (e.g. when two slots hold the same firmware image). */
|
||||
if (MB_SetActiveSlot(selected) != MB_OK)
|
||||
{
|
||||
mb_show_message("STATE ERROR", "Marker not saved", "Press any key");
|
||||
(void)mb_get_key();
|
||||
continue;
|
||||
}
|
||||
|
||||
mb_prepare_progress(selected);
|
||||
uint8_t err = MB_RestoreSlot(selected, gFrameBuffer[6]);
|
||||
|
||||
/* Only reached when the final pre-erase validation refused the slot. */
|
||||
status[selected] = err;
|
||||
mb_show_message("RESTORE REFUSED", mb_error_text(err), "Press any key");
|
||||
(void)mb_get_key();
|
||||
mb_scan_slots(headers, status);
|
||||
}
|
||||
}
|
||||
|
||||
/* Adopt the running internal firmware as Main: back it up into slot 0 and point
|
||||
* the marker at bank 0. Reached when the firmware was installed outside
|
||||
* multiboot (fresh radio, or a plain Flash-Firmware). */
|
||||
static uint8_t mb_adopt_internal_as_main(void)
|
||||
{
|
||||
mb_backup_prepare();
|
||||
BACKLIGHT_TurnOn();
|
||||
if (MB_BackupInternalToSlot0(mb_backup_progress) == MB_OK)
|
||||
(void)MB_SetActiveSlot(MB_SLOT_BACKUP);
|
||||
return MB_SLOT_BACKUP;
|
||||
}
|
||||
|
||||
uint8_t MB_BootResolveState(void)
|
||||
{
|
||||
mb_state_t mark;
|
||||
mb_mark_status_t ms = MB_MARK_IO;
|
||||
|
||||
for (uint8_t retry = 0; retry < 3u && ms == MB_MARK_IO; retry++)
|
||||
{
|
||||
ms = MB_ReadActiveState(&mark);
|
||||
if (ms == MB_MARK_IO)
|
||||
SYSTEM_DelayMs(10);
|
||||
}
|
||||
|
||||
/* A reliably-read marker is authoritative: it carries the expected internal
|
||||
* identity, so we don't even need the slot header. */
|
||||
if (ms == MB_MARK_VALID)
|
||||
{
|
||||
if (MB_InternalMatchesState(&mark))
|
||||
return mb_remember_boot_state(mark.firmware_slot, mark.config_bank);
|
||||
|
||||
/* Marker read fine but internal no longer carries its identity -> the
|
||||
* firmware was replaced outside multiboot (a plain Flash-Firmware). Adopt
|
||||
* it as Main. Deliberately NOT a content scan here: a build that merely
|
||||
* duplicates a user slot (or a marker that already points at such a slot)
|
||||
* must still refresh Main. */
|
||||
return mb_remember_boot_state(mb_adopt_internal_as_main(), MB_SLOT_BACKUP);
|
||||
}
|
||||
|
||||
/* Marker unreliable (MISSING / LEGACY / CORRUPT / IO): identify the running
|
||||
* firmware by content, and never destroy Main on uncertainty - internal is
|
||||
* adopted only when it matches no slot AND every read was clean, so a
|
||||
* transient SPI error or a half-written marker can never destroy Main. */
|
||||
|
||||
/* An FMP1 record still names a coupled slot/bank; honour it before the
|
||||
* content scan so a duplicate image in a lower slot cannot hijack the
|
||||
* migration. Falls through to the scan below on mismatch or IO. */
|
||||
if (ms == MB_MARK_LEGACY && mark.firmware_slot < MB_SLOT_COUNT)
|
||||
{
|
||||
mb_fw_match_t m = MB_FW_IO;
|
||||
for (uint8_t retry = 0; retry < 3u && m == MB_FW_IO; retry++)
|
||||
m = MB_InternalMatchesSlot(mark.firmware_slot);
|
||||
if (m == MB_FW_MATCH)
|
||||
{
|
||||
(void)MB_SetActiveSlot(mark.firmware_slot);
|
||||
return mb_remember_boot_state(mark.firmware_slot, mark.config_bank);
|
||||
}
|
||||
}
|
||||
|
||||
bool had_io = false;
|
||||
for (uint8_t slot = 0; slot < MB_SLOT_COUNT; slot++)
|
||||
{
|
||||
mb_fw_match_t m = MB_FW_IO;
|
||||
for (uint8_t retry = 0; retry < 3u && m == MB_FW_IO; retry++)
|
||||
m = MB_InternalMatchesSlot(slot);
|
||||
if (m == MB_FW_MATCH)
|
||||
{
|
||||
(void)MB_SetActiveSlot(slot); /* record/repair the marker */
|
||||
return mb_remember_boot_state(slot, slot);
|
||||
}
|
||||
if (m == MB_FW_IO)
|
||||
had_io = true;
|
||||
}
|
||||
|
||||
if (had_io || ms == MB_MARK_IO || ms == MB_MARK_CORRUPT)
|
||||
{
|
||||
/* Halt to protect an existing Main while the flash state is uncertain;
|
||||
* but if slot 0 holds no valid backup there is nothing to protect, so
|
||||
* fall through and adopt instead of bricking a first boot. */
|
||||
uint8_t main_status = MB_SlotInfo(MB_SLOT_BACKUP, NULL);
|
||||
bool main_exists = (main_status != MB_ERR_MAGIC &&
|
||||
main_status != MB_ERR_NOT_COMMITTED);
|
||||
if (main_exists)
|
||||
mb_state_error_halt();
|
||||
}
|
||||
|
||||
return mb_remember_boot_state(mb_adopt_internal_as_main(), MB_SLOT_BACKUP);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/* Copyright 2026 Armel F4HWN
|
||||
* https://github.com/armel
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef UI_MULTIBOOT_H
|
||||
#define UI_MULTIBOOT_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* Blocking boot-time slot selector. Returns only when the user chooses EXIT;
|
||||
* a successful restore resets the radio from the RAM-resident copier. */
|
||||
void UI_MultibootSelector(void);
|
||||
|
||||
/* Show a blocking, acknowledged error screen for a failed SetCfg operation. */
|
||||
void UI_MultibootShowConfigError(uint8_t err);
|
||||
|
||||
/* Resolve the active config bank at boot, BEFORE any settings read. Detects a
|
||||
* firmware installed outside multiboot (internal identity != marker) and, if so,
|
||||
* discreetly self-backs it up into slot 0 and adopts slot 0 / bank 0. Returns the
|
||||
* config bank (0..MB_BANK_COUNT-1) to feed PY25Q16_SetBankBase(); the exact
|
||||
* firmware slot and bank are cached too (see MB_GetRunningSlot/MB_GetActiveBank). */
|
||||
uint8_t MB_BootResolveState(void);
|
||||
|
||||
/* Exact firmware slot and active config bank resolved for this session. Both
|
||||
* are cached in RAM so UI callers never have to reread the marker or scan slot
|
||||
* headers merely to render their state. */
|
||||
uint8_t MB_GetRunningSlot(void);
|
||||
uint8_t MB_GetActiveBank(void);
|
||||
|
||||
#endif
|
||||
Loaded 100 of 128 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user