mirror of
https://github.com/armel/uv-k1-k5v3-firmware-custom.git
synced 2026-10-04 03:57:29 +00:00
Compare commits
218
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
345535de4c | ||
|
|
bf6c28f74b | ||
|
|
705d68f89f | ||
|
|
150b96f736 | ||
|
|
a73a606b4b | ||
|
|
0aebf26ada | ||
|
|
43e9e1ec67 | ||
|
|
dbb9d42d2d | ||
|
|
749e856959 | ||
|
|
960d13e120 | ||
|
|
d12dbe0bf9 | ||
|
|
34ea3fd6af | ||
|
|
07226822cd | ||
|
|
a41fb99862 | ||
|
|
cc6527afc1 | ||
|
|
d95e4efa57 | ||
|
|
74b2397c96 | ||
|
|
e7db0bc1dc | ||
|
|
9019d0df1a | ||
|
|
cd99ddc46d | ||
|
|
b26265c5f0 | ||
|
|
c8da5f99cc | ||
|
|
902e19ba8d | ||
|
|
62906636ed | ||
|
|
99057f9dd2 | ||
|
|
0850420fdf | ||
|
|
4c3847ef34 | ||
|
|
bd049c0467 | ||
|
|
66ca97fa5e | ||
|
|
2a82a62ffd | ||
|
|
2f80e60c01 | ||
|
|
048e7e483b | ||
|
|
88e76b0e9b | ||
|
|
8c78d28f0d | ||
|
|
b86da168e9 | ||
|
|
104b7fb0b7 | ||
|
|
9358d5d58f | ||
|
|
9d6b560a54 | ||
|
|
f4811cbe1e | ||
|
|
678cb9b73a | ||
|
|
f35408317e | ||
|
|
3f958f637d | ||
|
|
b0143f9f9c | ||
|
|
24d3e5283f | ||
|
|
285f543d46 | ||
|
|
ce6bf08b23 | ||
|
|
265f83a447 | ||
|
|
88d17655de | ||
|
|
92c380dd7a | ||
|
|
e1dfbde590 | ||
|
|
04a80916a2 | ||
|
|
4315b98aec | ||
|
|
751c6064d1 | ||
|
|
ec7b4083c0 | ||
|
|
941693a0d8 | ||
|
|
66fb8b1ed4 | ||
|
|
7d550585d0 | ||
|
|
33dde24226 | ||
|
|
1bf75db62a | ||
|
|
e4a645a5c7 | ||
|
|
5b6174085b | ||
|
|
4ffff5149f | ||
|
|
4c74e79a40 | ||
|
|
ec0d537b69 | ||
|
|
86e48f5f69 | ||
|
|
024bfbaaf3 | ||
|
|
7d8b95407f | ||
|
|
94f1f978ff | ||
|
|
4cc5d11142 | ||
|
|
c8b65bb9d8 | ||
|
|
78f5b76a71 | ||
|
|
295d4a2c05 | ||
|
|
435192e3bf | ||
|
|
9c86a7c349 | ||
|
|
0a61a6de12 | ||
|
|
1021ee5e18 | ||
|
|
bef97f787b | ||
|
|
ca06478e24 | ||
|
|
919a687936 | ||
|
|
a2551d97dc | ||
|
|
a25d2fc75e | ||
|
|
6692b5191b | ||
|
|
c65db46bad | ||
|
|
a38f611bd5 | ||
|
|
528678a31b | ||
|
|
abc146a1b6 | ||
|
|
c7ff3eeb66 | ||
|
|
a3c72bdd93 | ||
|
|
e4408fcb55 | ||
|
|
5715cd27ef | ||
|
|
253104bb87 | ||
|
|
debf67a027 | ||
|
|
535ee80d2b | ||
|
|
559460739e | ||
|
|
0d69388e6d | ||
|
|
9bf075ea22 | ||
|
|
fe9c4e9432 | ||
|
|
1e4698409b | ||
|
|
39f46a8f93 | ||
|
|
3518ab8294 | ||
|
|
c45b7d2deb | ||
|
|
f904b87172 | ||
|
|
926787ddf3 | ||
|
|
fb9491caa5 | ||
|
|
04e8d47130 | ||
|
|
3bdddce67b | ||
|
|
d5cde97e5b | ||
|
|
66ed14dead | ||
|
|
602dfad6f3 | ||
|
|
0203586cdc | ||
|
|
3e5795255d | ||
|
|
a8912d3208 | ||
|
|
fe602b075d | ||
|
|
e445cfca85 | ||
|
|
18e9de38e0 | ||
|
|
f371795ef8 | ||
|
|
e9181c2287 | ||
|
|
5437bf2d8e | ||
|
|
b972e026ca | ||
|
|
0cad22f369 | ||
|
|
2a13c1819d | ||
|
|
4799eb46ac | ||
|
|
2569e4649d | ||
|
|
a59f3edcff | ||
|
|
027240de54 | ||
|
|
73f44911ed | ||
|
|
c0e3909853 | ||
|
|
31ff3b4df0 | ||
|
|
bbee375623 | ||
|
|
12dfa8a87f | ||
|
|
c9da65117f | ||
|
|
222448e1ad | ||
|
|
e1610bcc6f | ||
|
|
7f906300e3 | ||
|
|
bf7eda8a48 | ||
|
|
2ee1d96e5c | ||
|
|
65ec12bb0d | ||
|
|
dd3fc786b5 | ||
|
|
10868858c5 | ||
|
|
3bd3ebba2c | ||
|
|
39cbbd7e90 | ||
|
|
0567f01786 | ||
|
|
fc12a35418 | ||
|
|
2580acfb07 | ||
|
|
56a6083689 | ||
|
|
29725ed704 | ||
|
|
b44df132d0 | ||
|
|
676f26be34 | ||
|
|
46034ec39f | ||
|
|
e2d21c61c2 | ||
|
|
f8e1b7a436 | ||
|
|
9447eaada9 | ||
|
|
43533c838b | ||
|
|
7306db5f8f | ||
|
|
fbd7b37c21 | ||
|
|
8d3754b42f | ||
|
|
6a556e0601 | ||
|
|
9c1b1cb063 | ||
|
|
ae7bf65383 | ||
|
|
fb1360cce2 | ||
|
|
374e52c4d1 | ||
|
|
42aeac3469 | ||
|
|
98ac57b032 | ||
|
|
3e6e7c87b5 | ||
|
|
e908eda1da | ||
|
|
4fccdb78b9 | ||
|
|
a14127867e | ||
|
|
c02aac265a | ||
|
|
c24592cbe8 | ||
|
|
ff096e12e5 | ||
|
|
5fcc1598dd | ||
|
|
9b8493b450 | ||
|
|
f1df96f1f6 | ||
|
|
f36c524fa7 | ||
|
|
9ab5542326 | ||
|
|
0187c05e65 | ||
|
|
914c354da0 | ||
|
|
a771387edd | ||
|
|
09bc14e64b | ||
|
|
58c0170fb1 | ||
|
|
5b4b3e2a71 | ||
|
|
65a5c9f4ef | ||
|
|
a2936402c2 | ||
|
|
f069ed935f | ||
|
|
dcbbd744ac | ||
|
|
bca06e3382 | ||
|
|
8723863fb0 | ||
|
|
c775fa8b2f | ||
|
|
80e7d4b3d1 | ||
|
|
e6a727eeda | ||
|
|
887e18efe6 | ||
|
|
20b87feb1d | ||
|
|
e2e9c9ba60 | ||
|
|
337eb0cb12 | ||
|
|
504a2b0fb0 | ||
|
|
16b07c6374 | ||
|
|
f5b20d4f80 | ||
|
|
ae14bf5df3 | ||
|
|
9dc85852ba | ||
|
|
f7d29f6a14 | ||
|
|
1184618731 | ||
|
|
3d15d59ce0 | ||
|
|
15fccc31d6 | ||
|
|
48f753caaf | ||
|
|
edc4673983 | ||
|
|
9e03f02296 | ||
|
|
25ea1f827b | ||
|
|
8f11a32c9d | ||
|
|
576b8c552f | ||
|
|
85db5a2394 | ||
|
|
98989d4f0a | ||
|
|
8c976cfffd | ||
|
|
0ce5422db3 | ||
|
|
d939009949 | ||
|
|
50efade5c4 | ||
|
|
682a59fc0e | ||
|
|
3d3f370b86 | ||
|
|
eebeba09db |
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
|
||||
+10
@@ -8,6 +8,7 @@ firmware
|
||||
compile_commands.json
|
||||
.vscode
|
||||
.claude
|
||||
.agents
|
||||
/docs
|
||||
k5_eeprom.raw
|
||||
|
||||
@@ -15,3 +16,12 @@ k5_eeprom.raw
|
||||
__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/
|
||||
+81
-16
@@ -70,6 +70,7 @@ if(ENABLE_FEAT_F4HWN)
|
||||
"AUTHOR_STRING_2=\"${AUTHOR_STRING_2}\""
|
||||
"VERSION_STRING_1=\"${VERSION_STRING_1}\""
|
||||
"VERSION_STRING_2=\"${VERSION_STRING_2}\""
|
||||
"DISPLAY_VERSION_STRING_2=\"${DISPLAY_VERSION_STRING_2}\""
|
||||
"EDITION_STRING=\"${EDITION_STRING}\""
|
||||
ALERT_TOT=10
|
||||
)
|
||||
@@ -94,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
|
||||
)
|
||||
|
||||
@@ -117,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
|
||||
@@ -131,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
|
||||
@@ -151,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)
|
||||
@@ -166,26 +170,47 @@ 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 ----
|
||||
|
||||
# Thank you @markusb
|
||||
enable_feature(ENABLE_REGA
|
||||
app/rega.c
|
||||
)
|
||||
# Thank you @reppad
|
||||
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_SCREENSHOT
|
||||
screenshot.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
|
||||
)
|
||||
enable_feature(ENABLE_FEAT_F4HWN_SPECTRUM)
|
||||
enable_feature(ENABLE_FEAT_F4HWN_RX_TX_TIMER)
|
||||
@@ -197,6 +222,8 @@ enable_feature(ENABLE_FEAT_F4HWN_INV)
|
||||
enable_feature(ENABLE_FEAT_F4HWN_CTR)
|
||||
enable_feature(ENABLE_FEAT_F4HWN_SCAN_PROGRESS)
|
||||
enable_feature(ENABLE_FEAT_F4HWN_SCAN_FASTER)
|
||||
enable_feature(ENABLE_FEAT_F4HWN_SCAN_RSSI)
|
||||
enable_feature(ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE)
|
||||
enable_feature(ENABLE_FEAT_F4HWN_RESCUE_OPS)
|
||||
enable_feature(ENABLE_FEAT_F4HWN_VOL)
|
||||
enable_feature(ENABLE_FEAT_F4HWN_AUDIO)
|
||||
@@ -209,18 +236,56 @@ enable_feature(ENABLE_FEAT_F4HWN_MEM)
|
||||
enable_feature(ENABLE_FEAT_F4HWN_BEAM
|
||||
app/beam.c
|
||||
)
|
||||
enable_feature(ENABLE_FEAT_F4HWN_RXTX_LOG
|
||||
app/rxtx_log.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)
|
||||
message(FATAL_ERROR "ENABLE_FEAT_F4HWN_ACTION_PICKER requires ENABLE_FEAT_F4HWN.")
|
||||
endif()
|
||||
|
||||
enable_feature(ENABLE_FEAT_F4HWN_RXTX_LOG_WRAP)
|
||||
enable_feature(ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER)
|
||||
|
||||
if(ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER AND NOT (ENABLE_FEAT_F4HWN_RXTX_LOG AND ENABLE_FEAT_F4HWN_K5VIEWER))
|
||||
message(FATAL_ERROR "ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER requires ENABLE_FEAT_F4HWN_RXTX_LOG (log data) and ENABLE_FEAT_F4HWN_K5VIEWER (serial transport).")
|
||||
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)
|
||||
enable_feature(ENABLE_FEAT_F4HWN_MENU_CAT)
|
||||
|
||||
if(ENABLE_FEAT_F4HWN_LOGO_SAV AND NOT ENABLE_FEAT_F4HWN_LOGO)
|
||||
message(FATAL_ERROR "ENABLE_FEAT_F4HWN_LOGO_SAV requires ENABLE_FEAT_F4HWN_LOGO.")
|
||||
endif()
|
||||
|
||||
if(ENABLE_FEAT_F4HWN_MENU_CAT AND NOT ENABLE_FEAT_F4HWN)
|
||||
message(FATAL_ERROR "ENABLE_FEAT_F4HWN_MENU_CAT requires ENABLE_FEAT_F4HWN.")
|
||||
endif()
|
||||
|
||||
# ---- DEBUGGING ----
|
||||
|
||||
enable_feature(ENABLE_AGC_SHOW_DATA)
|
||||
|
||||
-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
|
||||
+250
-120
@@ -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"
|
||||
@@ -42,26 +45,34 @@
|
||||
#include "ui/inputbox.h"
|
||||
#include "ui/main.h"
|
||||
#include "ui/ui.h"
|
||||
#ifdef ENABLE_REGA
|
||||
#include "app/rega.h"
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_BEAM
|
||||
#include "app/beam.h"
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
|
||||
#include "app/rxtx_log.h"
|
||||
#endif
|
||||
#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 (*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,
|
||||
@@ -73,38 +84,18 @@ void (*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
|
||||
@@ -112,7 +103,6 @@ void (*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
|
||||
@@ -120,27 +110,65 @@ void (*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_REGA
|
||||
[ACTION_OPT_REGA_ALARM] = &ACTION_RegaAlarm,
|
||||
[ACTION_OPT_REGA_TEST] = &ACTION_RegaTest,
|
||||
#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
|
||||
#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)
|
||||
{
|
||||
@@ -187,7 +215,7 @@ void ACTION_Monitor(void)
|
||||
|
||||
RADIO_SetupRegisters(true);
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (gFmRadioMode) {
|
||||
FM_Start();
|
||||
gRequestDisplayScreen = DISPLAY_FM;
|
||||
@@ -201,7 +229,7 @@ void ACTION_Scan(bool bRestart)
|
||||
{
|
||||
(void)bRestart;
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (gFmRadioMode) {
|
||||
ACTION_Scan_FM(bRestart);
|
||||
return;
|
||||
@@ -244,7 +272,7 @@ void ACTION_Scan(bool bRestart)
|
||||
|
||||
// channel mode. Keep scanning but toggle between scan lists
|
||||
RADIO_NextValidList(1);
|
||||
UI_MAIN_NotifyScanProgressDataChanged();
|
||||
UI_MAIN_NotifyScanListChanged();
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_RESUME_STATE
|
||||
SETTINGS_WriteCurrentState();
|
||||
@@ -294,6 +322,123 @@ void ACTION_SwitchDemodul(void)
|
||||
}
|
||||
|
||||
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
inline static bool ACTION_IsBlockedInFM(uint8_t action)
|
||||
{
|
||||
switch (action) {
|
||||
case ACTION_OPT_POWER:
|
||||
case ACTION_OPT_MONITOR:
|
||||
case ACTION_OPT_A_B:
|
||||
case ACTION_OPT_VFO_MR:
|
||||
case ACTION_OPT_SWITCH_DEMODUL:
|
||||
#ifdef ENABLE_VOX
|
||||
case ACTION_OPT_VOX:
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN
|
||||
case ACTION_OPT_RXMODE:
|
||||
case ACTION_OPT_MAINONLY:
|
||||
case ACTION_OPT_WN:
|
||||
#ifdef ENABLE_FEAT_F4HWN_AUDIO
|
||||
case ACTION_OPT_RXA:
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
|
||||
case ACTION_OPT_POWER_HIGH:
|
||||
case ACTION_OPT_REMOVE_OFFSET:
|
||||
#endif
|
||||
#endif
|
||||
#if defined(ENABLE_FEAT_F4HWN_BEAM) || defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
|
||||
case ACTION_OPT_BEAM:
|
||||
#endif
|
||||
#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;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
static void ACTION_Execute(uint8_t action)
|
||||
{
|
||||
if (!ACTION_IsAvailable(action)) {
|
||||
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (gFmRadioMode && ACTION_IsBlockedInFM(action)) {
|
||||
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
|
||||
action_opt_table[action]();
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_ACTION_PICKER
|
||||
uint8_t gActionPickerKey;
|
||||
uint8_t gActionPickerSelection[2] = {1, 1};
|
||||
uint8_t gActionPickerTimeout_500ms;
|
||||
|
||||
bool ACTION_PickerProcessKey(KEY_Code_t key, bool isPressed, bool isHeld)
|
||||
{
|
||||
if (gActionPickerKey == 0)
|
||||
return false;
|
||||
if (isPressed)
|
||||
gActionPickerTimeout_500ms = ACTION_PICKER_TIMEOUT_500MS;
|
||||
uint8_t *selection = &gActionPickerSelection[gActionPickerKey - 1];
|
||||
|
||||
switch (key) {
|
||||
case KEY_UP:
|
||||
case KEY_DOWN:
|
||||
if (isPressed && !isHeld) {
|
||||
if (key == KEY_UP) {
|
||||
if (--*selection == 0)
|
||||
*selection = gSubMenu_SIDEFUNCTIONS_size - 1;
|
||||
}
|
||||
else if (++*selection >= gSubMenu_SIDEFUNCTIONS_size) {
|
||||
*selection = 1;
|
||||
}
|
||||
|
||||
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
|
||||
gUpdateDisplay = true;
|
||||
}
|
||||
return true;
|
||||
|
||||
case KEY_MENU:
|
||||
if (!isPressed && !isHeld) {
|
||||
const uint8_t action = gSubMenu_SIDEFUNCTIONS[*selection].id;
|
||||
gActionPickerKey = 0;
|
||||
gUpdateDisplay = true;
|
||||
ACTION_Execute(action);
|
||||
}
|
||||
return true;
|
||||
|
||||
case KEY_EXIT:
|
||||
case KEY_F:
|
||||
if (!isPressed) {
|
||||
gActionPickerKey = 0;
|
||||
gUpdateDisplay = true;
|
||||
}
|
||||
return true;
|
||||
|
||||
case KEY_PTT:
|
||||
gActionPickerKey = 0;
|
||||
gUpdateDisplay = true;
|
||||
return false;
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void ACTION_Handle(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
|
||||
{
|
||||
HideFKeyIcon();
|
||||
@@ -355,51 +500,52 @@ void ACTION_Handle(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
|
||||
}
|
||||
|
||||
// held or released after short press
|
||||
ACTION_Execute(func);
|
||||
}
|
||||
|
||||
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
if (gFmRadioMode) { // do not run these actions in FM radio mode
|
||||
switch (func) {
|
||||
case ACTION_OPT_POWER:
|
||||
case ACTION_OPT_MONITOR:
|
||||
case ACTION_OPT_A_B:
|
||||
case ACTION_OPT_VFO_MR:
|
||||
case ACTION_OPT_SWITCH_DEMODUL:
|
||||
#ifdef ENABLE_VOX
|
||||
case ACTION_OPT_VOX:
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN
|
||||
case ACTION_OPT_RXMODE:
|
||||
case ACTION_OPT_MAINONLY:
|
||||
case ACTION_OPT_WN:
|
||||
#ifdef ENABLE_FEAT_F4HWN_AUDIO
|
||||
case ACTION_OPT_RXA:
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
|
||||
case ACTION_OPT_POWER_HIGH:
|
||||
case ACTION_OPT_REMOVE_OFFSET:
|
||||
#endif
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_BEAM
|
||||
case ACTION_OPT_BEAM:
|
||||
#endif
|
||||
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
|
||||
return;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
#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;
|
||||
}
|
||||
#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;
|
||||
@@ -415,8 +561,22 @@ 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) {
|
||||
// Stop the channel/frequency scan before switching to the FM radio.
|
||||
gScanKeepResult = false;
|
||||
CHFRSCANNER_Stop();
|
||||
}
|
||||
|
||||
RADIO_SelectVfos();
|
||||
RADIO_SetupRegisters(true);
|
||||
|
||||
@@ -424,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())
|
||||
@@ -465,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 && gEeprom.KEY_LOCK_PTT)
|
||||
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
|
||||
@@ -515,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)
|
||||
{
|
||||
@@ -611,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)
|
||||
@@ -657,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;
|
||||
|
||||
+14
-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);
|
||||
@@ -50,8 +49,19 @@ void ACTION_SwitchDemodul(void);
|
||||
void ACTION_Power_High(void);
|
||||
void ACTION_Remove_Offset(void);
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
|
||||
void ACTION_RxTxLog(void);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_ACTION_PICKER
|
||||
#define ACTION_PICKER_TIMEOUT_500MS 10u
|
||||
extern uint8_t gActionPickerKey;
|
||||
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
|
||||
+259
-205
@@ -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,9 +30,10 @@
|
||||
#include "ui/ui.h"
|
||||
#include "settings.h"
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
|
||||
#include "screenshot.h"
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
#include "k5viewer.h"
|
||||
#endif
|
||||
|
||||
static const uint16_t Obfuscation[8] = { 0x6C16, 0xE614, 0x912E, 0x400D, 0x3521, 0x40D5, 0x0313, 0x80E9 };
|
||||
@@ -40,140 +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, 0x90D6, AIRCOPY_WRITE_BYTES }, // VFO area (so scan-range source frequencies are replicated)
|
||||
};
|
||||
|
||||
// total_blocks = ceil(0x170/64) + ceil(0x60/64) + ceil(0xD6/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_SCREENSHOT
|
||||
SCREENSHOT_Update(true);
|
||||
#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;
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
|
||||
SCREENSHOT_Update(false);
|
||||
AircopyCountdown = 0;
|
||||
gAircopyState = state;
|
||||
gUpdateDisplay = true;
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
K5VIEWER_Update(false);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void AIRCOPY_Obfuscate(unsigned int count)
|
||||
@@ -183,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_SCREENSHOT
|
||||
SCREENSHOT_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)
|
||||
@@ -256,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;
|
||||
@@ -356,8 +408,6 @@ static void AIRCOPY_Key_EXIT()
|
||||
{
|
||||
if (gInputBoxIndex == 0) {
|
||||
AIRCOPY_InitTransfer(0); // Mode: Receive
|
||||
gErrorsDuringAirCopy = lErrorsDuringAirCopy = 0;
|
||||
|
||||
BK4819_PrepareFSKReceive();
|
||||
|
||||
} else {
|
||||
@@ -368,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)
|
||||
@@ -383,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;
|
||||
}
|
||||
}
|
||||
@@ -397,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;
|
||||
}
|
||||
@@ -425,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.
|
||||
|
||||
+258
-165
@@ -18,7 +18,6 @@
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "am_fix.h"
|
||||
#include "app/action.h"
|
||||
|
||||
#ifdef ENABLE_AIRCOPY
|
||||
@@ -33,12 +32,15 @@
|
||||
#ifdef ENABLE_FLASHLIGHT
|
||||
#include "app/flashlight.h"
|
||||
#endif
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
#include "app/fm.h"
|
||||
#endif
|
||||
#include "app/generic.h"
|
||||
#include "app/main.h"
|
||||
#include "app/menu.h"
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
|
||||
#include "app/rxtx_log.h"
|
||||
#endif
|
||||
#include "app/scanner.h"
|
||||
#if defined(ENABLE_UART) || defined(ENABLE_USB)
|
||||
#include "app/uart.h"
|
||||
@@ -51,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"
|
||||
@@ -80,8 +82,8 @@
|
||||
#include "ui/ui.h"
|
||||
#include "ui/welcome.h"
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
|
||||
#include "screenshot.h"
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
#include "k5viewer.h"
|
||||
#endif
|
||||
|
||||
static bool flagSaveVfo;
|
||||
@@ -104,26 +106,29 @@ static KEY_Code_t gScreenSaverWakeKey = KEY_INVALID;
|
||||
static void ProcessKey(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld);
|
||||
|
||||
|
||||
void (*ProcessKeysFunctions[])(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) = {
|
||||
void (*const ProcessKeysFunctions[])(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld) = {
|
||||
[DISPLAY_MAIN] = &MAIN_ProcessKeys,
|
||||
[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
|
||||
[DISPLAY_AIRCOPY] = &AIRCOPY_ProcessKeys,
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
|
||||
[DISPLAY_RXTX_LOG] = &RXTX_LOG_ProcessKeys,
|
||||
#endif
|
||||
};
|
||||
|
||||
#ifdef ENABLE_REGA
|
||||
// This is a hack for REGA as I need a special display element only for it with no key
|
||||
static_assert(ARRAY_SIZE(ProcessKeysFunctions) == DISPLAY_N_ELEM-1);
|
||||
#else
|
||||
static_assert(ARRAY_SIZE(ProcessKeysFunctions) == DISPLAY_N_ELEM);
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_LOGO_SAV
|
||||
static uint8_t ScreenSaverRandom(void)
|
||||
@@ -135,20 +140,13 @@ static uint8_t ScreenSaverRandom(void)
|
||||
static void ScreenSaverSetPixel(uint8_t x, uint8_t y, bool fill)
|
||||
{
|
||||
const uint8_t pattern = 1u << (y & 7u);
|
||||
|
||||
uint8_t *target = (y < 8) ? &gStatusLine[x] : &gFrameBuffer[(y >> 3) - 1][x];
|
||||
|
||||
if (y < 8) {
|
||||
if (fill)
|
||||
gStatusLine[x] |= pattern;
|
||||
else
|
||||
gStatusLine[x] &= (uint8_t)~pattern;
|
||||
return;
|
||||
}
|
||||
|
||||
y -= 8;
|
||||
if (fill)
|
||||
gFrameBuffer[y >> 3][x] |= pattern;
|
||||
*target |= pattern;
|
||||
else
|
||||
gFrameBuffer[y >> 3][x] &= (uint8_t)~pattern;
|
||||
*target &= (uint8_t)~pattern;
|
||||
}
|
||||
|
||||
static void ScreenSaverDrawMatrixGlyph(uint8_t x, int16_t y, uint8_t glyph)
|
||||
@@ -235,22 +233,25 @@ static void ScreenSaverRenderLogoPlus(bool reset)
|
||||
|
||||
static void ScreenSaverUpdateViewer(void)
|
||||
{
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
|
||||
SCREENSHOT_Update(false);
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
K5VIEWER_Update(false);
|
||||
#endif
|
||||
}
|
||||
|
||||
static bool ScreenSaverCanDisplay(void)
|
||||
static bool ScreenSaverCanDisplay(bool modal)
|
||||
{
|
||||
if (gSetting_set_sav == SET_SAV_OFF ||
|
||||
gEeprom.BACKLIGHT_TIME == 0 ||
|
||||
gEeprom.BACKLIGHT_TIME >= 61 ||
|
||||
gScreenSaverDisplayed ||
|
||||
#ifdef ENABLE_FEAT_F4HWN_SLEEP
|
||||
gWakeUp ||
|
||||
#endif
|
||||
gCurrentFunction == FUNCTION_TRANSMIT ||
|
||||
FUNCTION_IsRx() ||
|
||||
gPttIsPressed
|
||||
#ifdef ENABLE_FMRADIO
|
||||
|| gFM_ScanState != FM_SCAN_OFF
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
|| (gFM_ScanState != FM_SCAN_OFF && !gFM_FoundFrequency)
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_BEAM
|
||||
|| gBeamActive
|
||||
@@ -260,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
|
||||
@@ -271,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)
|
||||
@@ -298,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)
|
||||
@@ -309,11 +332,59 @@ 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_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)
|
||||
return;
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_LOGO_SAV
|
||||
ScreenSaverExit();
|
||||
#endif
|
||||
@@ -446,6 +517,12 @@ static void HandleIncoming(void)
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
// Defensive: do not leave FM scan for a main-channel signal.
|
||||
if (gFmRadioMode && gFM_ScanState != FM_SCAN_OFF)
|
||||
return;
|
||||
#endif
|
||||
|
||||
APP_StartListening(gMonitor ? FUNCTION_MONITOR : FUNCTION_RECEIVE);
|
||||
}
|
||||
|
||||
@@ -711,7 +788,7 @@ void APP_StartListening(FUNCTION_Type_t function)
|
||||
return;
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (gFmRadioMode)
|
||||
BK1080_Init0();
|
||||
#endif
|
||||
@@ -776,7 +853,11 @@ void APP_StartListening(FUNCTION_Type_t function)
|
||||
|
||||
FUNCTION_Select(function);
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
|
||||
RXTX_LOG_BeginRx(gRxVfo, function);
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (function == FUNCTION_MONITOR || gFmRadioMode)
|
||||
#else
|
||||
if (function == FUNCTION_MONITOR)
|
||||
@@ -993,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
|
||||
}
|
||||
@@ -1074,7 +1163,7 @@ static void HandleVox(void)
|
||||
gVoxPauseCountdown = 0;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (gFmRadioMode)
|
||||
return;
|
||||
#endif
|
||||
@@ -1119,10 +1208,10 @@ static void HandleVox(void)
|
||||
|
||||
void APP_Update(void)
|
||||
{
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
// Parse incoming packets on every tick so serial keys are never missed,
|
||||
// regardless of whether the screen needs redrawing.
|
||||
SCREENSHOT_ParseInput();
|
||||
K5VIEWER_ParseInput();
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_VOICE
|
||||
@@ -1236,16 +1325,18 @@ 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;
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_VOICE
|
||||
if (!SCANNER_IsScanning() && gScanStateDir != SCAN_OFF && gScheduleScanListen && !gPttIsPressed && gVoiceWriteIndex == 0)
|
||||
if (!SCANNER_IsScanning() && gScanStateDir != SCAN_OFF && gScheduleScanListen && !gPttIsPressed && gVoiceWriteIndex == 0
|
||||
&& !UI_MAIN_ShouldHoldScanResume())
|
||||
#else
|
||||
if (!SCANNER_IsScanning() && gScanStateDir != SCAN_OFF && gScheduleScanListen && !gPttIsPressed)
|
||||
if (!SCANNER_IsScanning() && gScanStateDir != SCAN_OFF && gScheduleScanListen && !gPttIsPressed
|
||||
&& !UI_MAIN_ShouldHoldScanResume())
|
||||
#endif
|
||||
{ // scanning
|
||||
CHFRSCANNER_ContinueScanning();
|
||||
@@ -1279,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
|
||||
@@ -1298,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();
|
||||
@@ -1318,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
|
||||
@@ -1430,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
|
||||
@@ -1580,16 +1671,14 @@ void APP_TimeSlice10ms(void)
|
||||
|
||||
SETTINGS_SaveVfoIndicesFlush();
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
|
||||
RXTX_LOG_Task10ms();
|
||||
#endif
|
||||
|
||||
BACKLIGHT_Update();
|
||||
|
||||
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();
|
||||
@@ -1601,8 +1690,17 @@ 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
|
||||
if (gActionPickerKey != 0 && FUNCTION_IsRx()) {
|
||||
gActionPickerKey = 0;
|
||||
gUpdateDisplay = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
if (gCurrentFunction == FUNCTION_TRANSMIT)
|
||||
{ // transmitting
|
||||
@@ -1626,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) {
|
||||
@@ -1636,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
|
||||
@@ -1660,19 +1752,21 @@ void APP_TimeSlice10ms(void)
|
||||
UI_DisplayStatus();
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
if (gUpdateDisplayCurrent || gUpdateStatusCurrent
|
||||
#ifdef ENABLE_FEAT_F4HWN_LOGO_SAV
|
||||
|| screenSaverRendered
|
||||
#endif
|
||||
) {
|
||||
SCREENSHOT_Update(false);
|
||||
K5VIEWER_Update(false);
|
||||
} else if (K5VIEWER_HasPendingStateChange()) {
|
||||
K5VIEWER_Update(false);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Skipping authentic device checks
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (gFmRadioMode && gFmRadioCountdown_500ms > 0) // 1of11
|
||||
return;
|
||||
#endif
|
||||
@@ -1692,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) {
|
||||
@@ -1750,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
|
||||
@@ -1762,8 +1810,12 @@ void APP_TimeSlice10ms(void)
|
||||
|
||||
SCANNER_TimeSlice10ms();
|
||||
|
||||
#if defined(ENABLE_SCAN_RANGES) && defined(ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE) && ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE
|
||||
CHFRSCANNER_UpdateCssDetection();
|
||||
#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();
|
||||
}
|
||||
@@ -1796,6 +1848,15 @@ void cancelUserInputModes(void)
|
||||
void APP_TimeSlice500ms(void)
|
||||
{
|
||||
gNextTimeslice_500ms = false;
|
||||
#ifdef ENABLE_FEAT_F4HWN_ACTION_PICKER
|
||||
if (gActionPickerKey != 0 && gActionPickerTimeout_500ms > 0 &&
|
||||
--gActionPickerTimeout_500ms == 0)
|
||||
{
|
||||
gActionPickerKey = 0;
|
||||
gUpdateDisplay = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
bool exit_menu = false;
|
||||
|
||||
// Skipped authentic device check
|
||||
@@ -1804,6 +1865,10 @@ void APP_TimeSlice500ms(void)
|
||||
if (--gKeypadLocked == 0)
|
||||
gUpdateDisplay = true;
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
|
||||
RXTX_LOG_Tick500ms();
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_RX_TX_TIMER
|
||||
if (gSetting_set_tmr && (gCurrentFunction == FUNCTION_TRANSMIT || FUNCTION_IsRx())) {
|
||||
const uint16_t timerCountdown = (gCurrentFunction == FUNCTION_TRANSMIT)
|
||||
@@ -1821,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
|
||||
)
|
||||
@@ -1868,7 +1933,7 @@ void APP_TimeSlice500ms(void)
|
||||
|
||||
// Skipped authentic device check
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (gFmRadioCountdown_500ms > 0)
|
||||
{
|
||||
gFmRadioCountdown_500ms--;
|
||||
@@ -1887,7 +1952,7 @@ void APP_TimeSlice500ms(void)
|
||||
) {
|
||||
BACKLIGHT_TurnOff();
|
||||
#ifdef ENABLE_FEAT_F4HWN_LOGO_SAV
|
||||
ScreenSaverTryDisplay();
|
||||
ScreenSaverTryDisplay(false);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1918,6 +1983,9 @@ void APP_TimeSlice500ms(void)
|
||||
gBacklightCountdown_500ms = 0;
|
||||
gPowerSave_10ms = 1;
|
||||
gWakeUp = true;
|
||||
#ifdef ENABLE_FEAT_F4HWN_LOGO_SAV
|
||||
ScreenSaverExit();
|
||||
#endif
|
||||
// TODO:
|
||||
// PWM_PLUS0_CH0_COMP = 0;
|
||||
BACKLIGHT_SetBrightness(0);
|
||||
@@ -1974,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1993,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
|
||||
@@ -2008,6 +2080,10 @@ void APP_TimeSlice500ms(void)
|
||||
{
|
||||
gEeprom.KEY_LOCK = true; // lock the keyboard
|
||||
gUpdateStatus = true; // lock symbol needs showing
|
||||
#ifdef ENABLE_FEAT_F4HWN_ACTION_PICKER
|
||||
gActionPickerKey = 0;
|
||||
gUpdateDisplay = true;
|
||||
#endif
|
||||
}
|
||||
|
||||
if (exit_menu) {
|
||||
@@ -2044,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;
|
||||
}
|
||||
@@ -2066,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();
|
||||
@@ -2082,7 +2158,7 @@ void APP_TimeSlice500ms(void)
|
||||
!gAskToSave &&
|
||||
!gCssBackgroundScan)
|
||||
{
|
||||
ScreenSaverTryDisplay();
|
||||
ScreenSaverTryDisplay(false);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -2120,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;
|
||||
@@ -2221,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;
|
||||
@@ -2248,7 +2322,11 @@ static void ProcessKey(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
|
||||
BACKLIGHT_TurnOn();
|
||||
}
|
||||
|
||||
if (Key == KEY_EXIT && bKeyHeld) { // exit key held pressed
|
||||
if (Key == KEY_EXIT && bKeyHeld
|
||||
#ifdef ENABLE_FEAT_F4HWN_ACTION_PICKER
|
||||
&& gActionPickerKey == 0
|
||||
#endif
|
||||
) { // exit key held pressed
|
||||
// clear the live DTMF decoder
|
||||
if (gDTMF_RX_live[0] != 0) {
|
||||
DTMF_clear_input_box_memory();
|
||||
@@ -2285,11 +2363,19 @@ static void ProcessKey(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
|
||||
}
|
||||
|
||||
bool lowBatPopup = gLowBattery && !gLowBatteryConfirmed && gScreenToDisplay == DISPLAY_MAIN;
|
||||
#ifdef ENABLE_FEAT_F4HWN_ACTION_PICKER
|
||||
if (gActionPickerKey != 0 &&
|
||||
(gEeprom.KEY_LOCK || lowBatPopup || gScreenToDisplay != DISPLAY_MAIN))
|
||||
gActionPickerKey = 0;
|
||||
if (ACTION_PickerProcessKey(Key, bKeyPressed, bKeyHeld))
|
||||
goto Skip;
|
||||
#endif
|
||||
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN // Disable PTT if KEY_LOCK
|
||||
bool lck_condition = (gEeprom.KEY_LOCK || lowBatPopup) && gCurrentFunction != FUNCTION_TRANSMIT;
|
||||
|
||||
if(!gSetting_set_lck)
|
||||
if((gSetting_set_lck & SET_LCK_PTT) == 0)
|
||||
lck_condition = lck_condition && Key != KEY_PTT;
|
||||
|
||||
if (lck_condition)
|
||||
@@ -2306,6 +2392,12 @@ static void ProcessKey(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
|
||||
return;
|
||||
}
|
||||
|
||||
// The ACTIONS restriction only applies when the keypad is actually
|
||||
// locked: this block is also entered for the low battery popup, where
|
||||
// action keys must keep working as before
|
||||
const bool passActionKey = ((gSetting_set_lck & SET_LCK_ACTIONS) == 0 || !gEeprom.KEY_LOCK) &&
|
||||
(Key == KEY_SIDE1 || Key == KEY_SIDE2 || (Key == KEY_MENU && bKeyHeld));
|
||||
|
||||
if (Key == KEY_F) { // function/key-lock key
|
||||
if (!bKeyPressed)
|
||||
return;
|
||||
@@ -2320,8 +2412,7 @@ static void ProcessKey(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
|
||||
// KEY_MENU has a special treatment here, because we want to pass hold event to ACTION_Handle
|
||||
// but we don't want it to complain when initial press happens
|
||||
// we want to react on realese instead
|
||||
else if (Key != KEY_SIDE1 && Key != KEY_SIDE2 && // pass side buttons
|
||||
!(Key == KEY_MENU && bKeyHeld)) // pass KEY_MENU held
|
||||
else if (!passActionKey)
|
||||
{
|
||||
if ((!bKeyPressed || bKeyHeld || (Key == KEY_MENU && bKeyPressed)) && // prevent released or held, prevent KEY_MENU pressed
|
||||
!(Key == KEY_MENU && !bKeyPressed)) // pass KEY_MENU released
|
||||
@@ -2377,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;
|
||||
@@ -2430,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);
|
||||
@@ -2499,7 +2589,7 @@ Skip:
|
||||
gUpdateStatus = true;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (gRequestSaveFM) {
|
||||
gRequestSaveFM = false;
|
||||
if (!bKeyHeld)
|
||||
@@ -2581,7 +2671,10 @@ Skip:
|
||||
gFlagRefreshSetting = false;
|
||||
gMenuCountdown = menu_timeout_500ms;
|
||||
|
||||
MENU_ShowCurrentSetting();
|
||||
#ifdef ENABLE_FEAT_F4HWN_MENU_CAT
|
||||
if (gMenuLevel != MENU_LEVEL_CAT)
|
||||
#endif
|
||||
MENU_ShowCurrentSetting();
|
||||
}
|
||||
|
||||
if (gFlagPrepareTX) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
+39
-38
@@ -16,10 +16,22 @@
|
||||
|
||||
#include "app/breakout.h"
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
|
||||
#include "screenshot.h"
|
||||
#if defined(ENABLE_UART) || defined(ENABLE_USB)
|
||||
#include "app/uart.h"
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
#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;
|
||||
|
||||
@@ -97,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;
|
||||
|
||||
@@ -115,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;
|
||||
}
|
||||
|
||||
@@ -141,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;
|
||||
}
|
||||
}
|
||||
@@ -168,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++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,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);
|
||||
@@ -234,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);
|
||||
@@ -304,10 +302,10 @@ static void OnKeyDown(uint8_t key)
|
||||
// HandleUserInput
|
||||
static bool HandleUserInput()
|
||||
{
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
// Parse incoming packets on every tick so serial keys are never missed,
|
||||
// regardless of whether the screen needs redrawing.
|
||||
SCREENSHOT_ParseInput();
|
||||
K5VIEWER_ParseInput();
|
||||
#endif
|
||||
|
||||
kbd.prev = kbd.current;
|
||||
@@ -359,6 +357,9 @@ void APP_RunBreakout(void) {
|
||||
|
||||
while(isInitialized)
|
||||
{
|
||||
#if defined(ENABLE_UART) || defined(ENABLE_USB)
|
||||
UART_ServiceCommands();
|
||||
#endif
|
||||
Tick();
|
||||
if(!isPaused)
|
||||
{
|
||||
@@ -388,10 +389,10 @@ void APP_RunBreakout(void) {
|
||||
ST7565_BlitStatusLine(); // Blank status line
|
||||
ST7565_BlitFullScreen();
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
if(isPaused || swap == 0)
|
||||
{
|
||||
SCREENSHOT_Update(false);
|
||||
K5VIEWER_Update(false);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
+143
-8
@@ -1,5 +1,6 @@
|
||||
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "app/app.h"
|
||||
#include "app/chFrScanner.h"
|
||||
@@ -21,7 +22,14 @@ bool gScanPauseMode;
|
||||
uint32_t gScanRangeStart;
|
||||
uint32_t gScanRangeStop;
|
||||
|
||||
#define SCAN_RANGE_SKIP_MAX 32
|
||||
#if defined(ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE) && ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE
|
||||
DCS_CodeType_t gScanRangeCssType = CODE_TYPE_OFF;
|
||||
uint8_t gScanRangeCssCode = 0xFF;
|
||||
static uint8_t scanRangeCssCandidate = 0xFF;
|
||||
static uint8_t scanRangeCssHitCount = 0;
|
||||
#endif
|
||||
|
||||
#define SCAN_RANGE_SKIP_MAX 64
|
||||
#if (SCAN_RANGE_SKIP_MAX & (SCAN_RANGE_SKIP_MAX - 1)) != 0
|
||||
#error SCAN_RANGE_SKIP_MAX must be a power of two
|
||||
#endif
|
||||
@@ -182,6 +190,58 @@ bool CHFRSCANNER_HasScanRangeExcludedOrdinal(uint32_t first_ordinal, uint32_t la
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#if defined(ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE) && ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE
|
||||
void CHFRSCANNER_UpdateCssDetection(void)
|
||||
{
|
||||
if (!gScanRangeStart || !FUNCTION_IsRx())
|
||||
{
|
||||
gScanRangeCssType = CODE_TYPE_OFF;
|
||||
gScanRangeCssCode = 0xFF;
|
||||
scanRangeCssCandidate = 0xFF;
|
||||
scanRangeCssHitCount = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t cdcssFreq;
|
||||
uint16_t ctcssFreq;
|
||||
const BK4819_CssScanResult_t result = BK4819_GetCxCSSScanResult(&cdcssFreq, &ctcssFreq);
|
||||
|
||||
if (result == BK4819_CSS_RESULT_CDCSS)
|
||||
{
|
||||
const uint8_t Code = DCS_GetCdcssCode(cdcssFreq);
|
||||
if (Code != 0xFF && Code != gScanRangeCssCode)
|
||||
{
|
||||
gScanRangeCssType = CODE_TYPE_DIGITAL;
|
||||
gScanRangeCssCode = Code;
|
||||
scanRangeCssCandidate = 0xFF;
|
||||
scanRangeCssHitCount = 0;
|
||||
gUpdateDisplay = true;
|
||||
}
|
||||
}
|
||||
else if (result == BK4819_CSS_RESULT_CTCSS)
|
||||
{
|
||||
const uint8_t Code = DCS_GetCtcssCode(ctcssFreq);
|
||||
if (Code != 0xFF)
|
||||
{
|
||||
if (Code == scanRangeCssCandidate)
|
||||
{
|
||||
if (++scanRangeCssHitCount >= 2 && Code != gScanRangeCssCode)
|
||||
{
|
||||
gScanRangeCssType = CODE_TYPE_CONTINUOUS_TONE;
|
||||
gScanRangeCssCode = Code;
|
||||
gUpdateDisplay = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
scanRangeCssCandidate = Code;
|
||||
scanRangeCssHitCount = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
static void CHFRSCANNER_AbortActiveReception(void)
|
||||
@@ -232,6 +292,65 @@ static uint32_t scanFastPrevFrequency;
|
||||
static bool scanFastLastFullTuneCandidate;
|
||||
static VFO_Info_t scanFastDisplayVfo;
|
||||
static bool scanFastDisplayVfoValid;
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCAN_RSSI
|
||||
static uint16_t scanRssiSparkline[CHFRSCANNER_RSSI_SPARKLINE_WIDTH];
|
||||
static uint8_t scanRssiSparklineWrite;
|
||||
static uint8_t scanRssiSparklineCount;
|
||||
|
||||
static void ScanRssiSparklineReset(void)
|
||||
{
|
||||
memset(scanRssiSparkline, 0, sizeof(scanRssiSparkline));
|
||||
scanRssiSparklineWrite = 0;
|
||||
scanRssiSparklineCount = 0;
|
||||
}
|
||||
|
||||
static void ScanRssiSparklinePush(uint16_t rssi)
|
||||
{
|
||||
scanRssiSparkline[scanRssiSparklineWrite] = rssi;
|
||||
scanRssiSparklineWrite++;
|
||||
if (scanRssiSparklineWrite >= CHFRSCANNER_RSSI_SPARKLINE_WIDTH)
|
||||
scanRssiSparklineWrite = 0;
|
||||
|
||||
if (scanRssiSparklineCount < CHFRSCANNER_RSSI_SPARKLINE_WIDTH)
|
||||
scanRssiSparklineCount++;
|
||||
}
|
||||
|
||||
bool CHFRSCANNER_HasScanRssiSparkline(void)
|
||||
{
|
||||
return gScanStateDir != SCAN_OFF && scanRssiSparklineCount > 1;
|
||||
}
|
||||
|
||||
uint8_t CHFRSCANNER_GetScanRssiSparklineLevel(uint8_t index)
|
||||
{
|
||||
uint16_t minRssi = SCAN_FAST_RSSI_MAX;
|
||||
uint16_t rssi = 0;
|
||||
uint8_t oldest;
|
||||
|
||||
if (index >= CHFRSCANNER_RSSI_SPARKLINE_WIDTH || scanRssiSparklineCount == 0)
|
||||
return 0;
|
||||
|
||||
if (index < CHFRSCANNER_RSSI_SPARKLINE_WIDTH - scanRssiSparklineCount)
|
||||
return 0;
|
||||
|
||||
oldest = (uint8_t)((scanRssiSparklineWrite + CHFRSCANNER_RSSI_SPARKLINE_WIDTH - scanRssiSparklineCount)
|
||||
% CHFRSCANNER_RSSI_SPARKLINE_WIDTH);
|
||||
index -= (uint8_t)(CHFRSCANNER_RSSI_SPARKLINE_WIDTH - scanRssiSparklineCount);
|
||||
|
||||
for (uint8_t i = 0; i < scanRssiSparklineCount; i++)
|
||||
{
|
||||
const uint16_t sample = scanRssiSparkline[(oldest + i) % CHFRSCANNER_RSSI_SPARKLINE_WIDTH];
|
||||
if (sample != 0 && sample < minRssi)
|
||||
minRssi = sample;
|
||||
}
|
||||
|
||||
rssi = scanRssiSparkline[(oldest + index) % CHFRSCANNER_RSSI_SPARKLINE_WIDTH];
|
||||
if (rssi == 0 || minRssi == SCAN_FAST_RSSI_MAX || rssi <= minRssi + 2)
|
||||
return 0;
|
||||
|
||||
// 48 RSSI units ~= 24 dB. This keeps quiet jitter low while strong hits pop.
|
||||
return (uint8_t)MIN(((uint32_t)(rssi - minRssi) * 5u + 24u) / 48u, 5u);
|
||||
}
|
||||
#endif
|
||||
|
||||
static void ScanFastResetState(void)
|
||||
{
|
||||
@@ -434,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)
|
||||
@@ -538,6 +653,9 @@ static scan_fast_result_t ScanRangeFastPrecheck(void)
|
||||
ScanFastTune(freq);
|
||||
|
||||
const uint16_t rssi = ScanFastReadCandidateRssi();
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCAN_RSSI
|
||||
ScanRssiSparklinePush(rssi);
|
||||
#endif
|
||||
if (ScanFastIsCandidate(rssi))
|
||||
{
|
||||
ScanRangeFastRefineCandidate(rssi);
|
||||
@@ -573,7 +691,12 @@ static bool MemChannelFastPrecheck(uint16_t channel)
|
||||
scanFastReg30 = BK4819_ReadRegister(BK4819_REG_30) & ~BK4819_REG_30_MASK_ENABLE_AF_DAC;
|
||||
ScanFastTune(frequency);
|
||||
|
||||
if (ScanFastIsCandidate(ScanFastReadCandidateRssi()))
|
||||
const uint16_t rssi = ScanFastReadCandidateRssi();
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCAN_RSSI
|
||||
ScanRssiSparklinePush(rssi);
|
||||
#endif
|
||||
|
||||
if (ScanFastIsCandidate(rssi))
|
||||
{
|
||||
scanFastLastFullTuneCandidate = true;
|
||||
return true; // signal detected: let the full tune path follow
|
||||
@@ -637,17 +760,24 @@ void CHFRSCANNER_Start(const bool storeBackupSettings, const int8_t scan_directi
|
||||
currentScanList = SCAN_NEXT_CHAN_SCANLIST1;
|
||||
gScanStateDir = scan_direction;
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCAN_RSSI
|
||||
ScanRssiSparklineReset();
|
||||
#endif
|
||||
ScanFastResetState();
|
||||
#endif
|
||||
|
||||
if (IS_MR_CHANNEL(gNextMrChannel))
|
||||
{
|
||||
{
|
||||
bool scanListChanged = false;
|
||||
|
||||
if(!RADIO_CheckValidList(gEeprom.SCAN_LIST_DEFAULT)) {
|
||||
RADIO_NextValidList(1);
|
||||
UI_MAIN_NotifyScanProgressDataChanged();
|
||||
scanListChanged = true;
|
||||
}
|
||||
|
||||
if (storeBackupSettings || scanListChanged)
|
||||
UI_MAIN_NotifyScanListChanged();
|
||||
|
||||
// channel mode
|
||||
if (storeBackupSettings) {
|
||||
initialFrqOrChan = gRxVfo->CHANNEL_SAVE;
|
||||
@@ -820,6 +950,9 @@ void CHFRSCANNER_Stop(void)
|
||||
}
|
||||
|
||||
gScanStateDir = SCAN_OFF;
|
||||
#if defined(ENABLE_FEAT_F4HWN_SCAN_FASTER) && defined(ENABLE_FEAT_F4HWN_SCAN_RSSI)
|
||||
ScanRssiSparklineReset();
|
||||
#endif
|
||||
|
||||
const uint32_t chFr = gScanKeepResult ? lastFoundFrqOrChan : initialFrqOrChan;
|
||||
const bool channelChanged = chFr != initialFrqOrChan;
|
||||
@@ -996,7 +1129,9 @@ static void NextMemChannel(void)
|
||||
|
||||
if (!enabled || chan == 0xFFFF)
|
||||
{
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER
|
||||
const uint16_t searchStart = gNextMrChannel;
|
||||
#endif
|
||||
chan = RADIO_FindNextChannel(gNextMrChannel + gScanStateDir, gScanStateDir, true, gEeprom.SCAN_LIST_DEFAULT);
|
||||
if (chan == 0xFFFF)
|
||||
{ // no valid channel found -> wrapping back to the first channel
|
||||
|
||||
@@ -15,8 +15,16 @@ extern bool gScanPauseMode;
|
||||
#ifdef ENABLE_SCAN_RANGES
|
||||
extern uint32_t gScanRangeStart;
|
||||
extern uint32_t gScanRangeStop;
|
||||
#if defined(ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE) && ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE
|
||||
extern DCS_CodeType_t gScanRangeCssType;
|
||||
extern uint8_t gScanRangeCssCode;
|
||||
#endif
|
||||
|
||||
bool CHFRSCANNER_ExcludeCurrentScanRange(void);
|
||||
bool CHFRSCANNER_HasScanRangeExcludedOrdinal(uint32_t first_ordinal, uint32_t last_ordinal);
|
||||
#if defined(ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE) && ENABLE_FEAT_F4HWN_SCAN_SUBAUDIBLE
|
||||
void CHFRSCANNER_UpdateCssDetection(void);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
void CHFRSCANNER_Found(void);
|
||||
@@ -26,6 +34,11 @@ void CHFRSCANNER_ManualResume(const int8_t scan_direction);
|
||||
void CHFRSCANNER_ContinueScanning(void);
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER
|
||||
const VFO_Info_t *CHFRSCANNER_GetScanDisplayVfo(void);
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCAN_RSSI
|
||||
bool CHFRSCANNER_HasScanRssiSparkline(void);
|
||||
uint8_t CHFRSCANNER_GetScanRssiSparklineLevel(uint8_t index);
|
||||
#define CHFRSCANNER_RSSI_SPARKLINE_WIDTH 24u
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(ENABLE_FEAT_F4HWN_RESUME_STATE) || defined(ENABLE_SCAN_RANGES)
|
||||
|
||||
+3
-3
@@ -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();
|
||||
@@ -474,7 +474,7 @@ void DTMF_Reply(void)
|
||||
if (pString == NULL)
|
||||
return;
|
||||
|
||||
Delay = (gEeprom.DTMF_PRELOAD_TIME < 200) ? 200 : gEeprom.DTMF_PRELOAD_TIME;
|
||||
Delay = MAX(gEeprom.DTMF_PRELOAD_TIME, 200);
|
||||
|
||||
if (gEeprom.DTMF_SIDE_TONE)
|
||||
{ // the user will also hear the transmitted tones
|
||||
|
||||
+25
-14
@@ -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,12 @@ 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*/);
|
||||
}
|
||||
|
||||
void FM_TurnOff(void)
|
||||
{
|
||||
gFmRadioMode = false;
|
||||
@@ -163,11 +170,10 @@ void FM_Tune(uint16_t Frequency, int8_t Step, bool bFlag)
|
||||
|
||||
gFM_ScanState = Step;
|
||||
|
||||
BK1080_SetFrequency(gEeprom.FM_FrequencyPlaying, gEeprom.FM_Band/*, gEeprom.FM_Space*/);
|
||||
FM_SetFrequency();
|
||||
}
|
||||
|
||||
void FM_AudioPathOn(void) {
|
||||
BACKLIGHT_TurnOn();
|
||||
AUDIO_AudioPathOn();
|
||||
gEnableSpeaker = true;
|
||||
}
|
||||
@@ -182,15 +188,17 @@ void FM_PlayAndUpdate(void)
|
||||
}
|
||||
|
||||
FM_ConfigureChannelState();
|
||||
BK1080_SetFrequency(gEeprom.FM_FrequencyPlaying, gEeprom.FM_Band/*, gEeprom.FM_Space*/);
|
||||
FM_SetFrequency();
|
||||
SETTINGS_SaveFM();
|
||||
|
||||
gFmPlayCountdown_10ms = 0;
|
||||
gScheduleFM = false;
|
||||
gAskToSave = false;
|
||||
|
||||
BACKLIGHT_TurnOn();
|
||||
FM_AudioPathOn();
|
||||
}
|
||||
#endif
|
||||
|
||||
int FM_CheckFrequencyLock(uint16_t Frequency, uint16_t LowerLimit)
|
||||
{
|
||||
@@ -235,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 };
|
||||
@@ -291,7 +300,7 @@ static void Key_DIGITS(KEY_Code_t Key, uint8_t state)
|
||||
gAnotherVoiceID = (VOICE_ID_t)Key;
|
||||
#endif
|
||||
gEeprom.FM_FrequencyPlaying = gEeprom.FM_SelectedFrequency;
|
||||
BK1080_SetFrequency(gEeprom.FM_FrequencyPlaying, gEeprom.FM_Band/*, gEeprom.FM_Space*/);
|
||||
FM_SetFrequency();
|
||||
gRequestSaveFM = true;
|
||||
return;
|
||||
}
|
||||
@@ -311,7 +320,7 @@ static void Key_DIGITS(KEY_Code_t Key, uint8_t state)
|
||||
#endif
|
||||
gEeprom.FM_SelectedChannel = Channel;
|
||||
gEeprom.FM_FrequencyPlaying = gFM_Channels[Channel];
|
||||
BK1080_SetFrequency(gEeprom.FM_FrequencyPlaying, gEeprom.FM_Band/*, gEeprom.FM_Space*/);
|
||||
FM_SetFrequency();
|
||||
gRequestSaveFM = true;
|
||||
return;
|
||||
}
|
||||
@@ -366,7 +375,7 @@ static void Key_FUNC(KEY_Code_t Key, uint8_t state)
|
||||
gEeprom.FM_IsMrMode = !gEeprom.FM_IsMrMode;
|
||||
|
||||
if (!FM_ConfigureChannelState()) {
|
||||
BK1080_SetFrequency(gEeprom.FM_FrequencyPlaying, gEeprom.FM_Band/*, gEeprom.FM_Space*/);
|
||||
FM_SetFrequency();
|
||||
gRequestSaveFM = true;
|
||||
}
|
||||
else
|
||||
@@ -480,7 +489,7 @@ static void Key_MENU(uint8_t state)
|
||||
gFM_Channels[gEeprom.FM_SelectedChannel] = 0xFFFF;
|
||||
|
||||
FM_ConfigureChannelState();
|
||||
BK1080_SetFrequency(gEeprom.FM_FrequencyPlaying, gEeprom.FM_Band/*, gEeprom.FM_Space*/);
|
||||
FM_SetFrequency();
|
||||
|
||||
gRequestSaveFM = true;
|
||||
}
|
||||
@@ -558,7 +567,7 @@ static void Key_UP_DOWN(uint8_t state, int8_t Step)
|
||||
gRequestSaveFM = true;
|
||||
|
||||
Bail:
|
||||
BK1080_SetFrequency(gEeprom.FM_FrequencyPlaying, gEeprom.FM_Band/*, gEeprom.FM_Space*/);
|
||||
FM_SetFrequency();
|
||||
|
||||
gRequestDisplayScreen = DISPLAY_FM;
|
||||
}
|
||||
@@ -612,19 +621,19 @@ void FM_Play(void)
|
||||
if (!gEeprom.FM_IsMrMode)
|
||||
gEeprom.FM_SelectedFrequency = gEeprom.FM_FrequencyPlaying;
|
||||
|
||||
BACKLIGHT_TurnOn();
|
||||
FM_AudioPathOn();
|
||||
|
||||
GUI_SelectNextDisplay(DISPLAY_FM);
|
||||
return;
|
||||
goto Display;
|
||||
}
|
||||
|
||||
if (gFM_ChannelPosition < FM_CHANNELS_MAX)
|
||||
gFM_Channels[gFM_ChannelPosition++] = gEeprom.FM_FrequencyPlaying;
|
||||
|
||||
|
||||
if (gFM_ChannelPosition >= FM_CHANNELS_MAX) {
|
||||
FM_PlayAndUpdate();
|
||||
GUI_SelectNextDisplay(DISPLAY_FM);
|
||||
return;
|
||||
|
||||
goto Display;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -633,6 +642,7 @@ void FM_Play(void)
|
||||
else
|
||||
FM_Tune(gEeprom.FM_FrequencyPlaying, gFM_ScanState, false);
|
||||
|
||||
Display:
|
||||
GUI_SelectNextDisplay(DISPLAY_FM);
|
||||
}
|
||||
|
||||
@@ -656,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
|
||||
|
||||
|
||||
+1504
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,66 @@
|
||||
/* 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 APP_FOXHUNT_H
|
||||
#define APP_FOXHUNT_H
|
||||
|
||||
#if defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON)
|
||||
|
||||
#include "keyboard_state.h"
|
||||
|
||||
#include "../bitmaps.h"
|
||||
#include "../board.h"
|
||||
#include "py32f0xx.h"
|
||||
#include "../driver/backlight.h"
|
||||
#include "../driver/bk4819-regs.h"
|
||||
#include "../driver/bk4819.h"
|
||||
#include "../driver/gpio.h"
|
||||
#include "../driver/keyboard.h"
|
||||
#include "../driver/py25q16.h"
|
||||
#include "../driver/st7565.h"
|
||||
#include "../driver/system.h"
|
||||
#include "../driver/systick.h"
|
||||
#include "../external/printf/printf.h"
|
||||
#include "../font.h"
|
||||
#include "../helper/battery.h"
|
||||
#include "../misc.h"
|
||||
#include "../radio.h"
|
||||
#include "../settings.h"
|
||||
#include "../ui/battery.h"
|
||||
#include "../ui/helper.h"
|
||||
#include "../ui/ui.h"
|
||||
#include "../audio.h"
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#endif
|
||||
|
||||
#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 // 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
|
||||
) {
|
||||
|
||||
+50
-13
@@ -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"
|
||||
@@ -45,6 +48,7 @@
|
||||
#include "settings.h"
|
||||
#include "ui/inputbox.h"
|
||||
#include "ui/main.h"
|
||||
#include "ui/menu.h"
|
||||
#include "ui/ui.h"
|
||||
#include <stdlib.h>
|
||||
|
||||
@@ -105,7 +109,7 @@ static void toggle_chan_scanlist(void)
|
||||
|
||||
gTxVfo->SCANLIST_PARTICIPATION = scanlist;
|
||||
|
||||
SETTINGS_UpdateChannel(gTxVfo->CHANNEL_SAVE, gTxVfo, true, true, true);
|
||||
SETTINGS_UpdateChannel(gTxVfo->CHANNEL_SAVE, gTxVfo, true);
|
||||
}
|
||||
|
||||
gVfoConfigureMode = VFO_CONFIGURE;
|
||||
@@ -225,6 +229,12 @@ static void processFKeyFunction(const KEY_Code_t Key, const bool beep)
|
||||
case KEY_4:
|
||||
HideFKeyIcon();
|
||||
|
||||
if (gScanStateDir != SCAN_OFF) {
|
||||
// Stop the channel/frequency scan before saving the RX mode for Close Call.
|
||||
gScanKeepResult = false;
|
||||
CHFRSCANNER_Stop();
|
||||
}
|
||||
|
||||
gBackup_CROSS_BAND_RX_TX = gEeprom.CROSS_BAND_RX_TX;
|
||||
gEeprom.CROSS_BAND_RX_TX = CROSS_BAND_OFF;
|
||||
|
||||
@@ -262,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
|
||||
@@ -272,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
|
||||
|
||||
@@ -317,7 +333,7 @@ static void processFKeyFunction(const KEY_Code_t Key, const bool beep)
|
||||
|
||||
if (gScanStateDir != SCAN_OFF) {
|
||||
RADIO_NextValidList(isKeyUp ? 1 : -1);
|
||||
UI_MAIN_NotifyScanProgressDataChanged();
|
||||
UI_MAIN_NotifyScanListChanged();
|
||||
} else {
|
||||
// Adjust squelch: UP increments, DOWN decrements
|
||||
if (gSquelchLevelOriginal == 10)
|
||||
@@ -448,6 +464,16 @@ static void MAIN_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
|
||||
gRequestDisplayScreen = DISPLAY_MAIN;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_ACTION_PICKER
|
||||
if (gWasFKeyPressed && (Key == KEY_SIDE1 || Key == KEY_SIDE2)) {
|
||||
gActionPickerKey = (Key == KEY_SIDE1) ? 1 : 2;
|
||||
gActionPickerTimeout_500ms = ACTION_PICKER_TIMEOUT_500MS;
|
||||
gUpdateDisplay = true;
|
||||
HideFKeyIcon();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
HideFKeyIcon();
|
||||
|
||||
processFKeyFunction(Key, true);
|
||||
@@ -500,7 +526,7 @@ static void MAIN_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
|
||||
if (value == 0)
|
||||
{
|
||||
gEeprom.SCAN_LIST_DEFAULT = MR_CHANNELS_LIST + 1;
|
||||
UI_MAIN_NotifyScanProgressDataChanged();
|
||||
UI_MAIN_NotifyScanListChanged();
|
||||
#ifdef ENABLE_FEAT_F4HWN_RESUME_STATE
|
||||
SETTINGS_WriteCurrentState();
|
||||
#endif
|
||||
@@ -519,7 +545,7 @@ static void MAIN_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
|
||||
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
|
||||
RADIO_NextValidList(1);
|
||||
}
|
||||
UI_MAIN_NotifyScanProgressDataChanged();
|
||||
UI_MAIN_NotifyScanListChanged();
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_RESUME_STATE
|
||||
SETTINGS_WriteCurrentState();
|
||||
@@ -702,7 +728,7 @@ static void MAIN_Key_EXIT(bool bKeyPressed, bool bKeyHeld)
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
if (!gFmRadioMode)
|
||||
#endif
|
||||
{
|
||||
@@ -737,7 +763,7 @@ static void MAIN_Key_EXIT(bool bKeyPressed, bool bKeyHeld)
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FMRADIO
|
||||
#ifdef ENABLE_FMRADIO_EMBEDDED
|
||||
ACTION_FM();
|
||||
#endif
|
||||
return;
|
||||
@@ -829,6 +855,11 @@ static void MAIN_Key_MENU(bool bKeyPressed, bool bKeyHeld)
|
||||
|
||||
gFlagRefreshSetting = true;
|
||||
gRequestDisplayScreen = DISPLAY_MENU;
|
||||
#ifdef ENABLE_FEAT_F4HWN_MENU_CAT
|
||||
gMenuLevel = MENU_LEVEL_CAT;
|
||||
UI_MENU_BuildCategoryScreen();
|
||||
gMenuCursor = (gMenuCatCursor < gMenuListCount) ? gMenuCatCursor : 0;
|
||||
#endif
|
||||
#ifdef ENABLE_VOICE
|
||||
gAnotherVoiceID = VOICE_ID_MENU;
|
||||
#endif
|
||||
@@ -918,6 +949,12 @@ static void MAIN_Key_STAR(bool bKeyPressed, bool bKeyHeld)
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (gScanStateDir != SCAN_OFF) {
|
||||
// Stop the channel/frequency scan before saving the RX mode for the CTCSS/DCS scan.
|
||||
gScanKeepResult = false;
|
||||
CHFRSCANNER_Stop();
|
||||
}
|
||||
|
||||
// scan the CTCSS/DCS code
|
||||
gBackup_CROSS_BAND_RX_TX = gEeprom.CROSS_BAND_RX_TX;
|
||||
gEeprom.CROSS_BAND_RX_TX = CROSS_BAND_OFF;
|
||||
@@ -1027,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;
|
||||
|
||||
+133
-50
@@ -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
|
||||
@@ -85,10 +89,10 @@ void MENU_StartCssScan(void)
|
||||
void MENU_CssScanFound(void)
|
||||
{
|
||||
if(gScanCssResultType == CODE_TYPE_DIGITAL || gScanCssResultType == CODE_TYPE_REVERSE_DIGITAL) {
|
||||
gMenuCursor = UI_MENU_GetMenuIdx(MENU_R_DCS);
|
||||
gMenuCursor = UI_MENU_GetViewPos(MENU_R_DCS);
|
||||
}
|
||||
else if(gScanCssResultType == CODE_TYPE_CONTINUOUS_TONE) {
|
||||
gMenuCursor = UI_MENU_GetMenuIdx(MENU_R_CTCS);
|
||||
gMenuCursor = UI_MENU_GetViewPos(MENU_R_CTCS);
|
||||
}
|
||||
|
||||
MENU_ShowCurrentSetting();
|
||||
@@ -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
|
||||
@@ -426,7 +425,7 @@ int MENU_GetLimits(uint8_t menu_id, int32_t *pMin, int32_t *pMax)
|
||||
#endif
|
||||
case MENU_SET_LCK:
|
||||
//*pMin = 0;
|
||||
*pMax = ARRAY_SIZE(gSubMenu_SET_LCK) - 1;
|
||||
*pMax = SET_LCK_LEN - 1;
|
||||
break;
|
||||
case MENU_SET_MET:
|
||||
case MENU_SET_GUI:
|
||||
@@ -725,7 +724,7 @@ void MENU_AcceptSetting(void)
|
||||
|
||||
case MENU_LIST_CH:
|
||||
gTxVfo->SCANLIST_PARTICIPATION = gSubMenuSelection;
|
||||
SETTINGS_UpdateChannel(gTxVfo->CHANNEL_SAVE, gTxVfo, true, false, true);
|
||||
SETTINGS_UpdateChannel(gTxVfo->CHANNEL_SAVE, gTxVfo, true);
|
||||
gVfoConfigureMode = VFO_CONFIGURE;
|
||||
gFlagResetVfos = true;
|
||||
return;
|
||||
@@ -752,7 +751,7 @@ void MENU_AcceptSetting(void)
|
||||
|
||||
case MENU_COMPAND:
|
||||
gTxVfo->Compander = gSubMenuSelection;
|
||||
SETTINGS_UpdateChannel(gTxVfo->CHANNEL_SAVE, gTxVfo, true, false, true);
|
||||
SETTINGS_UpdateChannel(gTxVfo->CHANNEL_SAVE, gTxVfo, true);
|
||||
gVfoConfigureMode = VFO_CONFIGURE;
|
||||
gFlagResetVfos = true;
|
||||
// gRequestSaveChannel = 1;
|
||||
@@ -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;
|
||||
@@ -864,7 +847,7 @@ void MENU_AcceptSetting(void)
|
||||
#endif
|
||||
|
||||
case MENU_DEL_CH:
|
||||
SETTINGS_UpdateChannel(gSubMenuSelection, NULL, false, false, true);
|
||||
SETTINGS_UpdateChannel(gSubMenuSelection, NULL, false);
|
||||
gVfoConfigureMode = VFO_CONFIGURE_RELOAD;
|
||||
gFlagResetVfos = true;
|
||||
return;
|
||||
@@ -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;
|
||||
@@ -1607,6 +1582,36 @@ static void MENU_Key_0_to_9(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
|
||||
|
||||
gRequestDisplayScreen = DISPLAY_MENU;
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_MENU_CAT
|
||||
if (gMenuLevel == MENU_LEVEL_CAT)
|
||||
{ // saut-par-numero global : depuis l'ecran categories, entre dans All a l'item N
|
||||
const uint8_t allCount = UI_MENU_CategoryItemCount(CAT_ALL);
|
||||
uint16_t value;
|
||||
|
||||
if (gInputBoxIndex >= 2) {
|
||||
gInputBoxIndex = 0;
|
||||
value = (gInputBox[0] * 10) + gInputBox[1];
|
||||
} else {
|
||||
value = gInputBox[0];
|
||||
}
|
||||
|
||||
if (value > 0 && value <= allCount)
|
||||
{
|
||||
gMenuCategory = CAT_ALL;
|
||||
gMenuCatCursor = gMenuListCount - 1; // All = derniere entree de gCatOrder
|
||||
UI_MENU_BuildView();
|
||||
gMenuLevel = MENU_LEVEL_ITEMS;
|
||||
gMenuCursor = value - 1;
|
||||
gFlagRefreshSetting = true;
|
||||
}
|
||||
else if (gInputBoxIndex == 0)
|
||||
{
|
||||
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!gIsInSubMenu)
|
||||
{
|
||||
switch (gInputBoxIndex)
|
||||
@@ -1821,6 +1826,22 @@ Skip:
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_MENU_CAT
|
||||
if (gMenuLevel == MENU_LEVEL_ITEMS)
|
||||
{ // remonter aux categories au lieu de quitter le menu
|
||||
gCatLastPos[gMenuCategory] = gMenuCursor; // memorise la position dans la categorie
|
||||
gMenuLevel = MENU_LEVEL_CAT;
|
||||
UI_MENU_BuildCategoryScreen();
|
||||
gMenuCursor = gMenuCatCursor;
|
||||
gRequestDisplayScreen = DISPLAY_MENU;
|
||||
#ifdef ENABLE_VOICE
|
||||
gAnotherVoiceID = VOICE_ID_CANCEL;
|
||||
#endif
|
||||
gPttWasReleased = true;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_VOICE
|
||||
gAnotherVoiceID = VOICE_ID_CANCEL;
|
||||
#endif
|
||||
@@ -1854,13 +1875,28 @@ static void MENU_Key_MENU(const bool bKeyPressed, const bool bKeyHeld)
|
||||
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
|
||||
gRequestDisplayScreen = DISPLAY_MENU;
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_MENU_CAT
|
||||
if (gMenuLevel == MENU_LEVEL_CAT)
|
||||
{ // niveau categories : MENU descend dans la categorie choisie
|
||||
gMenuCatCursor = gMenuCursor;
|
||||
gMenuCategory = gCatOrder[gMenuCursor];
|
||||
|
||||
UI_MENU_BuildView();
|
||||
gMenuLevel = MENU_LEVEL_ITEMS;
|
||||
gMenuCursor = (gCatLastPos[gMenuCategory] < gMenuListCount) ? gCatLastPos[gMenuCategory] : 0;
|
||||
gIsInSubMenu = false;
|
||||
gFlagRefreshSetting = true;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!gIsInSubMenu)
|
||||
{
|
||||
const int m = UI_MENU_GetCurrentMenuId();
|
||||
|
||||
#ifdef ENABLE_VOICE
|
||||
if (m != MENU_SCR)
|
||||
gAnotherVoiceID = MenuList[gMenuCursor].voice_id;
|
||||
gAnotherVoiceID = MenuList[gMenuIndices[gMenuCursor]].voice_id;
|
||||
#endif
|
||||
if (m == MENU_UPCODE
|
||||
|| m == MENU_DWCODE
|
||||
@@ -1945,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)
|
||||
@@ -1973,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;
|
||||
@@ -2060,6 +2133,16 @@ static void MENU_Key_UP_DOWN(bool bKeyPressed, bool bKeyHeld, int8_t Direction)
|
||||
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_MENU_CAT
|
||||
if (gMenuLevel == MENU_LEVEL_CAT)
|
||||
{ // niveau categories : deplacement simple du curseur
|
||||
gMenuCursor = NUMBER_AddWithWraparound(gMenuCursor, -Direction, 0, gMenuListCount - 1);
|
||||
gFlagRefreshSetting = true; // rearme le timeout menu (ShowCurrentSetting saute au niveau CAT)
|
||||
gRequestDisplayScreen = DISPLAY_MENU;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!gEeprom.SET_NAV && gIsInSubMenu) {
|
||||
Direction = -Direction;
|
||||
}
|
||||
|
||||
-187
@@ -1,187 +0,0 @@
|
||||
/*
|
||||
* REGA Alam and Test
|
||||
* ==================
|
||||
*
|
||||
* This bit of code is to implement the REGA test and alarm functions.
|
||||
* This function sends out a fixed ZVEI tone sequence for testing or alarm purposes.
|
||||
* Further information: https://www.rega.ch/en/our-missions/sites-and-infrastructure/emergency-radio
|
||||
*
|
||||
* There are two REGA ACTIONS: Test and Alarm.
|
||||
* The Test action sends out a fixed ZVEI tone sequence for testing purposes.
|
||||
* The Alarm action sends out a fixed ZVEI tone sequence for alarm purposes.
|
||||
*
|
||||
* These actions can be assigned to a key in the settings menu.
|
||||
*
|
||||
* The Test/Alarm function will perform the following actions:
|
||||
* - Set the radio to transmit mode
|
||||
* - Wait 100ms to allow the radio to switch to transmit mode and stabilize the tramsmitter
|
||||
* - Send out the ZVEI tone sequence
|
||||
* - Wait 100ms to allow the radio to finish transmitting
|
||||
* - Set the radio back to receive mode
|
||||
*
|
||||
* The ZVEI tone squence for alarm is: 21414
|
||||
* The ZVEI tone squence for test is: 21301
|
||||
*
|
||||
* To save space the two tone sequences are stored as precalculated register values.
|
||||
* This avoids the need to calculate the register values at runtime.
|
||||
* They are hardcoded as array of 32bit integers in the rega.h file.
|
||||
*
|
||||
* Copyright 2025 Markus Bärtschi
|
||||
* https://github.com/markusb
|
||||
*
|
||||
* 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 <stdio.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "rega.h"
|
||||
#include "radio.h"
|
||||
#include "action.h"
|
||||
#include "misc.h"
|
||||
#include "settings.h"
|
||||
#include "functions.h"
|
||||
#include "driver/bk4819.h"
|
||||
#include "driver/st7565.h"
|
||||
#include "driver/system.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "bsp/dp32g030/gpio.h"
|
||||
#include "ui/helper.h"
|
||||
#include "ui/ui.h"
|
||||
|
||||
|
||||
const uint16_t rega_alarm_tones[5] = {
|
||||
1160, // 2 1160Hz
|
||||
1060, // 1 1060Hz
|
||||
1400, // 4 1400Hz
|
||||
1060, // 1 1060Hz
|
||||
1400, // 4 1400Hz
|
||||
};
|
||||
const uint16_t rega_test_tones[5] = {
|
||||
1160, // 2 1160Hz
|
||||
1060, // 1 1060Hz
|
||||
1260, // 3 1260Hz
|
||||
2400, // 0 2400Hz
|
||||
1060, // 1 1060Hz
|
||||
};
|
||||
|
||||
// Global variable to pass the message to the display
|
||||
char rega_message[16];
|
||||
|
||||
// Transmit the ZVEI tone sequence for alarm
|
||||
void ACTION_RegaAlarm()
|
||||
{
|
||||
const char message[16] = "REGA Alarm";
|
||||
REGA_TransmitZvei(rega_alarm_tones,message);
|
||||
}
|
||||
|
||||
// Tranmit the ZVEI tone sequence for test
|
||||
void ACTION_RegaTest()
|
||||
{
|
||||
const char message[16] = "REGA Test";
|
||||
REGA_TransmitZvei(rega_test_tones,message);
|
||||
}
|
||||
|
||||
// Display the REGA message on the screen
|
||||
void UI_DisplayREGA()
|
||||
{
|
||||
UI_DisplayClear();
|
||||
UI_DisplayPopup(rega_message);
|
||||
ST7565_BlitFullScreen();
|
||||
}
|
||||
|
||||
// Transmit a ZVEI tone sequence on the REGA frequency
|
||||
// Configures the radio on VFO A with the required parameters
|
||||
// tones: array of 5 ZVEI tones
|
||||
// message: message to display on the screen
|
||||
void REGA_TransmitZvei(const uint16_t tones[],const char message[])
|
||||
{
|
||||
// Copy the message text
|
||||
strncpy(rega_message,message,16);
|
||||
// Trigger the display
|
||||
gScreenToDisplay = DISPLAY_REGA;
|
||||
|
||||
// Flash the green LED twice
|
||||
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, true);
|
||||
SYSTEM_DelayMs(70);
|
||||
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, false);
|
||||
SYSTEM_DelayMs(50);
|
||||
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, true);
|
||||
SYSTEM_DelayMs(70);
|
||||
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, false);
|
||||
|
||||
// Select VFO A
|
||||
gEeprom.TX_VFO = 0;
|
||||
gEeprom.ScreenChannel[gEeprom.TX_VFO] = gEeprom.FreqChannel[gEeprom.TX_VFO];
|
||||
gRxVfo = gTxVfo;
|
||||
gRequestSaveVFO = true;
|
||||
gVfoConfigureMode = VFO_CONFIGURE_RELOAD;
|
||||
|
||||
// Set the REGA frequency, no offset
|
||||
gTxVfo->freq_config_RX.Frequency = REGA_FREQUENCY;
|
||||
gTxVfo->freq_config_TX = gTxVfo->freq_config_RX;
|
||||
gTxVfo->TX_OFFSET_FREQUENCY = 0;
|
||||
|
||||
// Set the modulation to FM narrow
|
||||
gTxVfo->Modulation = MODULATION_FM;
|
||||
gTxVfo->CHANNEL_BANDWIDTH = BK4819_FILTER_BW_NARROW;
|
||||
|
||||
// Set Tx Squelch Tone
|
||||
gTxVfo->freq_config_TX.CodeType = CODE_TYPE_CONTINUOUS_TONE;
|
||||
gTxVfo->freq_config_TX.Code = REGA_CTCSS_FREQ_INDEX;
|
||||
BK4819_SetCTCSSFrequency(CTCSS_Options[gTxVfo->freq_config_TX.Code]);
|
||||
// Turn Rx squelch tone off
|
||||
gTxVfo->freq_config_RX.CodeType = CODE_TYPE_OFF;
|
||||
|
||||
// Set the transmit power to high
|
||||
gTxVfo->OUTPUT_POWER = OUTPUT_POWER_HIGH;
|
||||
|
||||
// Lock the keyboard
|
||||
gEeprom.KEY_LOCK = true;
|
||||
|
||||
gRequestSaveChannel = 1;
|
||||
gRequestSaveSettings = true;
|
||||
|
||||
// Configure the receiver
|
||||
BK4819_RX_TurnOn();
|
||||
|
||||
// Set the radio to transmit mode
|
||||
RADIO_PrepareCssTX();
|
||||
FUNCTION_Select(FUNCTION_TRANSMIT);
|
||||
|
||||
// Wait to allow the radio to switch to transmit mode and stabilize the transmitter
|
||||
SYSTEM_DelayMs(ZVEI_PRE_LENGTH_MS);
|
||||
|
||||
// Send out the ZVEI2 tone sequence
|
||||
for (int i = 0; i < ZVEI_NUM_TONES; i++)
|
||||
{
|
||||
BK4819_PlaySingleTone(tones[i], ZVEI_TONE_LENGTH_MS, 100, true);
|
||||
SYSTEM_DelayMs(ZVEI_PAUSE_LENGTH_MS);
|
||||
}
|
||||
|
||||
// Wait to allow the radio to finish transmitting
|
||||
SYSTEM_DelayMs(ZVEI_POST_LENGTH_MS);
|
||||
|
||||
// Flash the green LED twice
|
||||
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, true);
|
||||
SYSTEM_DelayMs(70);
|
||||
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, false);
|
||||
SYSTEM_DelayMs(70);
|
||||
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, true);
|
||||
SYSTEM_DelayMs(70);
|
||||
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, false);
|
||||
|
||||
// Display the normal screen
|
||||
gRequestDisplayScreen = DISPLAY_MAIN;
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
/*
|
||||
* REGA Alam and Test
|
||||
* ==================
|
||||
*
|
||||
* This function sends out a fixed ZVEI tone sequence for testing or alarm purposes for the Alpine REGA alarm channel.
|
||||
* Further information: https://www.rega.ch/en/our-missions/sites-and-infrastructure/emergency-radio
|
||||
*
|
||||
* There are two REGA ACTIONS: Test and Alarm.
|
||||
* The Test action sends out a fixed ZVEI tone sequence for testing purposes.
|
||||
* The Alarm action sends out a fixed ZVEI tone sequence for alarm purposes.
|
||||
*
|
||||
* These actions can be assigned to a key in the settings menu.
|
||||
*
|
||||
* The Test/Alarm function will perform the following actions:
|
||||
* - Set the radio configuration to the REGA frequency, CTCSS, FM, full power etc.
|
||||
* - Start transmitting
|
||||
* - Wait 100ms to allow the radio to switch to transmit mode and stabilize the transmitter
|
||||
* - Send out the ZVEI tone sequence
|
||||
* - Wait 100ms to allow the radio to finish transmitting
|
||||
* - Set the radio back to receive mode
|
||||
*
|
||||
* The REGA frequency is 161.300 Mhz
|
||||
*
|
||||
* The ZVEI tone squence for alarm is: 21414
|
||||
* The ZVEI tone squence for test is: 21301
|
||||
*
|
||||
* The two tone sequences are hardcoded in two arrays
|
||||
*
|
||||
* Copyright 2025 Markus Bärtschi
|
||||
*
|
||||
* https://github.com/markusb
|
||||
*
|
||||
*
|
||||
* 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 REGA_H
|
||||
#define REGA_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define ZVEI_NUM_TONES 5
|
||||
#define ZVEI_TONE_LENGTH_MS 70 // 70
|
||||
#define ZVEI_PAUSE_LENGTH_MS 10 // 10
|
||||
#define ZVEI_PRE_LENGTH_MS 300
|
||||
#define ZVEI_POST_LENGTH_MS 100
|
||||
#define REGA_CTCSS_FREQ_INDEX 18 // dcs.c: CTCSS_Options[18] = 1230
|
||||
#define REGA_FREQUENCY 16130000 // 43370000 16130000
|
||||
|
||||
void ACTION_RegaAlarm(void);
|
||||
void ACTION_RegaTest(void);
|
||||
void UI_DisplayREGA(void);
|
||||
void REGA_TransmitZvei(const uint16_t[], const char[]);
|
||||
|
||||
#endif
|
||||
+1376
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,89 @@
|
||||
/* Copyright 2026
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
*/
|
||||
|
||||
#ifndef APP_RXTX_LOG_H
|
||||
#define APP_RXTX_LOG_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "driver/keyboard.h"
|
||||
#include "functions.h"
|
||||
#include "radio.h"
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
|
||||
|
||||
#define RXTX_LOG_VISIBLE_COUNT 512
|
||||
|
||||
// Field order mirrors the leading fields of RXTX_LogFlashEntry_t
|
||||
// (rxtx_log.c) so both layouts match byte-for-byte up to and including
|
||||
// battVolt, copied in one pass. Scan-only fields (sequence) sit past the
|
||||
// copied prefix in the flash layout and are not cached in RAM.
|
||||
// The channel name is not stored in flash: it is resolved from `channel` when
|
||||
// exporting the K5Viewer packet.
|
||||
typedef struct {
|
||||
uint32_t frequency;
|
||||
uint32_t trafficSeq;
|
||||
uint16_t durationSeconds;
|
||||
uint16_t channel;
|
||||
uint8_t flags;
|
||||
uint8_t sMeter;
|
||||
uint8_t battVolt;
|
||||
} RXTX_LogEntry_t;
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
|
||||
#define RXTX_LOG_K5VIEWER_VERSION 2u
|
||||
#define RXTX_LOG_K5VIEWER_ROW_COUNT 64u
|
||||
#define RXTX_LOG_K5VIEWER_HISTORY_ROW_COUNT 64u
|
||||
#define RXTX_LOG_K5VIEWER_HISTORY_START 0xFFFFFFFFu
|
||||
#define RXTX_LOG_K5VIEWER_NAME_LENGTH 10u
|
||||
#define RXTX_LOG_K5VIEWER_STATUS_ACTIVE (1u << 0)
|
||||
#define RXTX_LOG_K5VIEWER_STATUS_HAS_TRAFFIC (1u << 1)
|
||||
#define RXTX_LOG_K5VIEWER_STATUS_CLEARING (1u << 2)
|
||||
#define RXTX_LOG_K5VIEWER_STATUS_DISABLED (1u << 3)
|
||||
#define RXTX_LOG_K5VIEWER_STATUS_PACKET_SIZE 4u
|
||||
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint32_t frequency;
|
||||
uint32_t trafficSeq;
|
||||
uint16_t durationSeconds;
|
||||
uint16_t channel;
|
||||
uint8_t flags;
|
||||
uint8_t meter;
|
||||
uint8_t battVolt;
|
||||
char channelName[RXTX_LOG_K5VIEWER_NAME_LENGTH];
|
||||
} RXTX_LogK5ViewerRow_t;
|
||||
|
||||
// On-wire packet layout: a disabled log sends only the 4-byte status header.
|
||||
// An enabled log continues with one live row and
|
||||
// RXTX_LOG_K5VIEWER_ROW_COUNT row slots, zero-padded past the last valid row.
|
||||
// The packet is streamed row by row and never built whole in RAM.
|
||||
#define RXTX_LOG_K5VIEWER_PACKET_SIZE (RXTX_LOG_K5VIEWER_STATUS_PACKET_SIZE + ((RXTX_LOG_K5VIEWER_ROW_COUNT + 1u) * sizeof(RXTX_LogK5ViewerRow_t)))
|
||||
#define RXTX_LOG_K5VIEWER_HISTORY_PACKET_SIZE (RXTX_LOG_K5VIEWER_HISTORY_ROW_COUNT * sizeof(RXTX_LogK5ViewerRow_t))
|
||||
|
||||
uint32_t RXTX_LOG_K5ViewerSignature(void);
|
||||
void RXTX_LOG_SendK5ViewerPacket(void (*send)(const uint8_t *data, uint16_t size));
|
||||
// Sends the page of history rows below `beforeSeq` (HISTORY_START begins
|
||||
// a dump below the live packet's coverage) and returns the bound for the
|
||||
// next page, 0 once the visible history is exhausted.
|
||||
uint32_t RXTX_LOG_SendK5ViewerHistoryPage(uint32_t beforeSeq, void (*send)(const uint8_t *data, uint16_t size));
|
||||
#endif
|
||||
|
||||
void RXTX_LOG_Init(void);
|
||||
bool RXTX_LOG_IsEnabled(void);
|
||||
void RXTX_LOG_BeginRx(const VFO_Info_t *vfo, FUNCTION_Type_t function);
|
||||
void RXTX_LOG_BeginTx(const VFO_Info_t *vfo);
|
||||
void RXTX_LOG_EndActive(void);
|
||||
void RXTX_LOG_Task10ms(void);
|
||||
void RXTX_LOG_Tick500ms(void);
|
||||
const char *RXTX_LOG_GetFilterName(void);
|
||||
|
||||
void ACTION_RxTxLog(void);
|
||||
void RXTX_LOG_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld);
|
||||
void UI_DisplayRxTxLog(void);
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
+110
-14
@@ -41,6 +41,101 @@ bool gScanUseCssResult;
|
||||
STEP_Setting_t stepSetting;
|
||||
uint8_t scanHitCount;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
SCAN_FREQ_VERIFY_OFF,
|
||||
SCAN_FREQ_VERIFY_FUNDAMENTAL,
|
||||
SCAN_FREQ_VERIFY_HARMONIC
|
||||
} SCAN_FrequencyVerifyState_t;
|
||||
|
||||
static SCAN_FrequencyVerifyState_t scanFreqVerifyState;
|
||||
static uint32_t scanVerifyFundamental;
|
||||
static uint32_t scanVerifyHarmonic;
|
||||
static uint16_t scanVerifyFundamentalRssi;
|
||||
|
||||
static bool SCANNER_ShouldVerifyVhfSecondHarmonic(const uint32_t frequency)
|
||||
{
|
||||
const uint32_t fundamental = frequency / 2;
|
||||
|
||||
return frequency >= (frequencyBandTable[BAND3_137MHz].lower * 2) &&
|
||||
frequency < (frequencyBandTable[BAND4_174MHz].lower * 2) &&
|
||||
fundamental >= frequencyBandTable[BAND3_137MHz].lower &&
|
||||
fundamental < frequencyBandTable[BAND4_174MHz].lower;
|
||||
}
|
||||
|
||||
static void SCANNER_StartCssScanAtFrequency(const uint32_t frequency)
|
||||
{
|
||||
gScanFrequency = frequency;
|
||||
gScanCssResultCode = 0xFF;
|
||||
gScanCssResultType = 0xFF;
|
||||
scanHitCount = 0;
|
||||
gScanUseCssResult = false;
|
||||
gScanProgressIndicator = 0;
|
||||
gScanCssState = SCAN_CSS_STATE_SCANNING;
|
||||
gScanDelay_10ms = scan_delay_10ms;
|
||||
|
||||
BK4819_SetScanFrequency(gScanFrequency);
|
||||
|
||||
if (!gCssBackgroundScan)
|
||||
GUI_SelectNextDisplay(DISPLAY_SCANNER);
|
||||
|
||||
gUpdateStatus = true;
|
||||
}
|
||||
|
||||
static void SCANNER_TuneForFrequencyVerification(const uint32_t frequency)
|
||||
{
|
||||
BK4819_SetFrequency(frequency);
|
||||
BK4819_PickRXFilterPathBasedOnFrequency(frequency);
|
||||
BK4819_RX_TurnOn();
|
||||
}
|
||||
|
||||
static uint16_t SCANNER_ReadVerificationRssi(void)
|
||||
{
|
||||
(void)BK4819_GetRSSI();
|
||||
return BK4819_GetRSSI();
|
||||
}
|
||||
|
||||
static void SCANNER_StartFrequencyVerification(const uint32_t harmonic)
|
||||
{
|
||||
scanVerifyFundamental = harmonic / 2;
|
||||
scanVerifyHarmonic = harmonic;
|
||||
scanFreqVerifyState = SCAN_FREQ_VERIFY_FUNDAMENTAL;
|
||||
|
||||
SCANNER_TuneForFrequencyVerification(scanVerifyFundamental);
|
||||
gScanDelay_10ms = 2;
|
||||
}
|
||||
|
||||
static bool SCANNER_HandleFrequencyVerification(void)
|
||||
{
|
||||
switch (scanFreqVerifyState) {
|
||||
case SCAN_FREQ_VERIFY_FUNDAMENTAL:
|
||||
scanVerifyFundamentalRssi = SCANNER_ReadVerificationRssi();
|
||||
scanFreqVerifyState = SCAN_FREQ_VERIFY_HARMONIC;
|
||||
SCANNER_TuneForFrequencyVerification(scanVerifyHarmonic);
|
||||
gScanDelay_10ms = 2;
|
||||
return true;
|
||||
|
||||
case SCAN_FREQ_VERIFY_HARMONIC: {
|
||||
const uint16_t harmonicRssi = SCANNER_ReadVerificationRssi();
|
||||
const uint16_t margin = 4;
|
||||
uint32_t verifiedFrequency = scanVerifyHarmonic;
|
||||
|
||||
if (scanVerifyFundamentalRssi > harmonicRssi &&
|
||||
scanVerifyFundamentalRssi - harmonicRssi >= margin)
|
||||
{
|
||||
verifiedFrequency = scanVerifyFundamental;
|
||||
}
|
||||
|
||||
scanFreqVerifyState = SCAN_FREQ_VERIFY_OFF;
|
||||
SCANNER_StartCssScanAtFrequency(verifiedFrequency);
|
||||
return true;
|
||||
}
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static void SCANNER_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
|
||||
{
|
||||
if (!bKeyHeld && bKeyPressed)
|
||||
@@ -341,7 +436,8 @@ void SCANNER_Start(bool singleFreq)
|
||||
gScanCssState = SCAN_CSS_STATE_OFF;
|
||||
gScanFrequency = 0xFFFFFFFF;
|
||||
|
||||
BK4819_PickRXFilterPathBasedOnFrequency(gScanFrequency);
|
||||
// Keep the RX input path selected by RADIO_SetupRegisters().
|
||||
// 0xFFFFFFFF means "unknown frequency" here and would turn both LNAs off.
|
||||
BK4819_SetFrequencyScan(true);
|
||||
|
||||
gUpdateStatus = true;
|
||||
@@ -366,6 +462,7 @@ void SCANNER_Start(bool singleFreq)
|
||||
g_SquelchLost = false;
|
||||
gScannerSaveState = SCAN_SAVE_NO_PROMPT;
|
||||
gScanProgressIndicator = 0;
|
||||
scanFreqVerifyState = SCAN_FREQ_VERIFY_OFF;
|
||||
}
|
||||
|
||||
void SCANNER_Stop(void)
|
||||
@@ -377,6 +474,7 @@ void SCANNER_Stop(void)
|
||||
gUpdateStatus = true;
|
||||
gCssBackgroundScan = false;
|
||||
gScanUseCssResult = false;
|
||||
scanFreqVerifyState = SCAN_FREQ_VERIFY_OFF;
|
||||
#ifdef ENABLE_VOICE
|
||||
gAnotherVoiceID = VOICE_ID_CANCEL;
|
||||
#endif
|
||||
@@ -398,6 +496,10 @@ void SCANNER_TimeSlice10ms(void)
|
||||
return;
|
||||
}
|
||||
|
||||
if (SCANNER_HandleFrequencyVerification()) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (gScanCssState) {
|
||||
case SCAN_CSS_STATE_OFF: {
|
||||
// must be RF frequency scanning if we're here ?
|
||||
@@ -420,22 +522,16 @@ void SCANNER_TimeSlice10ms(void)
|
||||
if (scanHitCount < 3) {
|
||||
BK4819_SetFrequencyScan(true);
|
||||
}
|
||||
else if (SCANNER_ShouldVerifyVhfSecondHarmonic(gScanFrequency)) {
|
||||
SCANNER_StartFrequencyVerification(gScanFrequency);
|
||||
}
|
||||
else {
|
||||
BK4819_SetScanFrequency(gScanFrequency);
|
||||
gScanCssResultCode = 0xFF;
|
||||
gScanCssResultType = 0xFF;
|
||||
scanHitCount = 0;
|
||||
gScanUseCssResult = false;
|
||||
gScanProgressIndicator = 0;
|
||||
gScanCssState = SCAN_CSS_STATE_SCANNING;
|
||||
|
||||
if(!gCssBackgroundScan)
|
||||
GUI_SelectNextDisplay(DISPLAY_SCANNER);
|
||||
|
||||
gUpdateStatus = true;
|
||||
SCANNER_StartCssScanAtFrequency(gScanFrequency);
|
||||
}
|
||||
|
||||
gScanDelay_10ms = scan_delay_10ms;
|
||||
if (scanFreqVerifyState == SCAN_FREQ_VERIFY_OFF) {
|
||||
gScanDelay_10ms = scan_delay_10ms;
|
||||
}
|
||||
//gScanDelay_10ms = 1; // 10ms
|
||||
break;
|
||||
}
|
||||
|
||||
+28
-21
@@ -14,10 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
#include "app/spectrum.h"
|
||||
#include "am_fix.h"
|
||||
#include "audio.h"
|
||||
#include "misc.h"
|
||||
|
||||
#if defined(ENABLE_UART) || defined(ENABLE_USB)
|
||||
#include "app/uart.h"
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_SCAN_RANGES
|
||||
#include "chFrScanner.h"
|
||||
#endif
|
||||
@@ -27,8 +30,8 @@
|
||||
#include "ui/helper.h"
|
||||
#include "ui/main.h"
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
|
||||
#include "screenshot.h"
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
#include "k5viewer.h"
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_SPECTRUM
|
||||
@@ -73,7 +76,7 @@ static uint16_t blacklistFreqs[15];
|
||||
static uint8_t blacklistFreqsIdx;
|
||||
#endif
|
||||
|
||||
const char *bwOptions[] = {"25", "12.5", "6.25"};
|
||||
const char *const bwOptions[] = {"25", "12.5", "6.25"};
|
||||
const uint8_t modulationTypeTuneSteps[] = {100, 50, 10};
|
||||
const uint8_t modTypeReg47Values[] = {1, 7, 5};
|
||||
|
||||
@@ -176,7 +179,7 @@ char freqInputString[11];
|
||||
uint8_t menuState = 0;
|
||||
uint16_t listenT = 0;
|
||||
|
||||
RegisterSpec registerSpecs[] = {
|
||||
const RegisterSpec registerSpecs[] = {
|
||||
{},
|
||||
{"LNAs", BK4819_REG_13, 8, 0b11, 1},
|
||||
{"LNA", BK4819_REG_13, 5, 0b111, 1},
|
||||
@@ -433,8 +436,15 @@ static void ToggleAFDAC(bool on)
|
||||
BK4819_WriteRegister(BK4819_REG_30, Reg);
|
||||
}
|
||||
|
||||
static uint32_t NormalizeScanFrequency(uint32_t f)
|
||||
{
|
||||
const uint16_t step = scanStepValues[settings.scanStepIndex];
|
||||
return (step == 833) ? FREQUENCY_RoundToStep(f, step) : f;
|
||||
}
|
||||
|
||||
static void SetF(uint32_t f)
|
||||
{
|
||||
f = NormalizeScanFrequency(f);
|
||||
fMeasure = f;
|
||||
|
||||
BK4819_SetFrequency(fMeasure);
|
||||
@@ -451,6 +461,8 @@ static void SetF(uint32_t f)
|
||||
// cutting the SPI bus activity that causes SPI-induced audio interference.
|
||||
static void SetFScan(uint32_t f)
|
||||
{
|
||||
f = NormalizeScanFrequency(f);
|
||||
|
||||
// Refresh RF path only when crossing the VHF/UHF boundary (280 MHz)
|
||||
if ((f < 28000000) != (fMeasure < 28000000))
|
||||
BK4819_PickRXFilterPathBasedOnFrequency(f);
|
||||
@@ -595,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;
|
||||
}
|
||||
|
||||
@@ -756,7 +764,7 @@ static void UpdateScanInfo()
|
||||
if (scanInfo.rssi > scanInfo.rssiMax)
|
||||
{
|
||||
scanInfo.rssiMax = scanInfo.rssi;
|
||||
scanInfo.fPeak = scanInfo.f;
|
||||
scanInfo.fPeak = NormalizeScanFrequency(scanInfo.f);
|
||||
scanInfo.iPeak = scanInfo.i;
|
||||
}
|
||||
|
||||
@@ -1593,6 +1601,7 @@ static void ShowChannelName(uint32_t f)
|
||||
{
|
||||
static uint32_t channelF = 0;
|
||||
static char channelName[12];
|
||||
f = NormalizeScanFrequency(f);
|
||||
|
||||
// Channel name starts at x=43 (fixed), leaving room for the dBm
|
||||
// string on the left (max ~40 px) and battery indicator at x=116.
|
||||
@@ -1632,6 +1641,7 @@ static void FormatFrequency(uint32_t freq, char *buffer) {
|
||||
|
||||
static void DrawF(uint32_t f)
|
||||
{
|
||||
f = NormalizeScanFrequency(f);
|
||||
FormatFrequency(f, String);
|
||||
// Align frequency with channel name in status bar (both at x=43).
|
||||
// Left-aligned (End == Start = 43) so it does not collide with BW at x=108.
|
||||
@@ -2450,21 +2460,15 @@ static void UpdateListening()
|
||||
|
||||
static void Tick()
|
||||
{
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
// Parse incoming packets on every tick so serial keys are never missed,
|
||||
// regardless of whether the screen needs redrawing.
|
||||
SCREENSHOT_ParseInput();
|
||||
K5VIEWER_ParseInput();
|
||||
#endif
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -2521,9 +2525,9 @@ static void Tick()
|
||||
if (redrawScreen || ++renderTimer >= RENDER_PERIOD_TICKS)
|
||||
{
|
||||
Render();
|
||||
// For screenshot
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
|
||||
SCREENSHOT_Update(false);
|
||||
// For K5Viewer
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
K5VIEWER_Update(false);
|
||||
#endif
|
||||
redrawScreen = false;
|
||||
renderTimer = 0;
|
||||
@@ -2597,6 +2601,9 @@ void APP_RunSpectrum()
|
||||
|
||||
while (isInitialized)
|
||||
{
|
||||
#if defined(ENABLE_UART) || defined(ENABLE_USB)
|
||||
UART_ServiceCommands();
|
||||
#endif
|
||||
Tick();
|
||||
}
|
||||
|
||||
|
||||
+332
-29
@@ -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);
|
||||
@@ -863,7 +1153,20 @@ void UART_HandleCommand(uint32_t Port)
|
||||
#endif
|
||||
} // switch
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
|
||||
gUART_LockScreenshot = 20; // lock screenshot
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
gUART_LockK5Viewer = 20; // lock the K5Viewer stream
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void UART_ServiceCommands(void)
|
||||
{
|
||||
#ifdef ENABLE_USB
|
||||
if (UART_IsCommandAvailable(UART_PORT_VCP))
|
||||
UART_HandleCommand(UART_PORT_VCP);
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_UART
|
||||
if (UART_IsCommandAvailable(UART_PORT_UART))
|
||||
UART_HandleCommand(UART_PORT_UART);
|
||||
#endif
|
||||
}
|
||||
+1
-1
@@ -32,6 +32,6 @@ enum
|
||||
|
||||
bool UART_IsCommandAvailable(uint32_t Port);
|
||||
void UART_HandleCommand(uint32_t Port);
|
||||
void UART_ServiceCommands(void);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -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
|
||||
|
||||
+153
-8
@@ -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] =
|
||||
@@ -57,6 +58,21 @@ const uint8_t gFontLight[9] =
|
||||
0b00001100,
|
||||
};
|
||||
|
||||
// Same bulb outline as gFontLight with the inner filament removed,
|
||||
// used when the manual backlight is currently off
|
||||
const uint8_t gFontLightOff[9] =
|
||||
{
|
||||
0b00001100,
|
||||
0b00010010,
|
||||
0b00100001,
|
||||
0b01100001,
|
||||
0b01100001,
|
||||
0b01100001,
|
||||
0b00100001,
|
||||
0b00010010,
|
||||
0b00001100,
|
||||
};
|
||||
|
||||
const uint8_t gFontMute[12] =
|
||||
{
|
||||
0b00011100,
|
||||
@@ -286,6 +302,135 @@ const uint8_t BITMAP_NOAA[12] =
|
||||
};
|
||||
#endif
|
||||
|
||||
#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,
|
||||
0b00011100,
|
||||
0b00011100,
|
||||
0b00001000,
|
||||
0b00000000,
|
||||
0b00100010,
|
||||
0b00011100,
|
||||
0b01000001,
|
||||
0b00100010,
|
||||
0b00011100
|
||||
};
|
||||
|
||||
const uint8_t BITMAP_FoxHuntSpeaker[10] =
|
||||
{ // speaker cone + two sound waves (bit0 = top row)
|
||||
0b00011100,
|
||||
0b00011100,
|
||||
0b00111110,
|
||||
0b01111111,
|
||||
0b00000000,
|
||||
0b00100010,
|
||||
0b00011100,
|
||||
0b01000001,
|
||||
0b00100010,
|
||||
0b00011100
|
||||
};
|
||||
|
||||
const uint8_t BITMAP_FoxHuntUp[11] =
|
||||
{ // filled up triangle (getting nearer)
|
||||
0b00100000,
|
||||
0b00110000,
|
||||
0b00111000,
|
||||
0b00111100,
|
||||
0b00111110,
|
||||
0b00111111,
|
||||
0b00111110,
|
||||
0b00111100,
|
||||
0b00111000,
|
||||
0b00110000,
|
||||
0b00100000
|
||||
};
|
||||
|
||||
const uint8_t BITMAP_FoxHuntDown[11] =
|
||||
{ // filled down triangle (getting farther)
|
||||
0b00000001,
|
||||
0b00000011,
|
||||
0b00000111,
|
||||
0b00001111,
|
||||
0b00011111,
|
||||
0b00111111,
|
||||
0b00011111,
|
||||
0b00001111,
|
||||
0b00000111,
|
||||
0b00000011,
|
||||
0b00000001
|
||||
};
|
||||
|
||||
const uint8_t BITMAP_FoxHuntFlat[11] =
|
||||
{ // equals sign (stable)
|
||||
0b00010010,
|
||||
0b00010010,
|
||||
0b00010010,
|
||||
0b00010010,
|
||||
0b00010010,
|
||||
0b00010010,
|
||||
0b00010010,
|
||||
0b00010010,
|
||||
0b00010010,
|
||||
0b00010010,
|
||||
0b00010010
|
||||
};
|
||||
|
||||
const uint8_t BITMAP_FoxHuntBars[11] =
|
||||
{ // ascending bars = S-meter (staircase) gauge mode (bit0 = top row)
|
||||
0b01000000,
|
||||
0b01000000,
|
||||
0b00000000,
|
||||
0b01110000,
|
||||
0b01110000,
|
||||
0b00000000,
|
||||
0b01111100,
|
||||
0b01111100,
|
||||
0b00000000,
|
||||
0b01111111,
|
||||
0b01111111
|
||||
};
|
||||
|
||||
const uint8_t BITMAP_FoxHuntGraph[15] =
|
||||
{ // sine wave = signal-history gauge mode (bit0 = top row)
|
||||
0b00001000,
|
||||
0b00000100,
|
||||
0b00000010,
|
||||
0b00000100,
|
||||
0b00001000,
|
||||
0b00010000,
|
||||
0b00100000,
|
||||
0b00010000,
|
||||
0b00001000,
|
||||
0b00000100,
|
||||
0b00000010,
|
||||
0b00000100,
|
||||
0b00001000,
|
||||
0b00010000,
|
||||
0b00100000
|
||||
};
|
||||
|
||||
const uint8_t BITMAP_FoxHuntTx[16] =
|
||||
{
|
||||
0b00011100,
|
||||
0b00100010,
|
||||
0b01000001,
|
||||
0b00011100,
|
||||
0b00100010,
|
||||
0b00000000,
|
||||
0b00001000,
|
||||
0b00011100,
|
||||
0b00011100,
|
||||
0b00001000,
|
||||
0b00000000,
|
||||
0b00100010,
|
||||
0b00011100,
|
||||
0b01000001,
|
||||
0b00100010,
|
||||
0b00011100
|
||||
};
|
||||
#endif
|
||||
|
||||
#ifndef ENABLE_CUSTOM_MENU_LAYOUT
|
||||
const uint8_t BITMAP_CurrentIndicator[8] = {
|
||||
0xFF,
|
||||
|
||||
+13
-1
@@ -7,11 +7,12 @@
|
||||
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];
|
||||
extern const uint8_t gFontLight[9];
|
||||
extern const uint8_t gFontLightOff[9];
|
||||
extern const uint8_t gFontMute[12];
|
||||
|
||||
extern const uint8_t gFontXB[2][6];
|
||||
@@ -44,6 +45,17 @@ extern const uint8_t BITMAP_compand[6];
|
||||
|
||||
extern const uint8_t BITMAP_NOAA[12];
|
||||
|
||||
#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];
|
||||
extern const uint8_t BITMAP_FoxHuntDown[11];
|
||||
extern const uint8_t BITMAP_FoxHuntFlat[11];
|
||||
extern const uint8_t BITMAP_FoxHuntBars[11];
|
||||
extern const uint8_t BITMAP_FoxHuntGraph[15];
|
||||
extern const uint8_t BITMAP_FoxHuntTx[16];
|
||||
#endif
|
||||
|
||||
#ifndef ENABLE_CUSTOM_MENU_LAYOUT
|
||||
extern const uint8_t BITMAP_CurrentIndicator[8];
|
||||
#endif
|
||||
|
||||
+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)
|
||||
{
|
||||
|
||||
+55
-15
@@ -32,7 +32,9 @@
|
||||
#endif
|
||||
|
||||
#define PWM_FREQ 4000
|
||||
#define DUTY_CYCLE_LEVELS 64
|
||||
// 32 levels keep every step of the value[] table distinct after
|
||||
// quantization while halving the DMA duty cycle buffer (128 bytes saved)
|
||||
#define DUTY_CYCLE_LEVELS 32
|
||||
|
||||
#define DUTY_CYCLE_ON_VALUE GPIO_PIN_MASK(GPIO_PIN_BACKLIGHT)
|
||||
#define DUTY_CYCLE_OFF_VALUE (DUTY_CYCLE_ON_VALUE << 16)
|
||||
@@ -52,12 +54,25 @@ static int16_t currentBrightness = 0;
|
||||
static int16_t targetBrightness = 0;
|
||||
static int16_t fadeStep = 0;
|
||||
|
||||
static void BACKLIGHT_SetHardwareBrightness(uint8_t brightness);
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN
|
||||
// Power-on fade-in tuning.
|
||||
// STEPS = number of intermediate brightness stops (smoothness).
|
||||
// STEP_MS = pacing between stops.
|
||||
// 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
|
||||
const uint8_t value[] = {
|
||||
0, // 0 off
|
||||
8, // 1 visible in the dark
|
||||
14, // 2
|
||||
22, // 3
|
||||
16, // 2
|
||||
24, // 3
|
||||
32, // 4
|
||||
48, // 5
|
||||
72, // 6
|
||||
@@ -128,6 +143,41 @@ void BACKLIGHT_UpdateTickless(void) {
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN
|
||||
// Soft, progressive power-on fade-in.
|
||||
// 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 = 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;
|
||||
return;
|
||||
}
|
||||
|
||||
for (uint16_t s = 1; s < BL_STARTUP_FADE_STEPS; s++) {
|
||||
// x in Q8 (0..256), e = smoothstep(x) in Q16 (0..65536)
|
||||
uint32_t x = (uint32_t)s * 256 / BL_STARTUP_FADE_STEPS;
|
||||
uint32_t e = (x * x * (768 - 2 * x)) >> 8;
|
||||
currentBrightness = from + (int16_t)(((uint32_t)span * e) >> 16);
|
||||
BACKLIGHT_SetHardwareBrightness((uint8_t)currentBrightness);
|
||||
SYSTEM_DelayMs(BL_STARTUP_FADE_STEP_MS);
|
||||
}
|
||||
|
||||
currentBrightness = targetBrightness;
|
||||
BACKLIGHT_SetHardwareBrightness((uint8_t)currentBrightness);
|
||||
gUpdateBacklight = false;
|
||||
}
|
||||
#endif
|
||||
|
||||
void BACKLIGHT_TurnOn(void)
|
||||
{
|
||||
#ifdef ENABLE_FEAT_F4HWN_SLEEP
|
||||
@@ -161,10 +211,11 @@ void BACKLIGHT_TurnOn(void)
|
||||
if(startup)
|
||||
#endif
|
||||
{
|
||||
BACKLIGHT_UpdateTickless();
|
||||
#ifdef ENABLE_FEAT_F4HWN
|
||||
BACKLIGHT_FadeInStartup();
|
||||
BACKLIGHT_Sound();
|
||||
#else
|
||||
BACKLIGHT_UpdateTickless();
|
||||
startup = false;
|
||||
#endif
|
||||
}
|
||||
@@ -182,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();
|
||||
|
||||
+22
-6
@@ -411,6 +411,11 @@ void BK4819_ToggleGpioOut(BK4819_GPIO_PIN_t Pin, bool bSet)
|
||||
BK4819_WriteRegister(BK4819_REG_33, gBK4819_GpioOutState);
|
||||
}
|
||||
|
||||
bool BK4819_IsGpioOutSet(BK4819_GPIO_PIN_t Pin)
|
||||
{
|
||||
return (gBK4819_GpioOutState & (0x40u >> Pin)) != 0;
|
||||
}
|
||||
|
||||
void BK4819_SetCDCSSCodeWord(uint32_t CodeWord)
|
||||
{
|
||||
// REG_51
|
||||
@@ -1090,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
|
||||
@@ -1719,7 +1724,7 @@ void BK4819_PrepareFSKReceive(void)
|
||||
BK4819_WriteRegister(BK4819_REG_59, 0x3068);
|
||||
}
|
||||
|
||||
static void BK4819_PlayRogerNormal(void)
|
||||
static void BK4819_PlayRogerNormal(BK4819_FilterBandwidth_t Bandwidth)
|
||||
{
|
||||
#if 0
|
||||
const uint32_t tone1_Hz = 500;
|
||||
@@ -1734,12 +1739,19 @@ static void BK4819_PlayRogerNormal(void)
|
||||
BK4819_EnterTxMute();
|
||||
BK4819_SetAF(BK4819_AF_MUTE);
|
||||
|
||||
BK4819_WriteRegister(BK4819_REG_70, BK4819_REG_70_ENABLE_TONE1 | (66u << BK4819_REG_70_SHIFT_TONE1_TUNING_GAIN));
|
||||
const uint8_t rogerToneGain = (Bandwidth == BK4819_FILTER_BW_WIDE) ? 32u : 66u;
|
||||
|
||||
if (Bandwidth == BK4819_FILTER_BW_WIDE)
|
||||
BK4819_SetFilterBandwidth(BK4819_FILTER_BW_NARROW, true);
|
||||
|
||||
BK4819_WriteRegister(BK4819_REG_71, scale_freq(tone1_Hz));
|
||||
|
||||
BK4819_EnableTXLink();
|
||||
SYSTEM_DelayMs(50);
|
||||
|
||||
BK4819_WriteRegister(BK4819_REG_71, scale_freq(tone1_Hz));
|
||||
BK4819_WriteRegister(BK4819_REG_70,
|
||||
BK4819_REG_70_ENABLE_TONE1 |
|
||||
(rogerToneGain << BK4819_REG_70_SHIFT_TONE1_TUNING_GAIN));
|
||||
|
||||
BK4819_ExitTxMute();
|
||||
SYSTEM_DelayMs(80);
|
||||
@@ -1752,6 +1764,10 @@ static void BK4819_PlayRogerNormal(void)
|
||||
BK4819_EnterTxMute();
|
||||
|
||||
BK4819_WriteRegister(BK4819_REG_70, 0x0000);
|
||||
|
||||
if (Bandwidth == BK4819_FILTER_BW_WIDE)
|
||||
BK4819_SetFilterBandwidth(Bandwidth, true);
|
||||
|
||||
BK4819_WriteRegister(BK4819_REG_30, 0xC1FE); // 1 1 0000 0 1 1111 1 1 1 0
|
||||
}
|
||||
|
||||
@@ -1804,10 +1820,10 @@ void BK4819_PlayRogerMDC(void)
|
||||
BK4819_WriteRegister(BK4819_REG_58, 0x0000);
|
||||
}
|
||||
|
||||
void BK4819_PlayRoger(void)
|
||||
void BK4819_PlayRoger(BK4819_FilterBandwidth_t Bandwidth)
|
||||
{
|
||||
if (gEeprom.ROGER == ROGER_MODE_ROGER) {
|
||||
BK4819_PlayRogerNormal();
|
||||
BK4819_PlayRogerNormal(Bandwidth);
|
||||
} else if (gEeprom.ROGER == ROGER_MODE_MDC) {
|
||||
BK4819_PlayRogerMDC();
|
||||
}
|
||||
|
||||
+3
-2
@@ -77,6 +77,7 @@ void BK4819_SetAGC(bool enable);
|
||||
void BK4819_InitAGC(bool amModulation);
|
||||
|
||||
void BK4819_ToggleGpioOut(BK4819_GPIO_PIN_t Pin, bool bSet);
|
||||
bool BK4819_IsGpioOutSet(BK4819_GPIO_PIN_t Pin);
|
||||
|
||||
void BK4819_SetCDCSSCodeWord(uint32_t CodeWord);
|
||||
void BK4819_SetCTCSSFrequency(uint32_t BaudRate);
|
||||
@@ -113,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);
|
||||
@@ -169,7 +170,7 @@ uint8_t BK4819_GetCTCType(void);
|
||||
void BK4819_SendFSKData(uint16_t *pData);
|
||||
void BK4819_PrepareFSKReceive(void);
|
||||
|
||||
void BK4819_PlayRoger(void);
|
||||
void BK4819_PlayRoger(BK4819_FilterBandwidth_t Bandwidth);
|
||||
|
||||
void BK4819_Enable_AfDac_DiscMode_TxDsp(void);
|
||||
|
||||
|
||||
+31
-12
@@ -441,6 +441,11 @@ void BK4819_ToggleGpioOut(BK4819_GPIO_PIN_t Pin, bool bSet)
|
||||
BK4819_WriteRegister(BK4819_REG_33, gBK4819_GpioOutState);
|
||||
}
|
||||
|
||||
bool BK4819_IsGpioOutSet(BK4819_GPIO_PIN_t Pin)
|
||||
{
|
||||
return (gBK4819_GpioOutState & (0x40u >> Pin)) != 0;
|
||||
}
|
||||
|
||||
void BK4819_SetCDCSSCodeWord(uint32_t CodeWord)
|
||||
{
|
||||
// REG_51
|
||||
@@ -696,11 +701,11 @@ void BK4819_SetFilterBandwidth(const BK4819_FilterBandwidth_t Bandwidth, const b
|
||||
break;
|
||||
|
||||
case BK4819_FILTER_BW_NARROWER: // 6.25kHz
|
||||
// 0x205C = (0b010 << 12) | (0b000 << 9) | (0b001 << 6) |
|
||||
// (0b01 << 4) | (1 << 3) | (1 << 2)
|
||||
// 0x2058 = (0b010 << 12) | (0b000 << 9) | (0b001 << 6) |
|
||||
// (0b01 << 4) | (1 << 3)
|
||||
// = RF 3.0 kHz, weak-RF 2.0 kHz, AF Tx LPF2 2.5 kHz,
|
||||
// 6.25k mode, FM gain +6 dB.
|
||||
val = 0x205C;
|
||||
// 6.25k mode, FM gain 0 dB.
|
||||
val = 0x2058;
|
||||
break;
|
||||
|
||||
case BK4819_FILTER_BW_AM: // Stock AM preset
|
||||
@@ -1128,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
|
||||
@@ -1754,7 +1759,7 @@ void BK4819_PrepareFSKReceive(void)
|
||||
BK4819_WriteRegister(BK4819_REG_59, 0x3068);
|
||||
}
|
||||
|
||||
static void BK4819_PlayRogerNormal(void)
|
||||
static void BK4819_PlayRogerNormal(BK4819_FilterBandwidth_t Bandwidth)
|
||||
{
|
||||
#if 0
|
||||
const uint32_t tone1_Hz = 500;
|
||||
@@ -1769,17 +1774,31 @@ static void BK4819_PlayRogerNormal(void)
|
||||
BK4819_EnterTxMute();
|
||||
BK4819_SetAF(BK4819_AF_MUTE);
|
||||
|
||||
BK4819_WriteRegister(BK4819_REG_70, // BK4819_REG_70_ENABLE_TONE1 | (66u << BK4819_REG_70_SHIFT_TONE1_TUNING_GAIN));
|
||||
0xC300
|
||||
);
|
||||
const uint8_t rogerToneGain = (Bandwidth == BK4819_FILTER_BW_WIDE) ? 32u : 67u;
|
||||
|
||||
if (Bandwidth == BK4819_FILTER_BW_WIDE)
|
||||
BK4819_SetFilterBandwidth(BK4819_FILTER_BW_NARROW, true);
|
||||
|
||||
BK4819_WriteRegister(BK4819_REG_71, scale_freq(tone1_Hz));
|
||||
|
||||
BK4819_EnableTXLink();
|
||||
SYSTEM_DelayMs(50);
|
||||
|
||||
BK4819_PlayToneRaw(tone1_Hz, 80);
|
||||
BK4819_WriteRegister(BK4819_REG_70,
|
||||
BK4819_REG_70_ENABLE_TONE1 |
|
||||
(rogerToneGain << BK4819_REG_70_SHIFT_TONE1_TUNING_GAIN));
|
||||
|
||||
BK4819_ExitTxMute();
|
||||
SYSTEM_DelayMs(80);
|
||||
BK4819_EnterTxMute();
|
||||
|
||||
BK4819_PlayToneRaw(tone2_Hz, 80);
|
||||
|
||||
BK4819_WriteRegister(BK4819_REG_70, 0x0000);
|
||||
|
||||
if (Bandwidth == BK4819_FILTER_BW_WIDE)
|
||||
BK4819_SetFilterBandwidth(Bandwidth, true);
|
||||
|
||||
BK4819_WriteRegister(BK4819_REG_30, 0xC1FE); // 1 1 0000 0 1 1111 1 1 1 0
|
||||
}
|
||||
|
||||
@@ -1832,10 +1851,10 @@ void BK4819_PlayRogerMDC(void)
|
||||
BK4819_WriteRegister(BK4819_REG_58, 0x0000);
|
||||
}
|
||||
|
||||
void BK4819_PlayRoger(void)
|
||||
void BK4819_PlayRoger(BK4819_FilterBandwidth_t Bandwidth)
|
||||
{
|
||||
if (gEeprom.ROGER == ROGER_MODE_ROGER) {
|
||||
BK4819_PlayRogerNormal();
|
||||
BK4819_PlayRogerNormal(Bandwidth);
|
||||
} else if (gEeprom.ROGER == ROGER_MODE_MDC) {
|
||||
BK4819_PlayRogerMDC();
|
||||
}
|
||||
|
||||
+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,7 +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, 0x0090D6), // 14 VFO * 16 Bytes (ex 0x001000)
|
||||
_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 / 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
|
||||
@@ -72,6 +77,13 @@ static const AddrMapping_t ADDR_MAPPINGS[] = {
|
||||
// [0x00..0x07] 8-byte header (reserved)
|
||||
// [0x08..0x407] 128x64 monochrome bitmap, 1024 Bytes
|
||||
// ST7565-native: 8 pages * 128 columns, column-major LSB-top
|
||||
|
||||
// Not mapped, for documentation only (the EEPROM API uses 16-bit
|
||||
// addresses and could not reach a 32 KB window anyway):
|
||||
//
|
||||
// 0x1E0000 -> 0x1E8000: RX/TX append-only log * 32 KB / 8 sectors
|
||||
// (ENABLE_FEAT_F4HWN_RXTX_LOG, accessed directly
|
||||
// by app/rxtx_log.c, not through this layer)
|
||||
};
|
||||
|
||||
static void AddrTranslate(uint16_t EEPROM_Addr, uint16_t Size, uint32_t *PY25Q16_Addr_out, uint16_t *Size_out, bool *End_out);
|
||||
@@ -97,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;
|
||||
|
||||
+33
-5
@@ -27,7 +27,7 @@ KEY_Code_t gKeyReading1 = KEY_INVALID;
|
||||
uint16_t gDebounceCounter = 0;
|
||||
bool gWasFKeyPressed = false;
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
// Short press: hold key for SERIAL_KEY_SHORT_POLLS calls.
|
||||
// Must exceed key_debounce_10ms (2) to trigger ProcessKey(key, true, false).
|
||||
#define SERIAL_KEY_SHORT_POLLS 5
|
||||
@@ -39,8 +39,14 @@ bool gWasFKeyPressed = false;
|
||||
// Packet types for serial key injection (K5Viewer → radio)
|
||||
#define SERIAL_KEY_TYPE 0x03
|
||||
#define SERIAL_KEY_TYPE_LONG 0x04
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
|
||||
#define SERIAL_FEATURE_TYPE 0x05
|
||||
#endif
|
||||
|
||||
volatile KEY_Code_t gKeyFromSerial = KEY_INVALID;
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
|
||||
volatile uint8_t gSerialViewerFeatures = 0;
|
||||
#endif
|
||||
static uint8_t gSerialKeyHoldCount = 0;
|
||||
static uint8_t gSerialKeyLong = 0; // 0 = short press, 1 = long press
|
||||
|
||||
@@ -71,13 +77,35 @@ bool KEYBOARD_ProcessProtocolByte(ParseState_t *state, uint8_t b)
|
||||
break;
|
||||
|
||||
case STATE_KA_2:
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
|
||||
if (b == 0x00)
|
||||
*state = STATE_KA_3;
|
||||
else if (b == SERIAL_FEATURE_TYPE)
|
||||
*state = STATE_KA_FEATURE;
|
||||
else
|
||||
*state = STATE_IDLE;
|
||||
#else
|
||||
*state = (b == 0x00) ? STATE_KA_3 : STATE_IDLE;
|
||||
#endif
|
||||
break;
|
||||
|
||||
|
||||
case STATE_KA_3:
|
||||
if (b == 0x00) connected = true;
|
||||
if (b == 0x00) {
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
|
||||
gSerialViewerFeatures = 0;
|
||||
#endif
|
||||
connected = true;
|
||||
}
|
||||
*state = STATE_IDLE;
|
||||
break;
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
|
||||
case STATE_KA_FEATURE:
|
||||
gSerialViewerFeatures = b;
|
||||
connected = true;
|
||||
*state = STATE_IDLE;
|
||||
break;
|
||||
#endif
|
||||
|
||||
case STATE_KEY_1:
|
||||
*state = (b == 0x55) ? STATE_KEY_2 : STATE_IDLE;
|
||||
@@ -156,7 +184,7 @@ static const KEY_Code_t keyboard[5][4] = {
|
||||
|
||||
KEY_Code_t KEYBOARD_Poll(void)
|
||||
{
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
// Serial-injected key: hold it for SHORT or LONG polls depending on press type,
|
||||
// so the debounce counter in app.c reaches the right threshold:
|
||||
// - Short: key_debounce_10ms (2) → ProcessKey(key, true, false)
|
||||
@@ -265,4 +293,4 @@ KEY_Code_t KEYBOARD_GetKey(void)
|
||||
void HideFKeyIcon(void) {
|
||||
gWasFKeyPressed = false;
|
||||
gUpdateStatus = true;
|
||||
}
|
||||
}
|
||||
+14
-2
@@ -50,6 +50,9 @@ typedef enum {
|
||||
STATE_KA_1,
|
||||
STATE_KA_2,
|
||||
STATE_KA_3,
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
|
||||
STATE_KA_FEATURE,
|
||||
#endif
|
||||
STATE_KEY_1,
|
||||
STATE_KEY_2,
|
||||
STATE_KEY_3,
|
||||
@@ -61,10 +64,19 @@ extern KEY_Code_t gKeyReading1;
|
||||
extern uint16_t gDebounceCounter;
|
||||
extern bool gWasFKeyPressed;
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
// Serial-injected key (written by UART/VCP parser, consumed by KEYBOARD_Poll).
|
||||
extern volatile KEY_Code_t gKeyFromSerial;
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
|
||||
#define SERIAL_VIEWER_FEATURE_RF_LOG 0x01u
|
||||
#define SERIAL_VIEWER_FEATURE_RF_LOG_HISTORY 0x02u
|
||||
#define SERIAL_VIEWER_FEATURE_RF_LOG_RESTART 0x80u
|
||||
|
||||
// Extension flags announced by the viewer's feature keepalive.
|
||||
extern volatile uint8_t gSerialViewerFeatures;
|
||||
#endif
|
||||
|
||||
bool KEYBOARD_ProcessProtocolByte(ParseState_t *state, uint8_t b);
|
||||
#endif
|
||||
|
||||
@@ -73,4 +85,4 @@ KEY_Code_t KEYBOARD_GetKey(void);
|
||||
|
||||
void HideFKeyIcon(void);
|
||||
|
||||
#endif
|
||||
#endif
|
||||
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 */
|
||||
+89
-4
@@ -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,16 +338,22 @@ 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;
|
||||
}
|
||||
|
||||
if (0 != memcmp(pBuffer, (char *)SectorCache + SecOffset, SecSize))
|
||||
{
|
||||
bool Erase = false;
|
||||
const uint8_t *oldData = SectorCache + SecOffset;
|
||||
const uint8_t *newData = (const uint8_t *)pBuffer;
|
||||
|
||||
for (uint32_t i = 0; i < SecSize; i++)
|
||||
{
|
||||
if (0xff != SectorCache[SecOffset + i])
|
||||
// NOR flash programming can only change bits from 1 to 0.
|
||||
if ((oldData[i] & newData[i]) != newData[i])
|
||||
{
|
||||
Erase = true;
|
||||
break;
|
||||
@@ -333,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)
|
||||
@@ -341,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));
|
||||
@@ -491,4 +576,4 @@ void DMA1_Channel4_5_6_7_IRQHandler()
|
||||
|
||||
TC_Flag = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
+19
-10
@@ -24,7 +24,7 @@
|
||||
#include "driver/st7565.h"
|
||||
#include "driver/system.h"
|
||||
#include "misc.h"
|
||||
#include "screenshot.h"
|
||||
#include "k5viewer.h"
|
||||
|
||||
#define SPIx SPI1
|
||||
|
||||
@@ -164,8 +164,8 @@ void ST7565_DrawLine(const unsigned int Column, const unsigned int Line, const u
|
||||
void ST7565_BlitLine(unsigned line)
|
||||
{
|
||||
ST7565_BlitScreen(line + 1);
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
|
||||
SCREENSHOT_Update(true); // Force immediate capture
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
K5VIEWER_Update(true); // Force immediate capture
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -260,7 +262,7 @@ void ST7565_FillScreen(uint8_t value)
|
||||
// D=0, display OFF
|
||||
#define ST7565_CMD_DISPLAY_ON_OFF 0xAE
|
||||
|
||||
uint8_t cmds[] = {
|
||||
const uint8_t cmds[] = {
|
||||
ST7565_CMD_BIAS_SELECT | 0, // Select bias setting: 1/9
|
||||
ST7565_CMD_COM_DIRECTION | (0 << 3), // Set output direction of COM: normal
|
||||
ST7565_CMD_SEG_DIRECTION | 1, // Set scan direction of SEG: reverse
|
||||
@@ -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
|
||||
|
||||
+2
-2
@@ -23,7 +23,7 @@
|
||||
#include "py32f071_ll_gpio.h"
|
||||
#include "py32f071_ll_usart.h"
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
#include "driver/keyboard.h"
|
||||
#endif
|
||||
|
||||
@@ -150,7 +150,7 @@ void UART_LogSend(const void *pBuffer, uint32_t Size)
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
bool UART_IsCableConnected(void) {
|
||||
static uint8_t read_ptr = 0;
|
||||
static ParseState_t state = STATE_IDLE;
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ void UART_Init(void);
|
||||
void UART_Send(const void *pBuffer, uint32_t Size);
|
||||
void UART_LogSend(const void *pBuffer, uint32_t Size);
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
bool UART_IsCableConnected(void);
|
||||
#endif
|
||||
|
||||
|
||||
+15
-9
@@ -18,7 +18,7 @@
|
||||
#include "usb_config.h"
|
||||
#include "py32f071_ll_bus.h"
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
#include "driver/keyboard.h"
|
||||
#endif
|
||||
|
||||
@@ -42,19 +42,25 @@ void VCP_Init()
|
||||
NVIC_EnableIRQ(USBD_IRQn);
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
|
||||
bool VCP_ScreenshotPing(void)
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
bool VCP_K5ViewerPing(void)
|
||||
{
|
||||
// State machine for parsing incoming packets:
|
||||
// Keepalive: 0x55 0xAA 0x00 0x00 → viewer alive
|
||||
// Short key press: 0xAA 0x55 0x03 <key> → inject short press
|
||||
// Long key press: 0xAA 0x55 0x04 <key> → inject long press
|
||||
// Keepalive: 0x55 0xAA 0x00 0x00 → viewer alive, no extensions
|
||||
// Feature keepalive (ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER builds only):
|
||||
// 0x55 0xAA 0x05 <flags> → viewer alive, extensions enabled
|
||||
// flags bit 0 = RF log stream
|
||||
// flags bit 1 = paged RF log history
|
||||
// flags bit 7 = restart RF log synchronization
|
||||
// Short key press: 0xAA 0x55 0x03 <key> → inject short press
|
||||
// Long key press: 0xAA 0x55 0x04 <key> → inject long press
|
||||
//
|
||||
// State transitions:
|
||||
// IDLE → 0x55 → KA_1
|
||||
// KA_1 → 0xAA → KA_2 (else IDLE)
|
||||
// KA_2 → 0x00 → KA_3 (else IDLE)
|
||||
// KA_3 → 0x00 → keepalive OK, IDLE
|
||||
// KA_2 → 0x00 → KA_3 (else 0x05 → KA_FEATURE, else IDLE)
|
||||
// KA_3 → 0x00 → keepalive OK, clear extensions, IDLE
|
||||
// KA_FEATURE → <flags> → keepalive OK, set extensions, IDLE
|
||||
//
|
||||
// IDLE → 0xAA → KEY_1
|
||||
// KEY_1 → 0x55 → KEY_2 (else IDLE)
|
||||
@@ -89,4 +95,4 @@ bool VCP_ScreenshotPing(void)
|
||||
|
||||
return connected;
|
||||
}
|
||||
#endif // ENABLE_FEAT_F4HWN_SCREENSHOT
|
||||
#endif // ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
+2
-2
@@ -29,8 +29,8 @@ extern volatile uint32_t VCP_RxBufPointer;
|
||||
|
||||
void VCP_Init();
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
|
||||
bool VCP_ScreenshotPing(void);
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
bool VCP_K5ViewerPing(void);
|
||||
#endif
|
||||
|
||||
static inline void VCP_Send(const uint8_t *Buf, uint32_t Size)
|
||||
|
||||
+34
-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"
|
||||
@@ -33,6 +33,9 @@
|
||||
#include "frequencies.h"
|
||||
#include "functions.h"
|
||||
#include "helper/battery.h"
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
|
||||
#include "app/rxtx_log.h"
|
||||
#endif
|
||||
#include "misc.h"
|
||||
#include "radio.h"
|
||||
#include "settings.h"
|
||||
@@ -100,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
|
||||
@@ -153,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();
|
||||
@@ -195,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;
|
||||
@@ -239,8 +208,31 @@ void FUNCTION_Select(FUNCTION_Type_t Function)
|
||||
const FUNCTION_Type_t PreviousFunction = gCurrentFunction;
|
||||
const bool bWasPowerSave = PreviousFunction == FUNCTION_POWER_SAVE;
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
|
||||
const bool previousWasActive =
|
||||
PreviousFunction == FUNCTION_TRANSMIT ||
|
||||
PreviousFunction == FUNCTION_MONITOR ||
|
||||
PreviousFunction == FUNCTION_RECEIVE;
|
||||
const bool previousWasTx = PreviousFunction == FUNCTION_TRANSMIT;
|
||||
const bool nextIsActive =
|
||||
Function == FUNCTION_TRANSMIT ||
|
||||
Function == FUNCTION_MONITOR ||
|
||||
Function == FUNCTION_RECEIVE;
|
||||
const bool nextIsTx = Function == FUNCTION_TRANSMIT;
|
||||
|
||||
if (previousWasActive &&
|
||||
(!nextIsActive ||
|
||||
(previousWasTx != nextIsTx)))
|
||||
RXTX_LOG_EndActive();
|
||||
#endif
|
||||
|
||||
gCurrentFunction = Function;
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
|
||||
if (nextIsActive && !nextIsTx && (!previousWasActive || previousWasTx))
|
||||
RXTX_LOG_BeginRx(gRxVfo, Function);
|
||||
#endif
|
||||
|
||||
if (bWasPowerSave && Function != FUNCTION_POWER_SAVE) {
|
||||
BK4819_Conditional_RX_TurnOn_and_GPIO6_Enable();
|
||||
gRxIdleMode = false;
|
||||
@@ -292,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 @@
|
||||
#ifndef FUNCTIONS_H
|
||||
#define FUNCTIONS_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
enum FUNCTION_Type_t
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
+33
-8
@@ -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))
|
||||
{
|
||||
@@ -73,12 +93,14 @@ BOOT_Mode_t BOOT_GetMode(void)
|
||||
|
||||
void BOOT_ProcessMode(BOOT_Mode_t Mode)
|
||||
{
|
||||
GUI_DisplayType_t display = DISPLAY_MAIN;
|
||||
|
||||
if (Mode == BOOT_MODE_F_LOCK)
|
||||
{
|
||||
#ifdef ENABLE_FEAT_F4HWN_RESUME_STATE
|
||||
gEeprom.CURRENT_STATE = 0; // Don't resume is active...
|
||||
#endif
|
||||
GUI_SelectNextDisplay(DISPLAY_MENU);
|
||||
display = DISPLAY_MENU;
|
||||
}
|
||||
#ifdef ENABLE_AIRCOPY
|
||||
else
|
||||
@@ -119,11 +141,14 @@ void BOOT_ProcessMode(BOOT_Mode_t Mode)
|
||||
gEeprom.CURRENT_STATE = 0; // Don't resume is active...
|
||||
#endif
|
||||
|
||||
GUI_SelectNextDisplay(DISPLAY_AIRCOPY);
|
||||
display = DISPLAY_AIRCOPY;
|
||||
}
|
||||
#endif
|
||||
else
|
||||
{
|
||||
GUI_SelectNextDisplay(DISPLAY_MAIN);
|
||||
}
|
||||
|
||||
#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
|
||||
};
|
||||
|
||||
|
||||
+358
@@ -0,0 +1,358 @@
|
||||
/* Copyright 2024 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 "debugging.h"
|
||||
#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
|
||||
#include "app/rxtx_log.h"
|
||||
#endif
|
||||
|
||||
// SRAM optimization: minimize static allocations
|
||||
// - previousHash: one fingerprint per 8-byte chunk instead of a full
|
||||
// 1024-byte copy of the previous frame (chunks are only compared,
|
||||
// never retransmitted from history). A 16-bit fingerprint keeps the
|
||||
// collision odds at ~1/65536 per chunk, so a stale chunk slipping
|
||||
// through is rare; the forcedBlock rotation repairs that residual
|
||||
// within at most 128 frames.
|
||||
// Stack optimization: chunks are computed on demand straight from
|
||||
// gStatusLine/gFrameBuffer instead of building the whole 1024-byte
|
||||
// frame on the stack. Changed chunks are tracked in a 16-byte bitmap.
|
||||
// Peak stack drops from ~1.3 KB to ~40 bytes, at the cost of
|
||||
// transforming each transmitted chunk twice (negligible next to the
|
||||
// blocking UART transfer).
|
||||
static uint16_t previousHash[128];
|
||||
static uint8_t previousStateFlags = 0xFF;
|
||||
static uint8_t forcedBlock = 0;
|
||||
static uint8_t keepAlive = 3;
|
||||
static bool hasConnectionPing = false;
|
||||
static bool wasConnected = false;
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
|
||||
static uint32_t previousRfLogSignature = 0;
|
||||
static bool rfLogSent = false;
|
||||
// Pagination bound of the history dump: rows with trafficSeq below this
|
||||
// value remain to be sent; HISTORY_START arms a new dump, 0 means done.
|
||||
static uint32_t rfLogHistoryBefore = RXTX_LOG_K5VIEWER_HISTORY_START;
|
||||
#endif
|
||||
|
||||
// FNV-1a over one 8-byte chunk, folded to 16 bits
|
||||
static uint16_t K5VIEWER_Hash(const uint8_t *data)
|
||||
{
|
||||
uint32_t h = 2166136261u;
|
||||
for (uint8_t i = 0; i < 8; i++) {
|
||||
h = (h ^ data[i]) * 16777619u;
|
||||
}
|
||||
return (uint16_t)(h ^ (h >> 16));
|
||||
}
|
||||
|
||||
void K5VIEWER_ParseInput(void)
|
||||
{
|
||||
if (K5VIEWER_IsLocked())
|
||||
return;
|
||||
|
||||
#ifdef ENABLE_UART
|
||||
if (UART_IsCableConnected()) {
|
||||
keepAlive = 15;
|
||||
hasConnectionPing = true;
|
||||
gUSB_K5ViewerEnabled = false;
|
||||
return;
|
||||
}
|
||||
#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 {
|
||||
K5VIEWER_CHUNK_SIZE = 8,
|
||||
K5VIEWER_CHUNKS_PER_LINE = 16,
|
||||
K5VIEWER_HALF_LINE_COLUMNS = LCD_WIDTH / 2,
|
||||
K5VIEWER_MARKER_BASE = 0xF0,
|
||||
K5VIEWER_TYPE_DIFF = 0x02,
|
||||
K5VIEWER_TYPE_RXTX_LOG = 0x05,
|
||||
K5VIEWER_TYPE_RXTX_LOG_HISTORY = 0x06,
|
||||
K5VIEWER_FLAG_DEEP_SLEEP = 1 << 0,
|
||||
K5VIEWER_FLAG_LED_RED = 1 << 1,
|
||||
K5VIEWER_FLAG_LED_GREEN = 1 << 2,
|
||||
};
|
||||
|
||||
static uint8_t K5VIEWER_StateFlags(void)
|
||||
{
|
||||
uint8_t flags = 0;
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_SLEEP
|
||||
if (gWakeUp)
|
||||
flags |= K5VIEWER_FLAG_DEEP_SLEEP;
|
||||
#endif
|
||||
|
||||
if (BK4819_IsGpioOutSet(BK4819_GPIO5_PIN1_RED))
|
||||
flags |= K5VIEWER_FLAG_LED_RED;
|
||||
|
||||
if (BK4819_IsGpioOutSet(BK4819_GPIO6_PIN2_GREEN))
|
||||
flags |= K5VIEWER_FLAG_LED_GREEN;
|
||||
|
||||
return flags;
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
|
||||
static bool K5VIEWER_HasPendingRfLogUpdate(void)
|
||||
{
|
||||
if (!RXTX_LOG_IsEnabled())
|
||||
rfLogHistoryBefore = RXTX_LOG_K5VIEWER_HISTORY_START;
|
||||
|
||||
if ((gSerialViewerFeatures & SERIAL_VIEWER_FEATURE_RF_LOG_RESTART) != 0) {
|
||||
gSerialViewerFeatures &= ~SERIAL_VIEWER_FEATURE_RF_LOG_RESTART;
|
||||
rfLogSent = false;
|
||||
rfLogHistoryBefore = RXTX_LOG_K5VIEWER_HISTORY_START;
|
||||
}
|
||||
|
||||
if ((gSerialViewerFeatures & SERIAL_VIEWER_FEATURE_RF_LOG) == 0)
|
||||
return false;
|
||||
|
||||
return !rfLogSent || RXTX_LOG_K5ViewerSignature() != previousRfLogSignature;
|
||||
}
|
||||
|
||||
static void K5VIEWER_SendRfLogFrameHeader(uint8_t type, uint16_t len)
|
||||
{
|
||||
uint8_t header[5] = {0xAA, 0x55, type, (uint8_t)(len >> 8), (uint8_t)len};
|
||||
|
||||
K5VIEWER_Send(header, sizeof(header));
|
||||
}
|
||||
|
||||
static void K5VIEWER_SendRfLogFrameEnd(void)
|
||||
{
|
||||
const uint8_t end = 0x0A;
|
||||
|
||||
K5VIEWER_Send(&end, 1);
|
||||
}
|
||||
|
||||
static void K5VIEWER_SendRfLog(void)
|
||||
{
|
||||
// Capture the signature before streaming: a state change landing
|
||||
// during the blocking send still differs afterwards and triggers a
|
||||
// resend on the next cycle.
|
||||
previousRfLogSignature = RXTX_LOG_K5ViewerSignature();
|
||||
rfLogSent = true;
|
||||
|
||||
const uint16_t size = RXTX_LOG_IsEnabled()
|
||||
? RXTX_LOG_K5VIEWER_PACKET_SIZE
|
||||
: RXTX_LOG_K5VIEWER_STATUS_PACKET_SIZE;
|
||||
K5VIEWER_SendRfLogFrameHeader(K5VIEWER_TYPE_RXTX_LOG, size);
|
||||
RXTX_LOG_SendK5ViewerPacket(K5VIEWER_Send);
|
||||
K5VIEWER_SendRfLogFrameEnd();
|
||||
}
|
||||
|
||||
static bool K5VIEWER_HasPendingRfLogHistory(void)
|
||||
{
|
||||
return RXTX_LOG_IsEnabled() &&
|
||||
(gSerialViewerFeatures & SERIAL_VIEWER_FEATURE_RF_LOG_HISTORY) != 0 &&
|
||||
rfLogHistoryBefore != 0;
|
||||
}
|
||||
|
||||
static void K5VIEWER_SendRfLogHistory(void)
|
||||
{
|
||||
K5VIEWER_SendRfLogFrameHeader(K5VIEWER_TYPE_RXTX_LOG_HISTORY,
|
||||
RXTX_LOG_K5VIEWER_HISTORY_PACKET_SIZE);
|
||||
rfLogHistoryBefore = RXTX_LOG_SendK5ViewerHistoryPage(rfLogHistoryBefore, K5VIEWER_Send);
|
||||
K5VIEWER_SendRfLogFrameEnd();
|
||||
}
|
||||
#endif
|
||||
|
||||
bool K5VIEWER_HasPendingStateChange(void)
|
||||
{
|
||||
if (gUART_LockK5Viewer > 0 || keepAlive == 0 || !hasConnectionPing)
|
||||
return false;
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
|
||||
if (K5VIEWER_HasPendingRfLogUpdate())
|
||||
return true;
|
||||
if (K5VIEWER_HasPendingRfLogHistory())
|
||||
return true;
|
||||
#endif
|
||||
|
||||
return !wasConnected || K5VIEWER_StateFlags() != previousStateFlags;
|
||||
}
|
||||
|
||||
// Compute one 8-byte output chunk directly from the display buffers.
|
||||
// Frame layout: per line (status + 7 frame lines), 8 bit layers of
|
||||
// 16 bytes each. Each bit layer is split into two 64-column chunks.
|
||||
static void K5VIEWER_Chunk(uint8_t chunkIdx, uint8_t *dest)
|
||||
{
|
||||
const uint8_t chunkInLine = chunkIdx % K5VIEWER_CHUNKS_PER_LINE;
|
||||
const uint8_t line = chunkIdx / K5VIEWER_CHUNKS_PER_LINE;
|
||||
const uint8_t bit = chunkInLine / 2;
|
||||
const uint8_t columnBase = (chunkInLine % 2) * K5VIEWER_HALF_LINE_COLUMNS;
|
||||
const uint8_t *src = (line == 0 ? gStatusLine : gFrameBuffer[line - 1])
|
||||
+ columnBase;
|
||||
|
||||
for (uint8_t j = 0; j < K5VIEWER_CHUNK_SIZE; j++) {
|
||||
uint8_t acc = 0;
|
||||
for (uint8_t k = 0; k < K5VIEWER_CHUNK_SIZE; k++) {
|
||||
if (src[j * K5VIEWER_CHUNK_SIZE + k] & (1 << bit)) acc |= (1 << k);
|
||||
}
|
||||
dest[j] = gSetting_set_inv ? ~acc : acc;
|
||||
}
|
||||
}
|
||||
|
||||
void K5VIEWER_Update(bool force)
|
||||
{
|
||||
if (K5VIEWER_IsLocked())
|
||||
return;
|
||||
|
||||
if (keepAlive > 0) {
|
||||
if (--keepAlive == 0) {
|
||||
// Connection just lost → reset state for next reconnection
|
||||
wasConnected = false;
|
||||
hasConnectionPing = false;
|
||||
previousStateFlags = 0xFF;
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
|
||||
gSerialViewerFeatures = 0;
|
||||
rfLogSent = false;
|
||||
rfLogHistoryBefore = RXTX_LOG_K5VIEWER_HISTORY_START;
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
// Connection is alive — detect reconnection and force full frame
|
||||
if (!wasConnected) {
|
||||
force = true;
|
||||
}
|
||||
|
||||
const uint8_t stateFlags = K5VIEWER_StateFlags();
|
||||
const bool stateChanged = (stateFlags != previousStateFlags);
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
|
||||
const bool rfLogPending = K5VIEWER_HasPendingRfLogUpdate();
|
||||
const bool rfLogHistoryPending = K5VIEWER_HasPendingRfLogHistory();
|
||||
#else
|
||||
const bool rfLogPending = false;
|
||||
const bool rfLogHistoryPending = false;
|
||||
#endif
|
||||
|
||||
// ==== FIRST PASS: Count changed chunks ====
|
||||
uint16_t deltaLen = 0;
|
||||
uint8_t changedBitmap[16] = {0}; // 1 bit per chunk
|
||||
uint8_t chunk[9]; // [0] = index, [1..8] = payload
|
||||
|
||||
for (uint8_t chunkIdx = 0; chunkIdx < 128; chunkIdx++) {
|
||||
K5VIEWER_Chunk(chunkIdx, &chunk[1]);
|
||||
|
||||
bool changed = K5VIEWER_Hash(&chunk[1]) != previousHash[chunkIdx];
|
||||
bool isForced = (chunkIdx == forcedBlock);
|
||||
|
||||
if (changed || isForced || force) {
|
||||
changedBitmap[chunkIdx >> 3] |= 1 << (chunkIdx & 7);
|
||||
deltaLen += 9;
|
||||
}
|
||||
}
|
||||
|
||||
forcedBlock = (forcedBlock + 1) % 128;
|
||||
|
||||
if (deltaLen == 0 && !stateChanged && !rfLogPending && !rfLogHistoryPending)
|
||||
return;
|
||||
|
||||
// Skip transmission if a key is currently pressed
|
||||
// UART_Send is blocking - would freeze the main loop and lose keypresses
|
||||
if (gKeyReading0 != KEY_INVALID)
|
||||
return;
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
|
||||
// A pending RF log packet may be the only thing to send: skip the
|
||||
// frame section when it carries nothing. Without the RF log stream
|
||||
// the early return above already guarantees this condition.
|
||||
if (deltaLen != 0 || stateChanged)
|
||||
#endif
|
||||
{
|
||||
// ==== Send version marker and state flags ====
|
||||
// 0xF0 keeps a resync-safe marker before the standard AA 55 header.
|
||||
uint8_t versionMarker = K5VIEWER_MARKER_BASE | stateFlags;
|
||||
K5VIEWER_Send(&versionMarker, 1);
|
||||
|
||||
// ==== Send header ====
|
||||
uint8_t header[5] = {
|
||||
0xAA, 0x55, K5VIEWER_TYPE_DIFF,
|
||||
(uint8_t)(deltaLen >> 8),
|
||||
(uint8_t)(deltaLen & 0xFF)
|
||||
};
|
||||
|
||||
K5VIEWER_Send(header, 5);
|
||||
|
||||
// ==== SECOND PASS: Send only changed chunks ====
|
||||
for (uint8_t chunkIdx = 0; chunkIdx < 128; chunkIdx++) {
|
||||
if (!(changedBitmap[chunkIdx >> 3] & (1 << (chunkIdx & 7))))
|
||||
continue;
|
||||
|
||||
chunk[0] = chunkIdx;
|
||||
K5VIEWER_Chunk(chunkIdx, &chunk[1]);
|
||||
|
||||
K5VIEWER_Send(chunk, 9);
|
||||
|
||||
// Update the fingerprint only once the chunk is actually sent,
|
||||
// so chunks skipped by an early return stay marked as changed.
|
||||
// Hashing the recomputed payload also keeps the fingerprint in
|
||||
// sync if the display buffer changed between the two passes.
|
||||
previousHash[chunkIdx] = K5VIEWER_Hash(&chunk[1]);
|
||||
}
|
||||
|
||||
uint8_t end = 0x0A;
|
||||
K5VIEWER_Send(&end, 1);
|
||||
}
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG_K5VIEWER
|
||||
if (rfLogPending)
|
||||
K5VIEWER_SendRfLog();
|
||||
if (rfLogHistoryPending)
|
||||
K5VIEWER_SendRfLogHistory();
|
||||
#endif
|
||||
|
||||
previousStateFlags = stateFlags;
|
||||
wasConnected = true;
|
||||
}
|
||||
@@ -14,10 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef SCREENSHOT_H
|
||||
#define SCREENSHOT_H
|
||||
#ifndef K5VIEWER_H
|
||||
#define K5VIEWER_H
|
||||
|
||||
void SCREENSHOT_Update(bool force);
|
||||
void SCREENSHOT_ParseInput(void);
|
||||
void K5VIEWER_Update(bool force);
|
||||
void K5VIEWER_ParseInput(void);
|
||||
bool K5VIEWER_HasPendingStateChange(void);
|
||||
|
||||
#endif
|
||||
#endif
|
||||
+38
-17
@@ -18,12 +18,11 @@
|
||||
#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
|
||||
#include "app/rxtx_log.h"
|
||||
#endif
|
||||
#include "misc.h"
|
||||
#include "radio.h"
|
||||
#include "settings.h"
|
||||
@@ -49,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
|
||||
@@ -78,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
|
||||
@@ -99,12 +110,15 @@ void Main(void)
|
||||
|
||||
SETTINGS_InitEEPROM();
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_RXTX_LOG
|
||||
RXTX_LOG_Init();
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN
|
||||
gDW = gEeprom.DUAL_WATCH;
|
||||
gCB = gEeprom.CROSS_BAND_RX_TX;
|
||||
#endif
|
||||
|
||||
SETTINGS_WriteBuildOptions();
|
||||
SETTINGS_LoadCalibration();
|
||||
|
||||
RADIO_ConfigureChannel(0, VFO_CONFIGURE_RELOAD);
|
||||
@@ -119,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)
|
||||
{
|
||||
@@ -157,19 +177,17 @@ void Main(void)
|
||||
#ifdef ENABLE_FEAT_F4HWN
|
||||
gEeprom.KEY_LOCK = 0;
|
||||
SETTINGS_SaveSettings();
|
||||
#ifdef ENABLE_FEAT_F4HWN_MENU_CAT
|
||||
gMenuLevel = MENU_LEVEL_ITEMS;
|
||||
gMenuCategory = CAT_ALL;
|
||||
#endif
|
||||
gMenuCursor = UI_MENU_GetMenuIdx(FIRST_HIDDEN_MENU_ITEM);
|
||||
gSubMenuSelection = gSetting_F_LOCK;
|
||||
#endif
|
||||
}
|
||||
|
||||
// count the number of menu items
|
||||
gMenuListCount = 0;
|
||||
while (MenuList[gMenuListCount].name[0] != '\0') {
|
||||
if(!gF_LOCK && MenuList[gMenuListCount].menu_id == FIRST_HIDDEN_MENU_ITEM)
|
||||
break;
|
||||
|
||||
gMenuListCount++;
|
||||
}
|
||||
// build the current menu view (Etape 1: vue = All, identite)
|
||||
UI_MENU_BuildView();
|
||||
|
||||
// wait for user to release all butts before moving on
|
||||
if (GPIO_IsPttPressed() ||
|
||||
@@ -245,6 +263,9 @@ void Main(void)
|
||||
|
||||
BOOT_ProcessMode(BootMode);
|
||||
|
||||
if (gEeprom.AUTO_KEYPAD_LOCK && !gEeprom.KEY_LOCK)
|
||||
gKeyLockCountdown = gEeprom.AUTO_KEYPAD_LOCK * 30; // 15 seconds step
|
||||
|
||||
// GPIO_ClearBit(&GPIOA->DATA, GPIOA_PIN_VOICE_0);
|
||||
|
||||
gUpdateStatus = true;
|
||||
|
||||
+13
-24
@@ -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;
|
||||
@@ -127,7 +123,7 @@ enum BacklightOnRxTx_t gSetting_backlight_on_tx_rx;
|
||||
uint8_t gSetting_set_ctr = 10;
|
||||
bool gSetting_set_inv = false;
|
||||
uint8_t gSetting_set_eot = 0;
|
||||
bool gSetting_set_lck = false;
|
||||
uint8_t gSetting_set_lck = SET_LCK_KEYS;
|
||||
bool gSetting_set_met = 0;
|
||||
bool gSetting_set_gui = 0;
|
||||
#ifdef ENABLE_FEAT_F4HWN_AUDIO
|
||||
@@ -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
|
||||
@@ -210,9 +203,9 @@ volatile bool gTxTimeoutReached;
|
||||
#ifdef ENABLE_FEAT_F4HWN_RX_TX_TIMER
|
||||
volatile uint16_t gRxTimerCountdown_500ms;
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
|
||||
volatile uint8_t gUART_LockScreenshot = 0; // lock screenshot if Chirp is used
|
||||
bool gUSB_ScreenshotEnabled = false;
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
volatile uint8_t gUART_LockK5Viewer = 0; // lock the K5Viewer stream if Chirp is used
|
||||
bool gUSB_K5ViewerEnabled = false;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -646,11 +635,11 @@ void MR_PrintCacheStats(void)
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
|
||||
bool SCREENSHOT_IsLocked(void)
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
bool K5VIEWER_IsLocked(void)
|
||||
{
|
||||
if (gUART_LockScreenshot > 0) {
|
||||
gUART_LockScreenshot--;
|
||||
if (gUART_LockK5Viewer > 0) {
|
||||
gUART_LockK5Viewer--;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+23
-27
@@ -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;
|
||||
@@ -181,6 +169,19 @@ extern enum BacklightOnRxTx_t gSetting_backlight_on_tx_rx;
|
||||
#endif
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN
|
||||
// Keypad lock scope. Values are BOTH a bitmask (ACTIONS/PTT bits tested
|
||||
// individually) AND a contiguous 0..3 menu index: the order must stay
|
||||
// aligned with gSubMenu_SET_LCK[] and any new entry must keep the range
|
||||
// contiguous so that SET_LCK_LEN remains valid as menu bound and EEPROM
|
||||
// range check.
|
||||
enum SET_LCK_t {
|
||||
SET_LCK_KEYS = 0u,
|
||||
SET_LCK_ACTIONS = 1u,
|
||||
SET_LCK_PTT = 2u,
|
||||
SET_LCK_ACTIONS_PTT = SET_LCK_ACTIONS | SET_LCK_PTT,
|
||||
SET_LCK_LEN
|
||||
};
|
||||
|
||||
#ifdef ENABLE_FEAT_F4HWN_LOGO_SAV
|
||||
enum SET_SAV_t {
|
||||
SET_SAV_OFF,
|
||||
@@ -197,7 +198,7 @@ extern enum BacklightOnRxTx_t gSetting_backlight_on_tx_rx;
|
||||
extern uint8_t gSetting_set_ctr;
|
||||
extern bool gSetting_set_inv;
|
||||
extern uint8_t gSetting_set_eot;
|
||||
extern bool gSetting_set_lck;
|
||||
extern uint8_t gSetting_set_lck;
|
||||
extern bool gSetting_set_met;
|
||||
extern bool gSetting_set_gui;
|
||||
#ifdef ENABLE_FEAT_F4HWN_AUDIO
|
||||
@@ -218,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
|
||||
@@ -336,11 +334,11 @@ extern volatile bool gTxTimeoutReached;
|
||||
#ifdef ENABLE_FEAT_F4HWN_RX_TX_TIMER
|
||||
extern volatile uint16_t gRxTimerCountdown_500ms;
|
||||
#endif
|
||||
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
|
||||
extern volatile uint8_t gUART_LockScreenshot; // lock screenshot if Chirp is used
|
||||
extern bool gUSB_ScreenshotEnabled;
|
||||
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
|
||||
extern volatile uint8_t gUART_LockK5Viewer; // lock the K5Viewer stream if Chirp is used
|
||||
extern bool gUSB_K5ViewerEnabled;
|
||||
|
||||
bool SCREENSHOT_IsLocked(void);
|
||||
bool K5VIEWER_IsLocked(void);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -380,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;
|
||||
@@ -391,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;
|
||||
@@ -400,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;
|
||||
@@ -425,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;
|
||||
@@ -441,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;
|
||||
@@ -457,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;
|
||||
|
||||
Loaded 100 of 160 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user