Compare commits

...
360 Commits
Author SHA1 Message Date
Armel FAUVEAU 3a50e2127c Merge pull request #567 from armel/feature_update_v6
Feature update v6
2026-09-10 22:17:34 +02:00
Armel FAUVEAU c665bb0fa7 Patch Fieldops and Labs 2026-09-10 22:16:49 +02:00
Armel FAUVEAU fee567d417 Patch Fusion and Transfer 2026-09-10 22:14:43 +02:00
Armel FAUVEAU 7a2687b0b2 Fix SetNav persistence in Fusion and Transfer 2026-09-10 22:12:50 +02:00
Armel FAUVEAU e5f6fb236b Merge pull request #542 from Sarijen/main
Fix RSSI level line overlapping BW text in spectrum
2026-09-10 13:48:47 +02:00
Armel FAUVEAU 345535de4c Merge pull request #560 from armel/feature_update_v6
Remove bad idea
2026-09-09 22:10:31 +02:00
Armel FAUVEAU bf6c28f74b Remove bad idea 2026-09-09 22:10:01 +02:00
Armel FAUVEAU 705d68f89f Merge pull request #559 from armel/feature_update_v6
Feature update v6
2026-09-09 21:53:26 +02:00
Armel FAUVEAU 150b96f736 Update donors 2026-09-09 21:52:25 +02:00
Armel FAUVEAU a73a606b4b Prepare new version 2026-09-09 21:49:25 +02:00
Armel FAUVEAU 0aebf26ada Merge private multiboot development into feature_update_v6
# Conflicts:
#	App/app/app.c
#	CMakePresets.json
2026-09-09 21:38:15 +02:00
Armel FAUVEAU 43e9e1ec67 Support macOS Bash 3.2 in compile scripts 2026-09-08 19:25:22 +02:00
Armel FAUVEAU dbb9d42d2d Count the scan-list name hold in 10 ms ticks, not 500 ms 2026-09-07 22:41:50 +02:00
Armel FAUVEAU 749e856959 Restyle the F status indicator 2026-09-07 03:19:43 +02:00
Armel FAUVEAU 960d13e120 Don't force backlight off on serial session init 2026-09-07 01:17:11 +02:00
Armel FAUVEAU d12dbe0bf9 Fix beacon carrier staying off after a mid-window CARR→TONE switch 2026-09-06 23:39:17 +02:00
Armel FAUVEAU 34ea3fd6af Add optional CARR carrier-keying mode to beacon 2026-09-06 23:27:41 +02:00
Armel FAUVEAU 07226822cd Re-arm BLTime on keypress in overlay apps 2026-09-03 22:27:17 +02:00
Armel FAUVEAU a41fb99862 Fix CHIRP upload stall on unaligned final EEPROM write block 2026-09-03 19:02:21 +02:00
Armel FAUVEAU cc6527afc1 Fix Beam overlay VFO restore and deferred channel save 2026-09-03 02:53:38 +02:00
Armel FAUVEAU d95e4efa57 Add ENABLE_FMRADIO_EMBEDDED to split resident FM from shared driver 2026-09-03 00:18:52 +02:00
Armel FAUVEAU 74b2397c96 Optimize Tetris 2026-09-02 21:27:14 +02:00
Armel FAUVEAU e7db0bc1dc Make Triple VFO and Beam overlay services optional 2026-09-02 20:22:00 +02:00
Armel FAUVEAU 9019d0df1a Add Tetris app, just for fun 2026-09-02 12:41:54 +02:00
Armel FAUVEAU cd99ddc46d Rename BEAM overlay app to Beam 2026-08-31 21:54:30 +02:00
Armel FAUVEAU b26265c5f0 Optimize BEAM channel field copies 2026-08-31 21:49:12 +02:00
Armel FAUVEAU c8da5f99cc Version overlay app ABI and capabilities 2026-08-31 20:51:09 +02:00
Armel FAUVEAU 902e19ba8d Port BEAM to overlay apps 2026-08-31 18:28:38 +02:00
Armel FAUVEAU 62906636ed Optimize FM and overlay shortcuts 2026-08-31 02:03:28 +02:00
Armel FAUVEAU 99057f9dd2 Add BLTime support to overlay apps and Max preset 2026-08-31 01:33:23 +02:00
Armel FAUVEAU 0850420fdf Decouple FoxHunt and Beacon actions 2026-08-30 23:12:27 +02:00
Armel FAUVEAU 4c3847ef34 Launch Broadcast FM overlay from FM action 2026-08-30 22:22:20 +02:00
Armel FAUVEAU bd049c0467 Add K5Viewer support to multiboot 2026-08-30 18:42:02 +02:00
Armel FAUVEAU 66ca97fa5e Add K5Viewer support to overlay menu and apps 2026-08-30 18:31:50 +02:00
Armel FAUVEAU 2a82a62ffd Preserve MAIN VFO selection in Triple VFO 2026-08-30 17:39:43 +02:00
Armel FAUVEAU 2f80e60c01 Add SetGUI and PTT support to Triple VFO 2026-08-30 17:28:33 +02:00
Armel FAUVEAU 048e7e483b Optimize overlay apps and shared audio scope 2026-08-30 17:02:35 +02:00
Armel FAUVEAU 88e76b0e9b Optimize overlay ABI and reduce flash footprint 2026-08-30 13:15:16 +02:00
Armel FAUVEAU 8c78d28f0d Add TripleVFO, the king of clickbait... 2026-08-30 12:45:36 +02:00
Armel FAUVEAU b86da168e9 Improve apps 2026-08-30 00:38:51 +02:00
Armel FAUVEAU 104b7fb0b7 Refine Apps and multiboot UI 2026-08-29 13:54:44 +02:00
Armel FAUVEAU 9358d5d58f Add Cube3D app, just for fun 2026-08-29 11:58:20 +02:00
Armel FAUVEAU 9d6b560a54 Apply SetNav in overlay apps and FoxHunt 2026-08-28 23:43:42 +02:00
Armel FAUVEAU f4811cbe1e Add Plasma app, just for fun 2026-08-28 20:49:23 +02:00
Armel FAUVEAU 678cb9b73a Add full memory and settings transfer 2026-08-28 17:13:13 +02:00
Armel FAUVEAU f35408317e Add external-flash overlay app platform 2026-08-28 00:40:24 +02:00
Sarijen 3f395f9954 Fix RSSI level line overlapping BW text in spectrum 2026-08-26 23:19:58 +02:00
Armel FAUVEAU 3f958f637d Optimize FoxHunt Flash usage 2026-08-25 22:06:24 +02:00
Armel FAUVEAU b0143f9f9c Optimize Breakout Flash and RAM usage 2026-08-25 18:45:16 +02:00
Armel FAUVEAU 24d3e5283f Optimize Broadcast FM display and remove dead channel formatter 2026-08-25 15:25:24 +02:00
Armel FAUVEAU 285f543d46 Make Aircopy transfers reliable with ACK/RESEND recovery and leaner FSK handling 2026-08-25 13:05:20 +02:00
Armel FAUVEAU ce6bf08b23 Strengthening input boundary validation and preventing memory disclosure 2026-08-24 19:41:20 +02:00
Armel FAUVEAU 265f83a447 Reject oversized EEPROM read commands (Thanks @BB-KING777) 2026-08-24 02:46:58 +02:00
Armel FAUVEAU 88d17655de Rename Field preset to FieldOps 2026-08-24 00:53:35 +02:00
Armel FAUVEAU 92c380dd7a Rename Field preset to FieldOps 2026-08-24 00:48:31 +02:00
Armel FAUVEAU e1dfbde590 Prevent FM start during active VFO RX 2026-08-24 00:14:40 +02:00
Armel FAUVEAU 04a80916a2 Rename Extended preset to Max 2026-08-23 23:57:27 +02:00
Armel FAUVEAU 4315b98aec Preserve RescueOps settings across presets 2026-08-22 22:49:14 +02:00
Armel FAUVEAU 751c6064d1 Replace build options with CHIRP capabilities 2026-08-22 15:55:32 +02:00
Armel FAUVEAU ec7b4083c0 Stabilize action IDs and handle unavailable actions 2026-08-22 14:25:56 +02:00
Armel FAUVEAU 941693a0d8 Remove ENABLE_TX_WHEN_AM 2026-08-22 13:58:06 +02:00
Armel FAUVEAU 66fb8b1ed4 Remove ENABLE_AM_FIX 2026-08-22 13:52:04 +02:00
Armel FAUVEAU 7d550585d0 Remove ENABLE_BLMIN_TMP_OFF 2026-08-22 13:18:06 +02:00
Armel FAUVEAU 33dde24226 Remove ENABLE_ALARM and isolate ENABLE_TX1750 2026-08-22 12:58:34 +02:00
Armel FAUVEAU 1bf75db62a Preserve Ninja progress in interactive Docker builds 2026-08-21 03:26:43 +02:00
Armel FAUVEAU e4a645a5c7 Add SetCfg to pick a config bank independently of the slot 2026-08-21 02:56:54 +02:00
Armel FAUVEAU 5b6174085b Optimize multiboot RAM with a safe Overlay 2026-08-19 17:52:47 +02:00
Armel FAUVEAU 4ffff5149f Many fix presets and Multiboot 2026-08-19 16:23:04 +02:00
Armel FAUVEAU 4c74e79a40 Gate multiboot restore progress bar behind MULTIBOOT_LOW_RAM 2026-08-19 16:21:06 +02:00
Armel FAUVEAU ec0d537b69 Add ENABLE_NO_HEAP to drop the unused reserved heap 2026-08-19 16:19:48 +02:00
Armel FAUVEAU 86e48f5f69 Add slot tag to SysInfo 2026-08-19 14:42:46 +02:00
Armel FAUVEAU 024bfbaaf3 Fix optional UART and USB builds 2026-08-19 05:06:46 +02:00
Armel FAUVEAU 7d8b95407f Document support policy for disabled preset options 2026-08-19 04:17:40 +02:00
Armel FAUVEAU 94f1f978ff Improve startup backlight fade visibility 2026-08-19 04:02:39 +02:00
Armel FAUVEAU 4cc5d11142 Fix VCP reply buffer alignment 2026-08-18 18:02:25 +02:00
Armel FAUVEAU c8b65bb9d8 Many multiboot fix 2026-08-18 17:03:44 +02:00
Armel FAUVEAU 78f5b76a71 Multiboot: per-slot config profiles + restorable Main backup 2026-08-18 03:57:29 +02:00
Armel FAUVEAU 295d4a2c05 Change version 2026-08-17 02:44:28 +02:00
Armel FAUVEAU 435192e3bf Merge pull request #511 from armel/feature_update_v5
Feature update v5
2026-08-14 02:27:10 +02:00
Armel FAUVEAU 9c86a7c349 Prepare v5.9.0 2026-08-14 02:21:59 +02:00
Armel FAUVEAU 0a61a6de12 Update rolling Fusion development firmware 2026-08-13 20:18:09 +02:00
EthanYan6 1021ee5e18 FM scan ignore channel RX 2026-08-13 20:04:41 +02:00
EthanYan6 bef97f787b FM scan ignore channel RX 2026-08-13 19:53:28 +02:00
Armel FAUVEAU ca06478e24 Improve UI 2026-08-12 12:40:12 +02:00
Armel FAUVEAU 919a687936 Refine multiboot presets, UI and build output 2026-08-12 03:25:53 +02:00
Armel FAUVEAU a2551d97dc Improve mb 2026-08-11 22:33:33 +02:00
Armel FAUVEAU a25d2fc75e Add mb firmware slots 2026-08-11 22:33:33 +02:00
Armel FAUVEAU 6692b5191b Update rolling Fusion development firmware 2026-08-11 22:24:36 +02:00
Armel FAUVEAU c65db46bad Add side-key action picker (F + hold side key) 2026-08-11 22:12:25 +02:00
Armel FAUVEAU a38f611bd5 Update rolling Fusion development firmware 2026-08-09 23:51:51 +02:00
Armel FAUVEAU 528678a31b Hold scan resume during scan list name (memory scan only) 2026-08-09 23:44:40 +02:00
Armel FAUVEAU abc146a1b6 Add 2 digits 2026-08-09 17:45:20 +02:00
Armel FAUVEAU c7ff3eeb66 Update rolling Fusion development firmware 2026-08-09 16:55:51 +02:00
Armel FAUVEAU a3c72bdd93 Add menu categories with All fallback 2026-08-09 16:54:45 +02:00
Armel FAUVEAU e4408fcb55 Fix unused searchStart warning in chFrScanner.c 2026-08-09 16:46:58 +02:00
Armel FAUVEAU 5715cd27ef Fix compile-with-docker.sh: pass extra CMake args safely 2026-08-09 16:37:18 +02:00
Armel FAUVEAU 253104bb87 Update rolling Fusion development firmware 2026-08-08 23:37:55 +02:00
Armel FAUVEAU debf67a027 Add Fox Hunt / Beacon keypad lock (long-press F, Discussion #498) 2026-08-08 23:36:38 +02:00
Armel FAUVEAU 535ee80d2b Update rolling Fusion development firmware 2026-08-08 01:34:34 +02:00
Armel FAUVEAU 559460739e Fix Fox Hunt: disable SetOff auto power-off in this mode 2026-08-08 01:33:34 +02:00
Armel FAUVEAU 0d69388e6d Update rolling Fusion development firmware 2026-08-06 04:45:44 +02:00
Armel FAUVEAU 9bf075ea22 Add experiment BYP/BYP+ mode 2026-08-06 04:45:11 +02:00
Armel FAUVEAU fe9c4e9432 Merge pull request #484 from armel/feature_update_v5
Feature update v5
2026-08-04 17:45:07 +02:00
Armel FAUVEAU 1e4698409b Prepare new version 5.8.0 2026-08-04 17:15:56 +02:00
Armel FAUVEAU 39f46a8f93 Update donors 2026-08-04 14:41:27 +02:00
Armel FAUVEAU 3518ab8294 Fix restart command during Beacon TX 2026-08-04 04:09:02 +02:00
Armel FAUVEAU c45b7d2deb Update README 2026-08-04 03:33:12 +02:00
Armel FAUVEAU f904b87172 Update README 2026-08-04 03:28:16 +02:00
Armel FAUVEAU 926787ddf3 Trim Fox Hunt buffers and drop hot-path divisions (-20 B flash) 2026-08-04 02:22:58 +02:00
Armel FAUVEAU fb9491caa5 Factor duplicated Fox Hunt helpers (-32 B flash) 2026-08-04 01:44:35 +02:00
Armel FAUVEAU 04e8d47130 Add F + number reverse-cycle and Beacon TX bitmap 2026-08-04 01:16:06 +02:00
Armel FAUVEAU 3bdddce67b Rework Fox Hunt/Beacon for ARDF: fox IDs, TX window, unified keys 2026-08-03 23:35:02 +02:00
Armel FAUVEAU d5cde97e5b Update rolling Fusion development firmware 2026-08-03 01:18:55 +02:00
Armel FAUVEAU 66ed14dead Show progressive CW beacon text 2026-08-03 01:18:25 +02:00
Armel FAUVEAU 602dfad6f3 Update rolling Fusion development firmware 2026-08-02 17:35:46 +02:00
Armel FAUVEAU 0203586cdc Fix RX mode restore after scan transitions (Issue #480) 2026-08-02 17:35:09 +02:00
Armel FAUVEAU 3e5795255d Update rolling Fusion development firmware 2026-08-02 05:51:35 +02:00
Armel FAUVEAU a8912d3208 Fix: handle restart in modal apps 2026-08-02 05:50:40 +02:00
Armel FAUVEAU fe602b075d Update rolling Fusion development firmware 2026-08-02 05:23:43 +02:00
Armel FAUVEAU e445cfca85 Fox Hunt: factor shared beacon draw code, -156 B flash 2026-08-02 05:22:37 +02:00
Armel FAUVEAU 18e9de38e0 Update rolling Fusion development firmware 2026-08-02 05:03:14 +02:00
Armel FAUVEAU f371795ef8 Update next version 2026-08-02 05:02:48 +02:00
Armel FAUVEAU e9181c2287 Add Fox Hunt signal-history gauge and persistent settings 2026-08-02 05:01:20 +02:00
Armel FAUVEAU 5437bf2d8e Update rolling Fusion development firmware 2026-08-01 23:41:34 +02:00
Armel FAUVEAU b972e026ca Enforce TX gates in Beacon mode: F LOCK, TX LOCK, battery, AM (Issue #478) 2026-08-01 23:40:20 +02:00
Armel FAUVEAU 0cad22f369 Update donors 2026-08-01 18:33:57 +02:00
Armel FAUVEAU 2a13c1819d Update rolling Fusion development firmware 2026-08-01 16:14:56 +02:00
Armel FAUVEAU 4799eb46ac Fix RX mode restore after Close Call (Issue #478) 2026-08-01 16:14:08 +02:00
Armel FAUVEAU 2569e4649d Update rolling Fusion development firmware 2026-08-01 15:36:21 +02:00
Armel FAUVEAU a59f3edcff Add Beacon mode 2026-08-01 15:35:46 +02:00
Armel FAUVEAU 027240de54 Update rolling Fusion development firmware 2026-08-01 03:38:43 +02:00
Armel FAUVEAU 73f44911ed Add Fox Hunt mode 2026-08-01 03:38:03 +02:00
Armel FAUVEAU c0e3909853 Update rolling Fusion development firmware 2026-07-31 02:37:14 +02:00
Armel FAUVEAU 31ff3b4df0 Remove dead empty block 2026-07-31 02:36:23 +02:00
Armel FAUVEAU bbee375623 Update rolling Fusion development firmware 2026-07-30 19:13:04 +02:00
Armel FAUVEAU 12dfa8a87f Optimize RAM usage by making lookup tables const 2026-07-30 19:05:16 +02:00
Armel FAUVEAU c9da65117f Avoid unnecessary flash sector erases 2026-07-30 02:56:06 +02:00
Armel FAUVEAU 222448e1ad Avoid full-buffer channel attribute writes 2026-07-30 02:32:13 +02:00
Armel FAUVEAU e1610bcc6f Avoid fast scan RSSI and chevron overlap 2026-07-29 23:23:30 +02:00
Armel FAUVEAU 7f906300e3 Add opt-in Fusion development firmware build 2026-07-28 18:33:19 +02:00
Armel FAUVEAU bf7eda8a48 Fix false Remove Offset indicator after disabling reverse (issue #469) 2026-07-27 17:09:39 +02:00
Armel FAUVEAU 2ee1d96e5c Smooth power-on backlight fade with smoothstep easing (~470 ms) 2026-07-26 16:53:15 +02:00
Armel FAUVEAU 65ec12bb0d Show active scan list name when it changes during scan (discussion #462) 2026-07-22 04:05:07 +02:00
Armel FAUVEAU dd3fc786b5 Gate RF logging on key assignment 2026-07-20 22:29:15 +02:00
Armel FAUVEAU 10868858c5 Update README 2026-07-19 15:50:34 +02:00
Armel FAUVEAU 3bd3ebba2c Merge pull request #457 from armel/feature_update_v5
Feature update v5
2026-07-19 01:41:58 +02:00
Armel FAUVEAU 39cbbd7e90 Prepare new version v5.7.0 2026-07-19 01:40:08 +02:00
Armel FAUVEAU 0567f01786 Keep RF log indexes traffic-only 2026-07-15 04:55:06 +02:00
Armel FAUVEAU fc12a35418 Update .gitignore 2026-07-15 04:55:06 +02:00
Armel FAUVEAU 2580acfb07 Give session markers a trafficSeq and stream them to K5Viewer 2026-07-13 04:27:33 +02:00
Armel FAUVEAU 56a6083689 Stream paginated RF log history to K5Viewer 2026-07-12 05:15:57 +02:00
Armel FAUVEAU 29725ed704 Allow unlock from RF LOG display 2026-07-11 03:26:47 +02:00
Armel FAUVEAU b44df132d0 Add ACTIONS lock scope to SetLck 2026-07-11 03:07:08 +02:00
Armel FAUVEAU 676f26be34 Remove legacy Python k5viewer tool 2026-07-09 05:25:42 +02:00
Armel FAUVEAU 46034ec39f Rename screenshot module to k5viewer 2026-07-09 05:22:39 +02:00
Armel FAUVEAU e2d21c61c2 Add optional RX/TX log streaming to K5Viewer 2026-07-09 05:12:57 +02:00
Armel FAUVEAU f8e1b7a436 Increase the scanrange skip list from 32 to 64 entries 2026-07-08 03:41:27 +02:00
Armel FAUVEAU 9447eaada9 Add secure clear log confirmation flow 2026-07-07 14:15:02 +02:00
Armel FAUVEAU 43533c838b Reorder RX/TX log flash layout to shrink the view cache 2026-07-07 02:56:22 +02:00
Armel FAUVEAU 7306db5f8f Show TX power level for TX rows in RX/TX log S-meter view 2026-07-07 02:36:16 +02:00
Armel FAUVEAU fbd7b37c21 Log minimum battery voltage per session in RX/TX log 2026-07-07 02:30:15 +02:00
Armel FAUVEAU 8d3754b42f Center S-meter text in the RX/TX log detail badge 2026-07-07 02:18:58 +02:00
Armel FAUVEAU 6a556e0601 Add S-meter to RX/TX log 2026-07-07 00:13:50 +02:00
Armel FAUVEAU 9c1b1cb063 Remove unused REGA emergency alarm feature (ENABLE_REGA) 2026-07-06 15:25:29 +02:00
Armel FAUVEAU ae7bf65383 Ignore squelch-only incoming state in RX/TX log 2026-07-06 12:22:50 +02:00
Armel FAUVEAU fb1360cce2 Replace unreachable RX/TX log EEPROM mapping with documentation comment 2026-07-06 03:52:57 +02:00
Armel FAUVEAU 374e52c4d1 Resolve RX/TX log channel names at display time instead of storing them 2026-07-06 03:35:54 +02:00
Armel FAUVEAU 42aeac3469 Move left by 1 px 2026-07-06 03:18:22 +02:00
Armel FAUVEAU 98ac57b032 Keep status bar layout stable when RX/TX log filter label is shown 2026-07-06 00:11:49 +02:00
Armel FAUVEAU 3e6e7c87b5 Add F + UP/DOWN shortcuts to jump to first/last RX/TX log entry 2026-07-05 23:28:31 +02:00
Armel FAUVEAU e908eda1da Make RX/TX log wrap-around scrolling optional via ENABLE_FEAT_F4HWN_RXTX_LOG_WRAP 2026-07-05 23:19:00 +02:00
Armel FAUVEAU 4fccdb78b9 Dedupe RX/TX log entry copies by aligning RAM and flash struct layouts 2026-07-05 23:01:27 +02:00
Armel FAUVEAU a14127867e Remove redundant name sanitization already done by SETTINGS_FetchChannelName 2026-07-05 22:48:40 +02:00
Armel FAUVEAU c02aac265a Remove unused KEY_STAR 2026-07-05 22:38:36 +02:00
Armel FAUVEAU c24592cbe8 Factor RX/TX log empty screen rendering 2026-07-05 22:33:33 +02:00
Armel FAUVEAU ff096e12e5 Simplify RX/TX log badge numbering and dedupe repeated reset/stop code 2026-07-05 21:01:31 +02:00
Armel FAUVEAU 5fcc1598dd Fix RX/TX log wrap upward before total is known 2026-07-05 20:02:35 +02:00
Armel FAUVEAU 9b8493b450 Fix RX/TX log end-of-list scrolling 2026-07-05 19:16:35 +02:00
Armel FAUVEAU f1df96f1f6 Keep RX/TX log numbering continuous across sessions 2026-07-05 18:44:32 +02:00
Armel FAUVEAU f36c524fa7 Expose 512 entries in RX/TX log view instead of 250 2026-07-05 18:39:40 +02:00
Armel FAUVEAU 9ab5542326 Fix duplicate session marker on repeated reboots without traffic 2026-07-05 18:16:49 +02:00
Armel FAUVEAU 0187c05e65 Add RX/TX history log stored in external flash 2026-07-05 17:59:58 +02:00
Armel FAUVEAU 914c354da0 Update donors 2026-07-02 00:57:02 +02:00
Armel FAUVEAU a771387edd Merge pull request #437 from armel/feature_update_v5
Feature update v5
2026-06-30 16:06:49 +02:00
Armel FAUVEAU 09bc14e64b Add NOTICE 2026-06-30 16:05:51 +02:00
Armel FAUVEAU 58c0170fb1 Fix auto key lock timer on startup (issue #431) 2026-06-29 15:09:55 +02:00
Armel FAUVEAU 5b4b3e2a71 Merge pull request #423 from armel/feature_update_v5
Feature update v5
2026-06-21 19:02:08 +02:00
Armel FAUVEAU 65a5c9f4ef Prepare new version v5.6.1 2026-06-21 19:01:14 +02:00
Armel FAUVEAU a2936402c2 Fix screenshot state updates without display redraw 2026-06-21 18:07:10 +02:00
Armel FAUVEAU f069ed935f Fix screensaver state during deep sleep 2026-06-21 04:28:58 +02:00
Armel FAUVEAU dcbbd744ac Add screenshot state flags 2026-06-21 03:21:16 +02:00
Armel FAUVEAU bca06e3382 Fix NAR+ FM demod boost 2026-06-21 00:17:50 +02:00
Armel FAUVEAU 8723863fb0 Fix F+4 verify VHF harmonic before accepting frequency 2026-06-20 13:41:14 +02:00
Armel FAUVEAU c775fa8b2f Update donors 2026-06-18 02:29:34 +02:00
Armel FAUVEAU 80e7d4b3d1 Fix F+4 frequency scan sensitivity 2026-06-17 03:25:24 +02:00
Armel FAUVEAU e6a727eeda Fix this fucking and stupid roger beep (Issue #416) 2026-06-17 02:32:23 +02:00
Armel FAUVEAU 887e18efe6 Fix reset defaults (Issue #413) 2026-06-16 16:00:56 +02:00
Armel FAUVEAU 20b87feb1d Merge pull request #407 from mrkusypl/feature_update_v5_2
Code refactoring (4 B)
2026-06-15 22:20:52 +02:00
mrkusypl e2e9c9ba60 Code refactoring (4 B) 2026-06-15 20:40:56 +02:00
Armel FAUVEAU 337eb0cb12 Update donors 2026-06-15 16:10:31 +02:00
Armel FAUVEAU 504a2b0fb0 Update .gitignore 2026-06-15 03:15:45 +02:00
Armel FAUVEAU 16b07c6374 Merge pull request #405 from armel/feature_update_v5
Feature update v5
2026-06-15 03:10:46 +02:00
Armel FAUVEAU f5b20d4f80 Prepare v5.6.0 2026-06-15 03:09:32 +02:00
Armel FAUVEAU ae14bf5df3 Hollow out backlight bulb icon when manual light is off 2026-06-13 14:33:42 +02:00
Armel FAUVEAU 9dc85852ba Widen screenshot chunk fingerprint to 16 bits to fix stale chunks 2026-06-13 13:20:10 +02:00
Armel FAUVEAU f7d29f6a14 Save 128 bytes of SRAM in backlight PWM while preserving distinct brightness steps 2026-06-13 03:58:43 +02:00
Armel FAUVEAU 1184618731 Reduce screenshot peak stack usage with on-demand chunk computation 2026-06-13 03:30:22 +02:00
Armel FAUVEAU 3d15d59ce0 Remove dead screenshot bit packing flush 2026-06-12 18:06:44 +02:00
Armel FAUVEAU 15fccc31d6 Fix AM to FM dual-watch RX reconfiguration (issue #389) 2026-06-11 16:49:06 +02:00
Armel FAUVEAU 48f753caaf Reduce screenshot RAM usage with per-chunk frame hashing 2026-06-10 13:46:18 +02:00
Armel FAUVEAU edc4673983 Add subaudible detection for scan ranges 2026-06-09 04:57:15 +02:00
Armel FAUVEAU 9e03f02296 Refine VFO lock icon placement during scan 2026-06-08 04:11:36 +02:00
Armel FAUVEAU 25ea1f827b Add scan RSSI sparkline indicator 2026-06-08 01:50:28 +02:00
Armel FAUVEAU 8f11a32c9d Merge pull request #392 from mrkusypl/feature_update_v5_2
Code refactoring (32 B)
2026-06-06 18:23:54 +02:00
mrkusypl 576b8c552f Code refactoring (32 B) 2026-06-06 17:56:33 +02:00
Armel FAUVEAU 85db5a2394 Update .gitignore 2026-06-03 04:06:57 +02:00
Armel FAUVEAU 98989d4f0a Fix bandscope 8.33 kHz frequency rounding (issue #380) 2026-05-28 00:26:46 +02:00
Armel FAUVEAU 8c976cfffd Update donors 2026-05-25 12:24:23 +02:00
Armel FAUVEAU 0ce5422db3 Fix ENABLE_FEAT_F4HWN_RX_TX_TIMER guard with ENABLE_FEAT_F4HWN_DEBUG 2026-05-25 01:41:27 +02:00
Armel FAUVEAU d939009949 Merge pull request #370 from mrkusypl/feature_update_v5_2
Code refactoring (104 B)
2026-05-25 01:35:25 +02:00
mrkusypl 50efade5c4 Code refactoring (104 B) 2026-05-25 00:46:34 +02:00
Armel FAUVEAU 682a59fc0e Merge pull request #365 from mrkusypl/feature_update_v5_2
Prevent screensaver during M-SCAN; Code refactoring (20 B)
2026-05-23 14:40:01 +02:00
mrkusypl 3d3f370b86 Minor correction for checking gFM_ChannelPosition 2026-05-23 10:48:59 +02:00
mrkusypl eebeba09db Prevent screensaver during M-SCAN; Code refactoring (20 B) 2026-05-23 02:21:11 +02:00
Armel FAUVEAU 6cceb26814 Merge pull request #357 from mrkusypl/feature_update_v5_2
Code refactoring (54 B)
2026-05-17 17:12:44 +02:00
mrkusypl a77b132fa5 Code refactoring (54 B) 2026-05-17 16:48:43 +02:00
Armel FAUVEAU 86aa05f1ec Sync screensaver frames to k5viewer 2026-05-17 12:33:16 +02:00
Armel FAUVEAU 953f579adb Prevent screensaver during FM scan 2026-05-17 02:40:13 +02:00
Armel FAUVEAU d429b3afe8 Prevent screensaver during Beam 2026-05-17 02:34:06 +02:00
Armel FAUVEAU a65a4f601a Improve F+4 frequency copy scan UI 2026-05-17 02:15:22 +02:00
Armel FAUVEAU 546cd35681 Add LOGO+ scrolling SetSav mode 2026-05-13 03:21:43 +02:00
Armel FAUVEAU 8ecf976067 Exit SetSav when waking from SetTmr sleep 2026-05-13 02:49:21 +02:00
Armel FAUVEAU 3e54dc1c00 Merge pull request #350 from mrkusypl/feature_update_v5_2
Code refactoring (16 B)
2026-05-13 01:24:17 +02:00
Armel FAUVEAU 9eb9a9da5c Keep RX blink while reducing LCD redraws 2026-05-13 01:16:57 +02:00
mrkusypl a74529b9ec Code refactoring (16 B) 2026-05-13 01:10:47 +02:00
Armel FAUVEAU 7ccb53572a Fix SetNav is BLTime is OFF 2026-05-13 00:44:17 +02:00
Armel FAUVEAU c515188a06 Improve SetSav from @mrkusypl feedback 2026-05-12 23:19:23 +02:00
Armel FAUVEAU cdcb950ccd Block SetSav during RX 2026-05-12 19:39:27 +02:00
Armel FAUVEAU 13ca3b8a39 Skip SetSav during TX 2026-05-12 18:38:40 +02:00
Armel FAUVEAU 34acff0fbf Throttle SetTmr status refresh 2026-05-12 18:34:10 +02:00
Armel FAUVEAU fa75ab4243 Add SetSav BLMin screensaver 2026-05-12 18:01:46 +02:00
Armel FAUVEAU 1589d8bdd7 Merge pull request #345 from mrkusypl/feature_update_v5_2
Code refactoring (288 B)
2026-05-11 22:59:28 +02:00
mrkusypl a5597a1ab9 Minor corrections to refactoring 2026-05-11 21:43:05 +02:00
mrkusypl 955b245e66 Code refactoring (328 B) 2026-05-11 15:21:48 +02:00
Armel FAUVEAU 2c4bca4202 Archive LOGO 2026-05-10 15:24:01 +02:00
Armel FAUVEAU 6c4c240e9a Enable boot beep with POnMsg LOGO (Issue #340) 2026-05-08 16:57:09 +02:00
Armel FAUVEAU e21d472609 Merge pull request #338 from armel/feature_update_v5
Feature update v5
2026-05-08 03:18:49 +02:00
Armel FAUVEAU 8f89312e99 Add binaries 2026-05-08 03:17:05 +02:00
Armel FAUVEAU ec9fdd17bf Prepare new version v5.5.0 2026-05-07 19:24:59 +02:00
Armel FAUVEAU 1ed550fb92 Add LOGO option to POnMsg menu 2026-05-07 15:32:07 +02:00
Armel FAUVEAU 7d69b74b08 Fix(aircopy): include VFO area in Settings map so scan-range source frequencies are replicated 2026-05-06 01:47:37 +02:00
Armel FAUVEAU f50a29ae08 Merge pull request #334 from mrkusypl/feature_update_v5_2
Code refactoring (224 B)
2026-05-06 00:52:10 +02:00
mrkusypl 7a4226f483 Code refactoring (224 B) 2026-05-06 00:03:26 +02:00
Armel FAUVEAU 4523108293 Skip spectrum curve smoothing in manual mode (issue #333) 2026-05-05 22:00:10 +02:00
Armel FAUVEAU c92e63da0d Restore spectrum predictable 1 dB step on manual squelch line (issue #333) 2026-05-05 21:51:21 +02:00
Armel FAUVEAU 33d949a4be Move BEAM Action 2026-05-05 16:51:53 +02:00
Armel FAUVEAU a9ff58e4a7 Beam: full Apache 2.0 header in beam.c 2026-05-05 04:39:49 +02:00
Armel FAUVEAU b8936ace0e Add beam feature behind ENABLE_FEAT_F4HWN_BEAM flag 2026-05-05 04:35:03 +02:00
Armel FAUVEAU 59ed9cd827 Fix display bug (overlapping) 2026-05-04 22:03:53 +02:00
Armel FAUVEAU a0e4d2708b Merge pull request #331 from mrkusypl/feature_update_v5_2
Displaying VFO_Lock on the second channel while receiving on the first; Code refactoring (42 B)
2026-05-04 19:28:13 +02:00
mrkusypl c0c1d9881c Displaying VFO_Lock on the second channel while receiving on the first; Code refactoring (42 B) 2026-05-04 12:38:43 +02:00
Armel FAUVEAU c1d5b7bd55 Add scan mode menu 2026-05-04 04:56:15 +02:00
Armel FAUVEAU 932d6e954a Add volatile scanrange exclusions 2026-05-04 03:36:40 +02:00
Armel FAUVEAU 3e6b169b7b Improve fast scan detection and display metadata 2026-05-03 22:12:54 +02:00
Armel FAUVEAU ec4ea28325 Reset fast scan RSSI floor on AM/FM changes 2026-05-03 16:49:47 +02:00
Armel FAUVEAU 5157dc573b Fix scan resume, AM/FM precheck and list refresh 2026-05-03 14:39:13 +02:00
Armel FAUVEAU a0aefae01b Fix manual scan resume from active RX 2026-05-03 02:07:32 +02:00
Armel FAUVEAU 46cf7d7d57 Fix Breakout beep audio setup order 2026-05-02 00:43:59 +02:00
Armel FAUVEAU 9a7b199bb9 Merge pull request #325 from mrkusypl/feature_update_v5_2
Refactoring of beep handling (94 B)
2026-05-02 00:41:31 +02:00
mrkusypl 1e03dc4af0 Minor changes to the refactoring of beep handling 2026-05-02 00:35:43 +02:00
mrkusypl 673d1b57ab Refactoring of beep handling (94 B) 2026-05-01 19:39:59 +02:00
Armel FAUVEAU 6ae360d029 Add faster scanning (range & memory) behind ENABLE_FEAT_F4HWN_SCAN_FASTER flag 2026-05-01 18:35:14 +02:00
Armel FAUVEAU d5a29aa241 Show scan range bandwidth instead of unused frequencyChangeStep in scan-range mode 2026-05-01 00:26:08 +02:00
Armel FAUVEAU e0154de186 Fix stuck squelch on quick PTT by seeding listenPrevRssi with peak.rssi at listen entry 2026-05-01 00:04:24 +02:00
Armel FAUVEAU 915083522d Save additional spectrum analyzer parameters 2026-04-30 23:12:52 +02:00
Armel FAUVEAU c6e7ac33e2 Show homologated DCS index on 2 digits (saves 8B) 2026-04-30 00:48:27 +02:00
Armel FAUVEAU bc494a4526 Merge pull request #322 from mrkusypl/feature_update_v5_2
Show DCS codes from 39 to 121; Code refactoring (16 B)
2026-04-30 00:42:05 +02:00
mrkusypl 1ee871cc79 Revert "Show DCS codes from 39 to 121" 2026-04-30 00:01:50 +02:00
mrkusypl b83e4c2491 Show DCS codes from 39 to 121; Code refactoring (16 B) 2026-04-29 20:54:00 +02:00
Armel FAUVEAU c1f8e945c9 Show homologated DCS index in Rx/Tx DCS menus 2026-04-29 19:27:41 +02:00
Armel FAUVEAU 9013f83734 Merge pull request #320 from mrkusypl/feature_update_v5_2
Major code refactoring (412 B)
2026-04-29 18:43:21 +02:00
mrkusypl a46604d8f3 Major code refactoring (412 B) 2026-04-29 17:06:18 +02:00
Armel FAUVEAU 037de071eb Clamp displayed RSSI at S9+40 (-53 dBm) ceiling 2026-04-29 00:41:17 +02:00
Armel FAUVEAU 2c116f9503 Fix S-meter display during scan reception 2026-04-29 00:35:14 +02:00
Armel FAUVEAU 50681ef90b Merge pull request #318 from mrkusypl/feature_update_v5_2
Add long-press functionality to ChName, minor refactoring
2026-04-28 22:46:13 +02:00
mrkusypl 0501422c64 Add long-press functionality to ChName, minor refactoring 2026-04-28 17:45:34 +02:00
Armel FAUVEAU 786318453f Merge pull request #314 from armel/feature_update_v5
Feature update v5
2026-04-28 03:51:29 +02:00
Armel FAUVEAU 61662f3965 Prepare new version v5.4.0 2026-04-28 03:48:04 +02:00
Armel FAUVEAU b3cfda76e5 Merge pull request #313 from mrkusypl/feature_update_v5_2
Code refactoring (68 B)
2026-04-27 22:13:05 +02:00
mrkusypl a20f1ce28e Code refactoring (68 B) 2026-04-27 16:26:49 +02:00
Armel FAUVEAU 8a292755b0 Prepare new version 2026-04-27 15:15:10 +02:00
Armel FAUVEAU 915fc6aa90 SysInf tweak 2026-04-27 15:12:48 +02:00
Armel FAUVEAU cac63f978e Revert "Add UI_PrintStringYOffset to shift big-font"
This reverts commit d5edba1fc2.
2026-04-27 15:03:44 +02:00
Armel FAUVEAU d5edba1fc2 Add UI_PrintStringYOffset to shift big-font 2026-04-27 06:10:44 +02:00
Armel FAUVEAU 2f0852cd03 Add SysInf build details page 2026-04-27 04:12:51 +02:00
Armel FAUVEAU a65f84305e Show firmware version in big font 2026-04-27 04:00:08 +02:00
Armel FAUVEAU 1c0d5ea628 Move SysInf battery data to dedicated page (and patch m lower...) 2026-04-27 03:54:13 +02:00
Armel FAUVEAU a0a5b93408 Merge pull request #312 from mrkusypl/feature_update_v5_2
Code refactoring (12 B)
2026-04-27 02:46:17 +02:00
mrkusypl e8cb32a4ca Code refactoring (12 B) 2026-04-27 02:34:49 +02:00
Armel FAUVEAU 706dea756c Refactor: QR codes only in SysInf menu, drop welcome splash 2026-04-26 23:20:44 +02:00
Armel FAUVEAU 1ea8aba88e Add QR code welcome screen with repo and wiki links 2026-04-26 15:12:24 +02:00
Armel FAUVEAU c2d6a4b751 Fix excluded scan gauge rendering for large channel lists 2026-04-26 04:39:14 +02:00
Armel FAUVEAU db61bce014 Merge pull request #310 from mrkusypl/feature_update_v5_2
Entering text into ChName in a multi-tap system
2026-04-26 03:09:28 +02:00
mrkusypl a3c03f18f0 Fix for string terminator overwriting 2026-04-26 02:24:27 +02:00
mrkusypl 8977a0a112 Entering text into ChName in a multi-tap system 2026-04-26 01:09:57 +02:00
Armel FAUVEAU 09a5e3c1e7 Fix limit bug with scan gauge 2026-04-25 05:09:32 +02:00
Armel FAUVEAU 4fa821e15f Oops ! Preserve scan gauge width calculation, with or without priority labels 2026-04-25 04:50:50 +02:00
Armel FAUVEAU c9a383919a Improve scan progress gauge when priority channels are in use 2026-04-25 04:39:18 +02:00
Armel FAUVEAU e8ba069000 Merge pull request #308 from mrkusypl/feature_update_v5_2
Code refactoring (200 B)
2026-04-25 02:21:18 +02:00
mrkusypl acd2c340ea Code refactoring (200 B) 2026-04-25 02:10:52 +02:00
Armel FAUVEAU 24fd6fd9b1 Improve spectrum trigger line rendering 2026-04-25 00:17:50 +02:00
Armel FAUVEAU 86bb710ca4 Merge pull request #307 from mrkusypl/feature_update_v5_2
Reversing the up/down button functions on the Spectrum
2026-04-24 20:04:32 +02:00
mrkusypl 7148c15f98 Reversing the up/down button functions on the Spectrum 2026-04-24 20:02:05 +02:00
Armel FAUVEAU 2d7093651d Show sweep direction arrow next to A:NORM/WEAK/STRG label 2026-04-24 19:45:20 +02:00
Armel FAUVEAU 874e5aac4b Extract SETTINGS_LoadEepromDtmf to file scope to drop the GCC nested-function extension 2026-04-24 19:35:48 +02:00
Armel FAUVEAU 8051605c16 Guard interlaced sweep code behind SPECTRUM_INTERLACE_LARGE_SWEEPS to silence unused warnings 2026-04-24 19:30:14 +02:00
Armel FAUVEAU 134624825f Merge pull request #306 from mrkusypl/feature_update_v5_2
Code refactoring (232 B)
2026-04-24 18:04:04 +02:00
mrkusypl 7d0fcc8415 Code refactoring (232 B) 2026-04-24 17:51:48 +02:00
Armel FAUVEAU c27b72d6bc Merge pull request #305 from mrkusypl/feature_update_v5_2
Code refactoring (180 B)
2026-04-24 04:49:11 +02:00
Armel FAUVEAU 066c0d0e14 Spectrum experimentation 2026-04-24 04:48:18 +02:00
mrkusypl e77b87f1f2 Another code refactoring (40 B) 2026-04-24 00:09:38 +02:00
mrkusypl 2dd5e1a509 Code refactoring (140 B) 2026-04-23 18:32:39 +02:00
Armel FAUVEAU ab9e97805e Merge pull request #300 from mrkusypl/feature_update_v5_2
Refactoring ScanProgress_DrawGaugeLine
2026-04-23 02:28:38 +02:00
Armel FAUVEAU 65c584ff7d Spectrum/BK4829: interlace large sweeps and cut SPI overhead 2026-04-23 02:27:52 +02:00
mrkusypl 4c76ca1fb4 Refactoring ScanProgress_DrawGaugeLine 2026-04-22 22:33:30 +02:00
Armel FAUVEAU 094896b5f1 UI format progress index with dynamic zero padding based on total digits 2026-04-22 04:00:29 +02:00
Armel FAUVEAU 669dad5b2e Merge pull request #298 from mrkusypl/feature_update_v5_2
Changing the length of the scan progress gauge
2026-04-22 03:31:48 +02:00
Armel FAUVEAU b9902e0c49 UI unify and center top-right menu info badges 2026-04-22 03:30:52 +02:00
mrkusypl 0a955c1f15 Changing the length of the scan progress gauge 2026-04-22 01:54:49 +02:00
Armel FAUVEAU 9f649b5f5b Merge pull request #297 from mrkusypl/feature_update_v5_2
Code refactoring (44 B)
2026-04-21 20:35:33 +02:00
Armel FAUVEAU bb992ca85d Scan progress gauge behind ENABLE_FEAT_F4HWN_SCAN_PROGRESS 2026-04-21 20:34:20 +02:00
mrkusypl 679cf4ea1f Code refactoring (44 B) 2026-04-21 17:26:18 +02:00
Armel FAUVEAU 0004074ffe Fix large-range spectrum stability and manual dB floor behavior 2026-04-21 05:25:28 +02:00
Armel FAUVEAU 35e5d8df90 Fix spectrum manual trigger persistence across scan relaunches 2026-04-21 03:07:23 +02:00
Armel FAUVEAU fa0de5f576 Spectrum: fix and polish manual mode (manualSetFlag) from PR #296 2026-04-21 02:36:32 +02:00
Armel FAUVEAU 4162618473 Merge pull request #296 from mrkusypl/feature_update_v5_2
Code refactoring, improvements in Spectrum
2026-04-21 01:58:39 +02:00
mrkusypl 284af7ca1d Code refactoring, improvements in Spectrum 2026-04-20 23:05:05 +02:00
Armel FAUVEAU c6f1f7b483 Spectrum: align channel name and frequency at x=43 to avoid dBm overlap 2026-04-19 03:21:43 +02:00
Armel FAUVEAU 6cea90d507 Merge pull request #292 from mrkusypl/feature_update_v5_2
Code optimization (80 B), behavior of the EXIT button when editing ChName
2026-04-19 00:29:34 +02:00
Armel FAUVEAU 46608af767 Spectrum refactoring 2026-04-18 18:36:39 +02:00
mrkusypl 24167a7902 Code optimization (80 B), behavior of the EXIT button when editing ChName 2026-04-18 18:30:22 +02:00
Armel FAUVEAU e300c4e7e3 Improve ChName editing with long-press EXIT backspace (issue #281) 2026-04-18 01:09:40 +02:00
Armel FAUVEAU 1a815b315f Update donors 2026-04-18 00:42:49 +02:00
Armel FAUVEAU a7fed2c2d8 Spectrum refactoring 2026-04-18 00:41:24 +02:00
Armel FAUVEAU 9e92037674 Fix build without ENABLE_FMRADIO in APP_TimeSlice500ms 2026-04-15 22:17:05 +02:00
Armel FAUVEAU e536725731 Show homologated CTCSS index in Rx/Tx CTCSS menus 2026-04-15 00:20:13 +02:00
Armel FAUVEAU f59370cee6 Merge pull request #268 from mrkusypl/feature_update_v5_2
Improvements to FM Radio and the UI
2026-04-14 23:11:03 +02:00
mrkusypl bdaf320c08 Fix the channel number display 2026-04-09 10:17:07 +02:00
Armel FAUVEAU fb60d6f7fd Add WIDE/NARROW filter selection for AM RX 2026-04-09 02:44:20 +02:00
Armel FAUVEAU 22295eedcf Update donors 2026-04-08 18:08:50 +02:00
mrkusypl 21c215e9fd Improvements to FM Radio and the UI 2026-04-05 21:01:13 +02:00
Armel FAUVEAU 0061613883 Merge pull request #265 from armel/feature_update_v5
Feature update v5
2026-04-05 06:29:27 +02:00
Armel FAUVEAU aecbcc6409 Update README 2026-04-05 06:27:34 +02:00
Armel FAUVEAU 642dcc9a43 Add support for decoding packed Quansheng stock firmware images 2026-04-03 05:00:38 +02:00
Armel FAUVEAU 2b05990a2c Refactor hidden menu entry to start from first hidden item 2026-04-03 04:28:39 +02:00
Armel FAUVEAU 975084baca Merge pull request #252 from mrkusypl/feature_update_v5_2
Code refactoring (108 B)
2026-04-02 00:06:08 +02:00
mrkusypl 775cd7751a Undo refactoring of AUDIO_Apply(FM/AM)Profile 2026-04-01 13:26:29 +02:00
mrkusypl c5da503cb3 Code refactoring (136 B) 2026-04-01 00:37:44 +02:00
Armel FAUVEAU 5f8a492f4f Merge pull request #251 from armel/feature_update_v5
Update donors
2026-03-31 19:01:33 +02:00
Armel FAUVEAU 8dd5da195e Update donors 2026-03-31 19:00:32 +02:00
180 changed files with 21156 additions and 5019 deletions

No files matched your search

+7 -4
View File
@@ -14,11 +14,14 @@ jobs:
- name: Compile firmware
run: |
chmod +x compile-with-docker.sh
./compile-with-docker.sh
chmod +x compile-firmware.sh compile-app.sh
./compile-firmware.sh All
./compile-app.sh All
- name: Upload firmware artifact
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: firmware-artifact
path: compiled-firmware/f4hwn.packed.bin
path: |
build/*/*.bin
build/Apps/*.app
+12 -2
View File
@@ -8,10 +8,20 @@ firmware
compile_commands.json
.vscode
.claude
.agents
/docs
k5_eeprom.raw
/build
/serialtool/__pycache__
__pycache__/
.DS_Store
.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/
+99 -17
View File
@@ -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
)
@@ -78,6 +79,7 @@ endif()
target_compile_definitions(App INTERFACE
"AUTHOR_STRING=\"${AUTHOR_STRING}\""
"VERSION_STRING=\"${VERSION_STRING}\""
"BUILD_COMMIT=\"${BUILD_COMMIT}\""
)
if(NOT SQL_TONE)
@@ -93,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
)
@@ -116,13 +117,19 @@ 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
app/aircopy.c
ui/aircopy.c
)
enable_feature(ENABLE_NOAA)
@@ -130,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
@@ -150,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)
@@ -165,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)
@@ -194,6 +220,10 @@ enable_feature(ENABLE_FEAT_F4HWN_RESUME_STATE)
enable_feature(ENABLE_FEAT_F4HWN_NARROWER)
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)
@@ -203,6 +233,58 @@ enable_feature(ENABLE_FEAT_F4HWN_GMRS_FRS_MURS)
enable_feature(ENABLE_FEAT_F4HWN_CA)
enable_feature(ENABLE_FEAT_F4HWN_DEBUG)
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 ----
-397
View File
@@ -1,397 +0,0 @@
/* Copyright 2023 OneOfEleven
* https://github.com/DualTachyon
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// code to 'try' and reduce the AM demodulator saturation problem
//
// that is until someone works out how to properly configure the BK chip !
#include <string.h>
#include "am_fix.h"
#include "app/main.h"
#include "board.h"
#include "driver/bk4819.h"
#include "external/printf/printf.h"
#include "frequencies.h"
#include "functions.h"
#include "misc.h"
#include "settings.h"
#ifdef ENABLE_AGC_SHOW_DATA
#include "ui/main.h"
#endif
#ifdef ENABLE_AM_FIX
typedef struct
{
uint16_t reg_val;
int8_t gain_dB;
} __attribute__((packed)) t_gain_table;
// REG_10 AGC gain table
//
// <15:10> ???
//
// <9:8> = LNA Gain Short
// 3 = 0dB < original value
// 2 = -19dB // was -11
// 1 = -24dB // was -16
// 0 = -28dB // was -19
//
// <7:5> = LNA Gain
// 7 = 0dB
// 6 = -2dB
// 5 = -4dB < original value
// 4 = -6dB
// 3 = -9dB
// 2 = -14dB
// 1 = -19dB
// 0 = -24dB
//
// <4:3> = MIXER Gain
// 3 = 0dB < original value
// 2 = -3dB
// 1 = -6dB
// 0 = -8dB
//
// <2:0> = PGA Gain
// 7 = 0dB
// 6 = -3dB < original value
// 5 = -6dB
// 4 = -9dB
// 3 = -15dB
// 2 = -21dB
// 1 = -27dB
// 0 = -33dB
// front end register dB values
//
// these values need to be accurate for the code to properly/reliably switch
// between table entries when adjusting the front end registers.
//
// these 4 tables need a measuring/calibration update
//
//// static const int16_t lna_short_dB[] = { -19, -16, -11, 0}; // was (but wrong)
// static const int16_t lna_short_dB[] = { (-28), (-24), (-19), 0}; // corrected'ish
// static const int16_t lna_dB[] = { (-24), (-19), (-14), ( -9), (-6), (-4), (-2), 0};
// static const int16_t mixer_dB[] = { ( -8), ( -6), ( -3), 0};
// static const int16_t pga_dB[] = { (-33), (-27), (-21), (-15), (-9), (-6), (-3), 0};
// lookup table is hugely easier than writing code to do the same
//
#define LOOKUP_TABLE 1
#if LOOKUP_TABLE
static const t_gain_table gain_table[] =
{
{0x03BE, -7}, // 0 .. 3 5 3 6 .. 0dB -4dB 0dB -3dB .. -7dB original
{0x0000,-93}, // 1 .. 0 0 0 0 .. -28dB -24dB -8dB -33dB .. -93dB
{0x0008,-91}, // 2 .. 0 0 1 0 .. -28dB -24dB -6dB -33dB .. -91dB
{0x0010,-88}, // 3 .. 0 0 2 0 .. -28dB -24dB -3dB -33dB .. -88dB
{0x0001,-87}, // 4 .. 0 0 0 1 .. -28dB -24dB -8dB -27dB .. -87dB
{0x0009,-85}, // 5 .. 0 0 1 1 .. -28dB -24dB -6dB -27dB .. -85dB
{0x0011,-82}, // 6 .. 0 0 2 1 .. -28dB -24dB -3dB -27dB .. -82dB
{0x0002,-81}, // 7 .. 0 0 0 2 .. -28dB -24dB -8dB -21dB .. -81dB
{0x000A,-79}, // 8 .. 0 0 1 2 .. -28dB -24dB -6dB -21dB .. -79dB
{0x0012,-76}, // 9 .. 0 0 2 2 .. -28dB -24dB -3dB -21dB .. -76dB
{0x0003,-75}, // 10 .. 0 0 0 3 .. -28dB -24dB -8dB -15dB .. -75dB
{0x000B,-73}, // 11 .. 0 0 1 3 .. -28dB -24dB -6dB -15dB .. -73dB
{0x0013,-70}, // 12 .. 0 0 2 3 .. -28dB -24dB -3dB -15dB .. -70dB
{0x0004,-69}, // 13 .. 0 0 0 4 .. -28dB -24dB -8dB -9dB .. -69dB
{0x000C,-67}, // 14 .. 0 0 1 4 .. -28dB -24dB -6dB -9dB .. -67dB
{0x000D,-64}, // 15 .. 0 0 1 5 .. -28dB -24dB -6dB -6dB .. -64dB
{0x001C,-61}, // 16 .. 0 0 3 4 .. -28dB -24dB 0dB - 9dB .. -61dB
{0x001D,-58}, // 17 .. 0 0 3 5 .. -28dB -24dB 0dB -6dB .. -58dB
{0x001E,-55}, // 18 .. 0 0 3 6 .. -28dB -24dB 0dB -3dB .. -55dB
{0x001F,-52}, // 19 .. 0 0 3 7 .. -28dB -24dB 0dB 0dB .. -52dB
{0x003E,-50}, // 20 .. 0 1 3 6 .. -28dB -19dB 0dB -3dB .. -50dB
{0x003F,-47}, // 21 .. 0 1 3 7 .. -28dB -19dB 0dB 0dB .. -47dB
{0x005E,-45}, // 22 .. 0 2 3 6 .. -28dB -14dB 0dB -3dB .. -45dB
{0x005F,-42}, // 23 .. 0 2 3 7 .. -28dB -14dB 0dB 0dB .. -42dB
{0x007E,-40}, // 24 .. 0 3 3 6 .. -28dB -9dB 0dB -3dB .. -40dB
{0x007F,-37}, // 25 .. 0 3 3 7 .. -28dB -9dB 0dB 0dB .. -37dB
{0x009F,-34}, // 26 .. 0 4 3 7 .. -28dB -6dB 0dB 0dB .. -34dB
{0x00BF,-32}, // 27 .. 0 5 3 7 .. -28dB -4dB 0dB 0dB .. -32dB
{0x00DF,-30}, // 28 .. 0 6 3 7 .. -28dB -2dB 0dB 0dB .. -30dB
{0x00FF,-28}, // 29 .. 0 7 3 7 .. -28dB 0dB 0dB 0dB .. -28dB
{0x01DF,-26}, // 30 .. 1 6 3 7 .. -24dB -2dB 0dB 0dB .. -26dB
{0x01FF,-24}, // 31 .. 1 7 3 7 .. -24dB 0dB 0dB 0dB .. -24dB
{0x02BF,-23}, // 32 .. 2 5 3 7 .. -19dB -4dB 0dB 0dB .. -23dB
{0x02DF,-21}, // 33 .. 2 6 3 7 .. -19dB -2dB 0dB -0dB .. -21dB
{0x02FF,-19}, // 34 .. 2 7 3 7 .. -19dB 0dB 0dB 0dB .. -19dB
{0x035E,-17}, // 35 .. 3 2 3 6 .. 0dB -14dB 0dB -3dB .. -17dB
{0x035F,-14}, // 36 .. 3 2 3 7 .. 0dB -14dB 0dB 0dB .. -14dB
{0x037E,-12}, // 37 .. 3 3 3 6 .. 0dB -9dB 0dB -3dB .. -12dB
{0x037F,-9}, // 38 .. 3 3 3 7 .. 0dB -9dB 0dB 0dB .. -9dB
{0x038F,-6}, // 39 .. 3 4 3 7 .. 0dB - 6dB 0dB 0dB .. -6dB
{0x03BF,-4}, // 40 .. 3 5 3 7 .. 0dB -4dB 0dB 0dB .. -4dB
{0x03DF,-2}, // 41 .. 3 6 3 7 .. 0dB - 2dB 0dB 0dB .. -2dB
{0x03FF,0} // 42 .. 3 7 3 7 .. 0dB 0dB 0dB 0dB .. 0dB
};
const uint8_t gain_table_size = ARRAY_SIZE(gain_table);
#else
t_gain_table gain_table[100] = {{0x03BE, -7}}; //original
uint8_t gain_table_size = 0;
void CreateTable()
{
typedef union {
struct {
uint8_t pgaIdx:3;
uint8_t mixerIdx:2;
uint8_t lnaIdx:3;
uint8_t lnaSIdx:2;
};
uint16_t __raw;
} GainData;
static const int8_t lna_short_dB[] = {-28, -24, -19, 0}; // corrected'ish
static const int8_t lna_dB[] = {-24, -19, -14, -9, -6, -4, -2, 0};
static const int8_t mixer_dB[] = { -8, -6, -3, 0};
static const int8_t pga_dB[] = {-33, -27, -21, -15, -9, -6, -3, 0};
unsigned i;
for (uint8_t lnaSIdx = 0; lnaSIdx < ARRAY_SIZE(lna_short_dB); lnaSIdx++) {
for (uint8_t lnaIdx = 0; lnaIdx < ARRAY_SIZE(lna_dB); lnaIdx++) {
for (uint8_t mixerIdx = 0; mixerIdx < ARRAY_SIZE(mixer_dB); mixerIdx++) {
for (uint8_t pgaIdx = 0; pgaIdx < ARRAY_SIZE(pga_dB); pgaIdx++) {
int16_t db = lna_short_dB[lnaSIdx] + lna_dB[lnaIdx] + mixer_dB[mixerIdx] + pga_dB[pgaIdx];
GainData gainData = {{
pgaIdx,
mixerIdx,
lnaIdx,
lnaSIdx,
}};
for (i = 1; i < ARRAY_SIZE(gain_table); i++) {
t_gain_table * gain = &gain_table[i];
if (db == gain->gain_dB)
break;
if (db > gain->gain_dB)
continue;
if (db < gain->gain_dB) {
if(gain->gain_dB)
memmove(gain + 1, gain, 100 - i);
gain->gain_dB = db;
gain->reg_val = gainData.__raw;
break;
}
gain->gain_dB = db;
gain->reg_val = gainData.__raw;
break;
}
}
}
}
}
gain_table_size = i+1;
}
#endif
#ifdef ENABLE_AM_FIX_SHOW_DATA
// display update rate
static const unsigned int display_update_rate = 250 / 10; // max 250ms display update rate
unsigned int counter = 0;
#endif
unsigned int gain_table_index[2] = {0, 0};
// used simply to detect a changed gain setting
unsigned int gain_table_index_prev[2] = {0, 0};
// holds the previous RSSI level .. we do an average of old + new RSSI reading
int16_t prev_rssi[2] = {0, 0};
// to help reduce gain hunting, peak hold count down tick
unsigned int hold_counter[2] = {0, 0};
// -89dBm, any higher and the AM demodulator starts to saturate/clip/distort
const int16_t desired_rssi = (-89 + 160) * 2;
int8_t currentGainDiff;
bool enabled = true;
void AM_fix_init(void)
{ // called at boot-up
for (int i = 0; i < 2; i++) {
gain_table_index[i] = 0; // re-start with original QS setting
}
#if !LOOKUP_TABLE
CreateTable();
#endif
}
void AM_fix_reset(const unsigned vfo)
{ // reset the AM fixer upper
if (vfo > 1)
return;
#ifdef ENABLE_AM_FIX_SHOW_DATA
counter = 0;
#endif
prev_rssi[vfo] = 0;
hold_counter[vfo] = 0;
gain_table_index_prev[vfo] = 0;
}
// adjust the RX gain to try and prevent the AM demodulator from
// saturating/overloading/clipping (distorted AM audio)
//
// we're actually doing the BK4819's job for it here, but as the chip
// won't/don't do it for itself, we're left to bodging it ourself by
// playing with the RF front end gain setting
//
void AM_fix_10ms(const unsigned vfo)
{
if(!gSetting_AM_fix || !enabled || vfo > 1 )
return;
if (gCurrentFunction != FUNCTION_FOREGROUND && !FUNCTION_IsRx()) {
#ifdef ENABLE_AM_FIX_SHOW_DATA
counter = display_update_rate; // queue up a display update as soon as we switch to RX mode
#endif
return;
}
#ifdef ENABLE_AM_FIX_SHOW_DATA
if (counter > 0) {
if (++counter >= display_update_rate) { // trigger a display update
counter = 0;
gUpdateDisplay = true;
}
}
#endif
static uint32_t lastFreq[2];
if(gEeprom.VfoInfo[vfo].pRX->Frequency != lastFreq[vfo]) {
lastFreq[vfo] = gEeprom.VfoInfo[vfo].pRX->Frequency;
AM_fix_reset(vfo);
}
int16_t rssi;
{ // sample the current RSSI level
// average it with the previous rssi (a bit of noise/spike immunity)
const int16_t new_rssi = BK4819_GetRSSI();
rssi = (prev_rssi[vfo] > 0) ? (prev_rssi[vfo] + new_rssi) / 2 : new_rssi;
prev_rssi[vfo] = new_rssi;
}
#ifdef ENABLE_AM_FIX_SHOW_DATA
{
static int16_t lastRssi;
if (lastRssi != rssi) { // rssi changed
lastRssi = rssi;
if (counter == 0) {
counter = 1;
gUpdateDisplay = true; // trigger a display update
}
}
}
#endif
// automatically adjust the RF RX gain
// update the gain hold counter
if (hold_counter[vfo] > 0)
hold_counter[vfo]--;
// dB difference between actual and desired RSSI level
int16_t diff_dB = (rssi - desired_rssi) / 2;
if (diff_dB > 0) { // decrease gain
unsigned int index = gain_table_index[vfo]; // current position we're at
if (diff_dB >= 10) { // jump immediately to a new gain setting
// this greatly speeds up initial gain reduction (but reduces noise/spike immunity)
const int16_t desired_gain_dB = (int16_t)gain_table[index].gain_dB - diff_dB + 8; // get no closer than 8dB (bit of noise/spike immunity)
// scan the table to see what index to jump straight too
while (index > 1)
if (gain_table[--index].gain_dB <= desired_gain_dB)
break;
}
else
{ // incrementally reduce the gain .. taking it slow improves noise/spike immunity
if (index > 1)
index--; // slow step-by-step gain reduction
}
index = MAX(1u, index);
if (gain_table_index[vfo] != index)
{
gain_table_index[vfo] = index;
hold_counter[vfo] = 30; // 300ms hold
}
}
if (diff_dB >= -6) // 6dB hysterisis (help reduce gain hunting)
hold_counter[vfo] = 30; // 300ms hold
if (hold_counter[vfo] == 0)
{ // hold has been released, we're free to increase gain
const unsigned int index = gain_table_index[vfo] + 1; // move up to next gain index
gain_table_index[vfo] = MIN(index, gain_table_size - 1u);
}
{ // apply the new settings to the front end registers
const unsigned int index = gain_table_index[vfo];
// remember the new table index
gain_table_index_prev[vfo] = index;
currentGainDiff = gain_table[0].gain_dB - gain_table[index].gain_dB;
BK4819_WriteRegister(BK4819_REG_13, gain_table[index].reg_val);
#ifdef ENABLE_AGC_SHOW_DATA
UI_MAIN_PrintAGC(true);
#endif
}
#ifdef ENABLE_AM_FIX_SHOW_DATA
if (counter == 0) {
counter = 1;
gUpdateDisplay = true;
}
#endif
}
#ifdef ENABLE_AM_FIX_SHOW_DATA
void AM_fix_print_data(const unsigned vfo, char *s) {
if (s != NULL && vfo < ARRAY_SIZE(gain_table_index)) {
const unsigned int index = gain_table_index[vfo];
sprintf(s, "%2u %4ddB %3u", index, gain_table[index].gain_dB, prev_rssi[vfo]);
counter = 0;
}
}
#endif
int8_t AM_fix_get_gain_diff()
{
return currentGainDiff;
}
void AM_fix_enable(bool on)
{
enabled = on;
}
#endif
+268 -138
View File
@@ -25,12 +25,15 @@
#ifdef ENABLE_FLASHLIGHT
#include "app/flashlight.h"
#endif
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
#include "app/fm.h"
#endif
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_APPS
#include "apps/app_overlay.h"
#endif
#include "app/scanner.h"
#include "audio.h"
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
#include "driver/bk1080.h"
#endif
#include "driver/bk4819.h"
@@ -40,24 +43,36 @@
#include "misc.h"
#include "settings.h"
#include "ui/inputbox.h"
#include "ui/main.h"
#include "ui/ui.h"
#ifdef ENABLE_REGA
#include "app/rega.h"
#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,
@@ -69,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
@@ -108,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
@@ -116,24 +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,
#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)
{
@@ -180,7 +215,7 @@ void ACTION_Monitor(void)
RADIO_SetupRegisters(true);
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
if (gFmRadioMode) {
FM_Start();
gRequestDisplayScreen = DISPLAY_FM;
@@ -194,7 +229,7 @@ void ACTION_Scan(bool bRestart)
{
(void)bRestart;
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
if (gFmRadioMode) {
ACTION_Scan_FM(bRestart);
return;
@@ -212,7 +247,7 @@ void ACTION_Scan(bool bRestart)
DTMF_clear_RX();
#endif
gDTMF_RX_live_timeout = 0;
memset(gDTMF_RX_live, 0, sizeof(gDTMF_RX_live));
DTMF_clear_input_box_memory();
RADIO_SelectVfos();
@@ -237,15 +272,14 @@ void ACTION_Scan(bool bRestart)
// channel mode. Keep scanning but toggle between scan lists
RADIO_NextValidList(1);
UI_MAIN_NotifyScanListChanged();
#ifdef ENABLE_FEAT_F4HWN_RESUME_STATE
SETTINGS_WriteCurrentState();
#endif
// jump to the next channel
CHFRSCANNER_Start(false, gScanStateDir);
gScanPauseDelayIn_10ms = 1;
gScheduleScanListen = false;
CHFRSCANNER_ManualResume(gScanStateDir);
} else {
#ifdef ENABLE_FEAT_F4HWN_RESUME_STATE
if(gScanRangeStart == 0) // No ScanRange
@@ -288,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();
@@ -343,62 +494,58 @@ void ACTION_Handle(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
break;
}
if (!bKeyHeld && bKeyPressed) // button pushed
{
if (bKeyHeld != bKeyPressed) { // button pushed or released after hold
// (!bKeyHeld && bKeyPressed) or (bKeyHeld && !bKeyPressed)
return;
}
// held or released beyond this point
// held or released after short press
ACTION_Execute(func);
}
if(!(bKeyHeld && !bKeyPressed)) // don't beep on released after hold
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
if (bKeyHeld && !bKeyPressed) // button released after hold
{
return;
}
// held or released after short press beyond this point
#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
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;
@@ -414,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);
@@ -423,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())
@@ -464,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
@@ -514,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)
{
@@ -597,7 +732,7 @@ void ACTION_Wn(void)
if (pVfo->Modulation == MODULATION_AM)
{
BK4819_SetFilterBandwidth(BK4819_FILTER_BW_AM, true);
BK4819_SetFilterBandwidth(RADIO_GetAMFilterBandwidth(pVfo), true);
return;
}
@@ -610,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)
@@ -656,30 +787,29 @@ 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;
BK4819_WriteRegister(BK4819_REG_48,
(11u << 12) | // ??? .. 0 ~ 15, doesn't seem to make any difference
(0u << 10) | // AF Rx Gain-1
(gEeprom.VOLUME_GAIN << 4) | // AF Rx Gain-2
(gEeprom.DAC_GAIN << 0)); // AF DAC Gain (after Gain-1 and Gain-2)
BK4819_SetRxAudioGain();
gUpdateStatus = true;
}
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
void ACTION_ToggleVfoSetting(bool *setting) {
*setting = !(*setting);
gVfoConfigureMode = VFO_CONFIGURE_RELOAD;
}
void ACTION_Power_High(void)
{
gPowerHigh = !gPowerHigh;
gVfoConfigureMode = VFO_CONFIGURE_RELOAD;
ACTION_ToggleVfoSetting(&gPowerHigh);
}
void ACTION_Remove_Offset(void)
{
gRemoveOffset = !gRemoveOffset;
gVfoConfigureMode = VFO_CONFIGURE_RELOAD;
ACTION_ToggleVfoSetting(&gRemoveOffset);
}
#endif
#endif
+14 -4
View File
@@ -17,6 +17,9 @@
#ifndef APP_ACTION_H
#define APP_ACTION_H
#include <stdbool.h>
#include <stdint.h>
#include "driver/keyboard.h"
void ACTION_Power(void);
@@ -31,10 +34,6 @@ void ACTION_Scan(bool bRestart);
#endif
void ACTION_SwitchDemodul(void);
#ifdef ENABLE_BLMIN_TMP_OFF
void ACTION_BlminTmpOff(void);
#endif
#ifdef ENABLE_FEAT_F4HWN
void ACTION_RxMode(void);
void ACTION_MainOnly(void);
@@ -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
+256 -219
View File
@@ -21,6 +21,7 @@
#include "driver/bk4819.h"
#include "driver/crc.h"
#include "driver/eeprom.h"
#include "driver/system.h"
#include "frequencies.h"
#include "misc.h"
#include "radio.h"
@@ -29,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,209 +42,204 @@ 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 },
};
static const AIRCOPY_TransferMap_t AIRCOPY_Map_Settings = {
.segments = AIRCOPY_Segments_Settings,
.num_segments = 2,
.total_blocks = 8
};
// 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(void)
{
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
}
}
static inline void AIRCOPY_Obfuscation(void)
void AIRCOPY_Obfuscate(unsigned int count)
{
for (unsigned int i = 0; i < 34; i++) {
for (unsigned int i = 0; i < count; i++) {
g_FSK_Buffer[i + 1] ^= Obfuscation[i % 8];
}
}
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_Obfuscation();
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)
@@ -252,74 +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) {
gErrorsDuringAirCopy++;
BK4819_ResetFSK(); // <- important
BK4819_PrepareFSKReceive(); // <- re-arm proprement
AIRCOPY_CheckComplete();
return;
}
AIRCOPY_Obfuscation();
uint16_t Crc = CRC_Calculate(&g_FSK_Buffer[1], 2 + 64);
if (g_FSK_Buffer[34] != Crc) {
gErrorsDuringAirCopy++;
AIRCOPY_CheckComplete();
return;
}
uint16_t Offset = g_FSK_Buffer[1];
const AIRCOPY_Segment_t *seg = AIRCOPY_FindSegmentForOffset(Offset);
if (seg == NULL) {
gErrorsDuringAirCopy++;
AIRCOPY_CheckComplete();
return;
}
if (seg->write_mode == AIRCOPY_WRITE_BYTES)
if (valid)
{
/* Raw bytes stream, written in 8-byte EEPROM chunks */
const uint8_t *p8 = (const uint8_t *)&g_FSK_Buffer[2];
for (unsigned int i = 0; i < 8; i++)
{
EEPROM_WriteBuffer(Offset + (i * 8), p8 + (i * 8));
}
AIRCOPY_Obfuscate(34);
valid = g_FSK_Buffer[34] ==
CRC_Calculate(&g_FSK_Buffer[0], 4 + AIRCOPY_BLOCK_SIZE);
}
else
if (gAirCopyIsSendMode)
{
/* Structured data path (kept as-is) */
const uint16_t *pData = &g_FSK_Buffer[2];
for (unsigned int i = 0; i < 8; i++)
if (!valid)
{
EEPROM_WriteBuffer(Offset, pData);
pData += 4;
Offset += 8;
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++;
AIRCOPY_CheckComplete();
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;
@@ -373,8 +408,6 @@ static void AIRCOPY_Key_EXIT()
{
if (gInputBoxIndex == 0) {
AIRCOPY_InitTransfer(0); // Mode: Receive
gErrorsDuringAirCopy = lErrorsDuringAirCopy = 0;
BK4819_PrepareFSKReceive();
} else {
@@ -385,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)
@@ -400,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;
}
}
@@ -414,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;
}
@@ -442,4 +479,4 @@ void AIRCOPY_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
gRequestDisplayScreen = DISPLAY_AIRCOPY;
}
#endif
#endif
+10 -47
View File
@@ -28,53 +28,10 @@
#define AIRCOPY_BLOCK_SIZE 0x0040u // 64 bytes per AirCopy block
#define AIRCOPY_CHANNELS_PER_BANK 128
#define AIRCOPY_NUM_BANKS MR_CHANNELS_MAX / AIRCOPY_CHANNELS_PER_BANK
#define AIRCOPY_CHANNEL_SIZE 16 // bytes per channel (freq/name)
#define AIRCOPY_BANK_SIZE_BYTES 0x1080u // 0x800 (Freq) + 0x800 (Name) + 0x80 (Attr)
#define AIRCOPY_NUM_MAPS (AIRCOPY_NUM_BANKS + 1u) // banks + one settings map
#define AIRCOPY_ALL_INDEX AIRCOPY_NUM_MAPS // selection sentinel: send/receive everything
#define AIRCOPY_BAR_WIDTH 120 // Visible width of the progress gauge
// ============================================================================
// Segment write mode
// ============================================================================
/*
* Defines how a segment must be written to EEPROM.
*
* - STRUCT: structured data (frequencies, names)
* - BYTES : raw byte stream (attributes, settings, etc.)
*/
typedef enum {
AIRCOPY_WRITE_STRUCT = 0,
AIRCOPY_WRITE_BYTES = 1,
} AIRCOPY_WriteMode_t;
// ============================================================================
// Transfer segment structure
// ============================================================================
/*
* Describes a contiguous EEPROM region involved in AirCopy.
* The write_mode defines how the RX side must write the data.
*/
typedef struct {
uint16_t start_offset;
uint16_t end_offset;
AIRCOPY_WriteMode_t write_mode;
} AIRCOPY_Segment_t;
// ============================================================================
// Transfer map structure
// ============================================================================
/*
* A transfer map is a collection of segments describing
* one complete AirCopy operation (bank, settings, etc.).
*/
typedef struct {
const AIRCOPY_Segment_t *segments;
uint16_t num_segments;
uint16_t total_blocks;
} AIRCOPY_TransferMap_t;
// ============================================================================
// AirCopy state
// ============================================================================
@@ -82,7 +39,8 @@ typedef struct {
typedef enum {
AIRCOPY_READY = 0,
AIRCOPY_TRANSFER,
AIRCOPY_COMPLETE
AIRCOPY_COMPLETE,
AIRCOPY_FAILED
} AIRCOPY_State_t;
// ============================================================================
@@ -93,6 +51,7 @@ extern AIRCOPY_State_t gAircopyState;
extern uint16_t gAirCopyBlockNumber;
extern uint16_t gErrorsDuringAirCopy;
extern bool gAirCopyIsSendMode;
extern bool gAircopyAll; // All mode: banks + settings in one pass
extern uint16_t g_FSK_Buffer[36];
@@ -103,8 +62,12 @@ 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);
uint16_t AIRCOPY_GetTotalBlocks(void);
uint8_t AIRCOPY_CurrentSliceMap(void); // map index of the block in progress (All slice label)
const AIRCOPY_TransferMap_t* AIRCOPY_GetCurrentMap(void);
// XOR-obfuscate `count` words of g_FSK_Buffer starting at index 1.
// Self-inverse: applying twice restores the original buffer.
void AIRCOPY_Obfuscate(unsigned int count);
#endif // ENABLE_AIRCOPY
#endif // APP_AIRCOPY_H
+599 -154
View File
File diff suppressed because it is too large. Load diff
+4 -1
View File
@@ -24,12 +24,15 @@
#include "radio.h"
void APP_EndTransmission(void);
void APP_HandleEndTransmission(void);
void APP_StartListening(FUNCTION_Type_t function);
uint32_t APP_SetFreqByStepAndLimits(VFO_Info_t *pInfo, int8_t direction, uint32_t lower, uint32_t upper);
uint32_t APP_SetFrequencyByStep(VFO_Info_t *pInfo, int8_t direction);
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
+314
View File
@@ -0,0 +1,314 @@
/* 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.
*/
#ifdef ENABLE_FEAT_F4HWN_BEAM
#include <assert.h>
#include <string.h>
#include "app/aircopy.h"
#include "app/beam.h"
#include "audio.h"
#include "driver/bk4819.h"
#include "driver/crc.h"
#include "driver/st7565.h"
#include "frequencies.h"
#include "misc.h"
#include "radio.h"
#include "settings.h"
#include "ui/main.h"
#include "ui/ui.h"
#define BEAM_PACKET_MAGIC 0xBEA5u
#define BEAM_PACKET_VERSION 2u
// One byte per field instead of bit-packing — payload still fits in 64 bytes,
// and avoids costly shift/mask/store sequences on Cortex-M.
typedef struct {
uint16_t magic;
uint8_t version;
uint8_t _pad;
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];
} BEAM_Payload_t;
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;
bool gBeamActive;
static VFO_Info_t gBeamRadioVfo;
static void BEAM_SetRadioToBeamFrequency(void)
{
const uint16_t channel = FREQ_CHANNEL_FIRST + BAND6_400MHz;
RADIO_InitInfo(&gBeamRadioVfo, channel, DEFAULT_FREQ);
gBeamRadioVfo.CHANNEL_BANDWIDTH = BANDWIDTH_NARROW;
gBeamRadioVfo.OUTPUT_POWER = OUTPUT_POWER_LOW1;
RADIO_ConfigureSquelchAndOutputPower(&gBeamRadioVfo);
gRxVfo = &gBeamRadioVfo;
gTxVfo = &gBeamRadioVfo;
gCurrentVfo = gRxVfo;
RADIO_SetupRegisters(true);
BK4819_SetupAircopy();
BK4819_ResetFSK();
}
static void BEAM_SendPacket(void)
{
memset(g_FSK_Buffer, 0, sizeof(g_FSK_Buffer));
g_FSK_Buffer[0] = 0xABCDu;
BEAM_Payload_t * const payload = (BEAM_Payload_t *)&g_FSK_Buffer[2];
const VFO_Info_t *vfo = &gEeprom.VfoInfo[gEeprom.TX_VFO];
payload->magic = BEAM_PACKET_MAGIC;
payload->version = BEAM_PACKET_VERSION;
payload->rx_frequency = vfo->freq_config_RX.Frequency;
payload->tx_offset_frequency = vfo->TX_OFFSET_FREQUENCY;
payload->rx_code = vfo->freq_config_RX.Code;
payload->tx_code = vfo->freq_config_TX.Code;
payload->rx_codetype = vfo->freq_config_RX.CodeType;
payload->tx_codetype = vfo->freq_config_TX.CodeType;
payload->modulation = vfo->Modulation;
payload->tx_offset_direction = vfo->TX_OFFSET_FREQUENCY_DIRECTION;
payload->tx_lock = vfo->TX_LOCK;
payload->busy_channel_lock = vfo->BUSY_CHANNEL_LOCK;
payload->output_power = vfo->OUTPUT_POWER;
payload->channel_bandwidth = vfo->CHANNEL_BANDWIDTH;
payload->frequency_reverse = vfo->FrequencyReverse;
payload->dtmf_ptt_id_mode = vfo->DTMF_PTT_ID_TX_MODE;
#ifdef ENABLE_DTMF_CALLING
payload->dtmf_decoding_enable = vfo->DTMF_DECODING_ENABLE;
#endif
payload->step_setting = vfo->STEP_SETTING;
payload->band = vfo->Band;
payload->scanlist = vfo->SCANLIST_PARTICIPATION;
payload->compander = vfo->Compander;
if (IS_MR_CHANNEL(vfo->CHANNEL_SAVE)) {
SETTINGS_FetchChannelName(payload->name, vfo->CHANNEL_SAVE);
} else {
memcpy(payload->name, vfo->Name, sizeof(vfo->Name));
}
g_FSK_Buffer[34] = CRC_Calculate(&g_FSK_Buffer[1], 2 + 64);
g_FSK_Buffer[35] = 0xDCBAu;
AIRCOPY_Obfuscate(32);
// Show "SENDING" before the blocking FSK transmission so the user gets
// immediate visual feedback instead of a frozen "BEAM TX" screen.
gBeamStatus = BEAM_STATUS_TX_WAIT;
UI_DisplayMain();
ST7565_BlitFullScreen();
RADIO_SetTxParameters();
BK4819_SendFSKData(g_FSK_Buffer);
BK4819_SetupPowerAmplifier(0, 0);
BK4819_ToggleGpioOut(BK4819_GPIO1_PIN29_PA_ENABLE, false);
RADIO_SelectVfos();
RADIO_SetupRegisters(true);
gBeamStatus = BEAM_STATUS_TX_DONE;
gBeepToPlay = BEEP_880HZ_60MS_TRIPLE_BEEP;
gUpdateDisplay = true;
}
static void BEAM_SavePayloadToFirstFreeChannel(const BEAM_Payload_t *payload)
{
uint16_t channel = 0xFFFFu;
for (uint16_t c = MR_CHANNEL_FIRST; IS_MR_CHANNEL(c); c++) {
if (!RADIO_CheckValidChannel(c, false, 0)) {
channel = c;
break;
}
}
if (channel == 0xFFFFu) {
gBeamStatus = BEAM_STATUS_RX_FULL;
gUpdateDisplay = true;
return;
}
VFO_Info_t vfo;
RADIO_InitInfo(&vfo, channel, payload->rx_frequency);
vfo.TX_OFFSET_FREQUENCY = payload->tx_offset_frequency;
vfo.freq_config_RX.Code = payload->rx_code;
vfo.freq_config_TX.Code = payload->tx_code;
vfo.freq_config_RX.CodeType = payload->rx_codetype;
vfo.freq_config_TX.CodeType = payload->tx_codetype;
vfo.TX_OFFSET_FREQUENCY_DIRECTION = payload->tx_offset_direction;
vfo.Modulation = payload->modulation;
vfo.TX_LOCK = payload->tx_lock;
vfo.BUSY_CHANNEL_LOCK = payload->busy_channel_lock;
vfo.OUTPUT_POWER = payload->output_power;
vfo.CHANNEL_BANDWIDTH = payload->channel_bandwidth;
vfo.FrequencyReverse = payload->frequency_reverse;
vfo.DTMF_PTT_ID_TX_MODE = payload->dtmf_ptt_id_mode;
#ifdef ENABLE_DTMF_CALLING
vfo.DTMF_DECODING_ENABLE = payload->dtmf_decoding_enable;
#endif
vfo.STEP_SETTING = payload->step_setting < STEP_N_ELEM ? payload->step_setting : STEP_12_5kHz;
vfo.StepFrequency = gStepFrequencyTable[vfo.STEP_SETTING];
vfo.SCANLIST_PARTICIPATION = payload->scanlist;
vfo.Compander = payload->compander;
memcpy(vfo.Name, payload->name, sizeof(vfo.Name));
vfo.Name[sizeof(vfo.Name) - 1] = '\0';
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;
gUpdateDisplay = true;
BACKLIGHT_TurnOn();
return;
}
static void BEAM_KeyMenu(void)
{
BEAM_SetRadioToBeamFrequency();
if (gBeamMode == BEAM_MODE_TX) {
BEAM_SendPacket();
} else {
gBeamStatus = BEAM_STATUS_RX_WAIT;
gBeamCopiedChannel = 0xFFFFu;
gFSKWriteIndex = 0;
BK4819_PrepareFSKReceive();
}
}
static void BEAM_KeyExit(void)
{
BK4819_ResetFSK();
if (gBeamMode == BEAM_MODE_RX && gBeamCopiedChannel != 0xFFFFu) {
gEeprom.MrChannel[gEeprom.TX_VFO] = gBeamCopiedChannel;
gEeprom.ScreenChannel[gEeprom.TX_VFO] = gBeamCopiedChannel;
RADIO_ConfigureChannel(gEeprom.TX_VFO, VFO_CONFIGURE_RELOAD);
}
RADIO_SelectVfos();
RADIO_SetupRegisters(true);
GUI_SelectNextDisplay(DISPLAY_MAIN);
gBeamActive = false;
}
void ACTION_Beam(void)
{
gBeamMode = BEAM_MODE_TX;
gBeamStatus = BEAM_STATUS_READY;
gBeamCopiedChannel = 0xFFFFu;
gBeamActive = true;
GUI_SelectNextDisplay(DISPLAY_MAIN);
}
void BEAM_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
{
if (bKeyHeld || !bKeyPressed)
return;
if (Key != KEY_PTT)
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
switch (Key) {
case KEY_UP:
case KEY_DOWN:
gBeamMode ^= 1; // (gBeamMode == BEAM_MODE_TX) ? BEAM_MODE_RX : BEAM_MODE_TX
gBeamStatus = BEAM_STATUS_READY;
break;
case KEY_MENU:
BEAM_KeyMenu();
break;
case KEY_EXIT:
BEAM_KeyExit();
return;
case KEY_PTT:
break;
default:
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
break;
}
gUpdateDisplay = true;
}
void BEAM_StorePacket(void)
{
if (gFSKWriteIndex < 36)
return;
gFSKWriteIndex = 0;
const uint16_t Status = BK4819_ReadRegister(BK4819_REG_0B);
BK4819_PrepareFSKReceive();
if ((Status & 0x0010U) != 0 || g_FSK_Buffer[0] != 0xABCDu || g_FSK_Buffer[35] != 0xDCBAu)
goto error;
AIRCOPY_Obfuscate(32);
if (g_FSK_Buffer[34] != CRC_Calculate(&g_FSK_Buffer[1], 2 + 64))
goto error;
BEAM_Payload_t * const payload = (BEAM_Payload_t *)&g_FSK_Buffer[2];
if (payload->magic != BEAM_PACKET_MAGIC || payload->version != BEAM_PACKET_VERSION)
goto error;
BEAM_SavePayloadToFirstFreeChannel(payload);
BK4819_ResetFSK();
return;
error:
gBeamStatus = BEAM_STATUS_ERROR;
gUpdateDisplay = true;
BACKLIGHT_TurnOn();
}
#endif // ENABLE_FEAT_F4HWN_BEAM
+52
View File
@@ -0,0 +1,52 @@
/* 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_BEAM_H
#define APP_BEAM_H
#ifdef ENABLE_FEAT_F4HWN_BEAM
#include <stdbool.h>
#include <stdint.h>
#include "driver/keyboard.h"
typedef enum {
BEAM_MODE_TX = 0,
BEAM_MODE_RX
} BEAM_Mode_t;
typedef enum {
BEAM_STATUS_READY = 0,
BEAM_STATUS_TX_DONE,
BEAM_STATUS_RX_WAIT,
BEAM_STATUS_RX_SAVED,
BEAM_STATUS_RX_FULL,
BEAM_STATUS_ERROR,
BEAM_STATUS_TX_WAIT
} BEAM_Status_t;
extern BEAM_Mode_t gBeamMode;
extern BEAM_Status_t gBeamStatus;
extern uint16_t gBeamCopiedChannel;
extern bool gBeamActive;
void ACTION_Beam(void);
void BEAM_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld);
void BEAM_StorePacket(void);
#endif // ENABLE_FEAT_F4HWN_BEAM
#endif // APP_BEAM_H
+134 -237
View File
@@ -16,18 +16,29 @@
#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;
static bool isInitialized = false;
bool isPaused = false;
bool isBeep = false;
uint8_t levelCountBreackout = 1;
uint8_t levelCountBreakout = 1;
uint16_t tone = 0;
uint16_t score = 0;
@@ -42,115 +53,7 @@ Brick brick[BRICK_NUMBER];
Racket racket;
Ball ball;
const uint8_t BITMAP_block[4][15] =
{
{
0b00011110,
0b00110001,
0b00101001,
0b00100101,
0b00100011,
0b00110001,
0b00101001,
0b00100101,
0b00100011,
0b00110001,
0b00101001,
0b00100101,
0b00100011,
0b00110001,
0b00011110,
},
{
0b00011110,
0b00101001,
0b00100101,
0b00100011,
0b00110001,
0b00101001,
0b00100101,
0b00100011,
0b00110001,
0b00101001,
0b00100101,
0b00100011,
0b00110001,
0b00101001,
0b00011110,
},
{
0b00011110,
0b00100101,
0b00100011,
0b00110001,
0b00101001,
0b00100101,
0b00100011,
0b00110001,
0b00101001,
0b00100101,
0b00100011,
0b00110001,
0b00101001,
0b00100101,
0b00011110,
},
{
0b00011110,
0b00100011,
0b00110001,
0b00101001,
0b00100101,
0b00100011,
0b00110001,
0b00101001,
0b00100101,
0b00100011,
0b00110001,
0b00101001,
0b00100101,
0b00100011,
0b00011110,
}
};
const uint8_t BITMAP_blockOn[15] =
{
0b00011110,
0b00111111,
0b00111111,
0b00111111,
0b00111111,
0b00111111,
0b00111111,
0b00111111,
0b00111111,
0b00111111,
0b00111111,
0b00111111,
0b00111111,
0b00111111,
0b00011110,
};
const uint8_t BITMAP_blockEmpty[15] =
{
0b00000000,
0b00000000,
0b00000000,
0b00000000,
0b00000000,
0b00000000,
0b00000000,
0b00000000,
0b00000000,
0b00000000,
0b00000000,
0b00000000,
0b00000000,
0b00000000,
0b00000000,
};
static const uint8_t BRICK_ANIM_PATTERNS[4] = {0b00110001, 0b00101001, 0b00100101, 0b00100011};
// Initialise seed
void srand_custom(uint32_t seed) {
@@ -172,18 +75,16 @@ int randInt(int min, int max) {
void reset(void)
{
ballCount = BALL_NUMBER;
levelCountBreackout = 1;
levelCountBreakout = 1;
score = 0;
}
// PlayBeep
void playBeep(uint16_t tone)
{
BK4819_PlayTone(tone, true); // 500 Hz ON
BK4819_PrepareToPlayTone(true);
AUDIO_AudioPathOn();
BK4819_ExitTxMute();
SYSTEM_DelayMs(100);
BK4819_EnterTxMute();
BK4819_PlayToneRaw(tone, 100);
AUDIO_AudioPathOff();
}
@@ -191,10 +92,10 @@ void playBeep(uint16_t tone)
void drawScore()
{
// Clean status line
memset(gStatusLine, 0, sizeof(gStatusLine));
UI_StatusClear();
// Level
sprintf(str, "Level %02u", levelCountBreackout);
sprintf(str, "Level %02u", levelCountBreakout);
GUI_DisplaySmallest(str, 0, 1, true, true);
// Ball
@@ -206,30 +107,38 @@ void drawScore()
GUI_DisplaySmallest(str, 88, 1, true, true);
}
// Render the ball
void renderBall(bool 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;
UI_DrawRectangleBuffer(gFrameBuffer, ball.x, ball.y, ball.x + ball.w -1, ball.y + ball.h - 1, true);
UI_DrawLineBuffer(gFrameBuffer, ball.x - 1, ball.y + 1, ball.x + ball.w, ball.y + 1, true);
renderBall(true);
}
// Calculate the direction of the bounced ball
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;
}
// Draw ball
void drawBall() {
UI_DrawRectangleBuffer(gFrameBuffer, ball.x, ball.y, ball.x + ball.w -1, ball.y + ball.h -1, false);
UI_DrawLineBuffer(gFrameBuffer, ball.x - 1, ball.y + 1, ball.x + ball.w, ball.y + 1, false);
renderBall(false);
ball.x += ball.dx;
ball.y += ball.dy;
if (ball.y <= 0) // Up
{
ball.dx = map(randInt(0, 7), 0, 7, 3, -3);
ball.dx = randInt(-3, 3);
ball.dy = 1;
}
else if (ball.x <= 2) // Left
@@ -242,10 +151,8 @@ void drawBall() {
}
// And now Down...
if (ball.y == 47) {
if (ball.x + 1 >= racket.x && ball.x - 1 <= racket.x + racket.w) {
ball.dx = map(racket.x + racket.w - ball.x, 0, racket.w, 3, -3);
ball.dy *= -1;
isBeep = true;
if (ball.x + 1 >= racket.x && ball.x - 1 <= racket.x + RACKET_WIDTH) {
directionBall(racket.x, RACKET_WIDTH, 3);
tone = 400;
}
}
@@ -253,7 +160,6 @@ void drawBall() {
ballCount--;
UI_DisplayClear();
drawScore();
isBeep = true;
tone = 800;
if (ballCount < 0) {
reset();
@@ -261,38 +167,26 @@ void drawBall() {
drawWall();
isPaused = true;
UI_PrintStringSmallBold("GAME", 32, 0, 4);
UI_PrintStringSmallBold("OVER", 66, 0, 4);
UI_PrintStringSmallBold("GAME OVER", 0, LCD_WIDTH - 1, 4);
}
initRacket();
initBall();
}
UI_DrawRectangleBuffer(gFrameBuffer, ball.x, ball.y, ball.x + ball.w -1, ball.y + ball.h - 1, true);
UI_DrawLineBuffer(gFrameBuffer, ball.x - 1, ball.y + 1, ball.x + ball.w, ball.y + 1, true);
renderBall(true);
}
// 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].s = randInt(0, 3);
brick[i].destroy = false;
j++;
}
}
@@ -302,59 +196,63 @@ void drawWall() {
for (i = 0; i < BRICK_NUMBER; i++) {
if (brick[i].destroy == false) {
uint8_t *fb_ptr = gFrameBuffer[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[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++;
ball.dx = map(brick[i].x + brick[i].w - ball.x, 0, brick[i].w, 2, -2);
ball.dy *= -1;
directionBall(brick[i].x, BRICK_WIDTH, 2);
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, true);
memcpy(gFrameBuffer[brick[i].y / 8] + brick[i].x, BITMAP_blockOn, sizeof(BITMAP_blockOn));
memset(fb_ptr + 1, 0b00111111, 13);
ST7565_BlitLine(brick[i].y / 8);
playBeep(600);
memcpy(gFrameBuffer[brick[i].y / 8] + brick[i].x, BITMAP_blockEmpty, sizeof(BITMAP_blockEmpty));
memset(fb_ptr + 0, 0b00000000, 15);
ST7565_BlitLine(brick[i].y / 8);
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, false);
if (score % BRICK_NUMBER == 0) {
levelCountBreackout++;
levelCountBreakout++;
ballCount++;
initWall();
return;
}
}
if (brick[i].destroy == false) {
memcpy(gFrameBuffer[brick[i].y / 8] + brick[i].x, BITMAP_block[(brick[i].s + blockAnim) % 4], sizeof(BITMAP_block[(brick[i].s + blockAnim) % 4]));
} else {
for(uint8_t k = 0; k < 13; k++) {
fb_ptr[k + 1] = BRICK_ANIM_PATTERNS[(blockAnim + k) % 4];
}
}
}
}
}
// Render the racket shape
void renderRacket(int x, bool 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;
UI_DrawRectangleBuffer(gFrameBuffer, racket.x + 1, racket.y, racket.x + racket.w - 2, racket.y + racket.h, true);
UI_DrawLineBuffer(gFrameBuffer, racket.x, racket.y + 1, racket.x + racket.w - 1, racket.y + 1, true);
renderRacket(racket.x, true);
}
// Draw racket
void drawRacket() {
if (racket.p != racket.x) {
UI_DrawRectangleBuffer(gFrameBuffer, racket.p + 1, racket.y, racket.p + racket.w - 2, racket.y + racket.h, false);
UI_DrawLineBuffer(gFrameBuffer, racket.p, racket.y + 1, racket.p + racket.w - 1, racket.y + 1, false);
renderRacket(racket.p, false);
racket.p = racket.x;
UI_DrawRectangleBuffer(gFrameBuffer, racket.x + 1, racket.y, racket.x + racket.w - 2, racket.y + racket.h, true);
UI_DrawLineBuffer(gFrameBuffer, racket.x, racket.y + 1, racket.x + racket.w - 1, racket.y + 1, true);
renderRacket(racket.x, true);
}
}
@@ -382,7 +280,7 @@ static void OnKeyDown(uint8_t key)
kbd.counter = 0;
if(isPaused)
{
UI_PrintStringSmallBold("PAUSE", 0, 128, 4);
UI_PrintStringSmallBold("PAUSE", 0, LCD_WIDTH - 1, 4);
}
break;
case KEY_EXIT:
@@ -404,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;
@@ -416,17 +314,13 @@ static bool HandleUserInput()
if (kbd.current == KEY_INVALID)
return true;
// Movement keys: dispatch on every tick while held
if (kbd.current == KEY_UP || kbd.current == KEY_DOWN ||
kbd.current == KEY_4 || kbd.current == KEY_0)
{
OnKeyDown(kbd.current);
return true;
}
// Action keys (MENU, EXIT): dispatch only on rising edge
if (kbd.current != kbd.prev)
{
if (
// Movement keys: dispatch on every tick while held
(kbd.current == KEY_UP || kbd.current == KEY_DOWN ||
kbd.current == KEY_4 || kbd.current == KEY_0) ||
// Action keys (MENU, EXIT): dispatch only on rising edge
(kbd.current != kbd.prev)
){
OnKeyDown(kbd.current);
}
@@ -441,62 +335,65 @@ static void Tick()
// APP_RunBreakout
void APP_RunBreakout(void) {
static uint8_t swap = 0;
static uint8_t swap = 0;
// Init seed
srand_custom(BK4819_ReadRegister(BK4819_REG_67) & 0x01FF * gBatteryVoltageAverage * gEeprom.VfoInfo[0].pRX->Frequency);
// Init seed
srand_custom(BK4819_ReadRegister(BK4819_REG_67) & 0x01FF * gBatteryVoltageAverage * gEeprom.VfoInfo[0].pRX->Frequency);
// Init led
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, false);
// Init led
BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, false);
// Finish the brightness fade if it is in progress
BACKLIGHT_UpdateTickless();
// Finish the brightness fade if it is in progress
BACKLIGHT_UpdateTickless();
// Init game
UI_DisplayClear();
reset();
initWall();
initRacket();
initBall();
memset(gStatusLine, 0, sizeof(gStatusLine));
isInitialized = true;
// Init game
UI_DisplayClear();
reset();
initWall();
initRacket();
initBall();
UI_StatusClear();
isInitialized = true;
while(isInitialized)
while(isInitialized)
{
#if defined(ENABLE_UART) || defined(ENABLE_USB)
UART_ServiceCommands();
#endif
Tick();
if(!isPaused)
{
Tick();
if(!isPaused)
if(swap == 0)
{
if(swap == 0)
{
blockAnim = (blockAnim + 1) % 4;
}
swap = (swap + 1) % 4;
drawScore();
drawWall();
drawRacket();
drawBall();
if(isBeep)
{
playBeep(tone);
isBeep = false;
}
else
{
SYSTEM_DelayMs(40 - MIN(levelCountBreackout - 1, 20)); // Add more fun...
}
blockAnim = (blockAnim + 1) % 4;
}
swap = (swap + 1) % 4;
ST7565_BlitStatusLine(); // Blank status line
ST7565_BlitFullScreen();
#ifdef ENABLE_FEAT_F4HWN_SCREENSHOT
if(isPaused || swap == 0)
{
SCREENSHOT_Update(false);
}
#endif
drawScore();
drawWall();
drawRacket();
drawBall();
if(tone != 0)
{
playBeep(tone);
tone = 0;
}
else
{
SYSTEM_DelayMs(40 - MIN(levelCountBreakout - 1, 20)); // Add more fun...
}
}
}
ST7565_BlitStatusLine(); // Blank status line
ST7565_BlitFullScreen();
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
if(isPaused || swap == 0)
{
K5VIEWER_Update(false);
}
#endif
}
}
-8
View File
@@ -47,25 +47,17 @@
typedef struct {
uint8_t x; // x
uint8_t y; // y
uint8_t w; // width
uint8_t h; // height
uint8_t s; // style
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;
+828 -7
View File
@@ -1,9 +1,17 @@
#include <stddef.h>
#include <string.h>
#include "app/app.h"
#include "app/chFrScanner.h"
#include "audio.h"
#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER
#include "driver/systick.h"
#endif
#include "functions.h"
#include "misc.h"
#include "settings.h"
#include "ui/main.h"
//#include "debugging.h"
int8_t gScanStateDir;
@@ -13,6 +21,29 @@ bool gScanPauseMode;
#ifdef ENABLE_SCAN_RANGES
uint32_t gScanRangeStart;
uint32_t gScanRangeStop;
#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
typedef struct {
uint16_t sample[SCAN_RANGE_SKIP_MAX];
uint32_t start;
uint32_t stop;
uint16_t step;
uint8_t count;
uint8_t next;
} ScanRangeSkipList_t;
static ScanRangeSkipList_t scanRangeSkip;
#endif
typedef enum {
@@ -36,13 +67,681 @@ uint8_t initialCROSS_BAND_RX_TX;
static void NextFreqChannel(void);
static void NextMemChannel(void);
#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER
static void ScanFastResetState(void);
#endif
#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER
static bool ScanFastEnabled(void)
{
return gSetting_set_scn;
}
#endif
#ifdef ENABLE_SCAN_RANGES
static void ScanRangeSkipClear(void)
{
scanRangeSkip.count = 0;
scanRangeSkip.next = 0;
}
static void ScanRangeSkipSync(void)
{
const uint16_t step = gRxVfo->StepFrequency;
if (scanRangeSkip.start == gScanRangeStart &&
scanRangeSkip.stop == gScanRangeStop &&
scanRangeSkip.step == step)
{
return;
}
ScanRangeSkipClear();
scanRangeSkip.start = gScanRangeStart;
scanRangeSkip.stop = gScanRangeStop;
scanRangeSkip.step = step;
}
static uint16_t ScanRangeSkipSampleForFrequency(uint32_t frequency)
{
const uint16_t step = gRxVfo->StepFrequency;
if (!gScanRangeStart || step == 0 || frequency < gScanRangeStart || frequency > gScanRangeStop)
return 0;
const uint32_t sample = (frequency - gScanRangeStart) / step;
if (sample >= 0xFFFFu)
return 0;
return (uint16_t)(sample + 1);
}
static bool ScanRangeSkipContainsFrequency(uint32_t frequency)
{
const uint16_t sample = ScanRangeSkipSampleForFrequency(frequency);
if (sample == 0)
return false;
for (uint8_t i = 0; i < scanRangeSkip.count; ++i)
if (scanRangeSkip.sample[i] == sample)
return true;
return false;
}
static uint32_t ScanRangeNextFrequency(void)
{
uint32_t frequency;
uint8_t guard = SCAN_RANGE_SKIP_MAX + 1;
ScanRangeSkipSync();
do
{
frequency = APP_SetFreqByStepAndLimits(gRxVfo, gScanStateDir, gScanRangeStart, gScanRangeStop);
gRxVfo->freq_config_RX.Frequency = frequency;
} while (--guard && ScanRangeSkipContainsFrequency(frequency));
return frequency;
}
bool CHFRSCANNER_ExcludeCurrentScanRange(void)
{
ScanRangeSkipSync();
uint16_t sample = ScanRangeSkipSampleForFrequency(lastFoundFrqOrChan);
if (sample == 0)
sample = ScanRangeSkipSampleForFrequency(gRxVfo->freq_config_RX.Frequency);
if (sample == 0)
return false;
for (uint8_t i = 0; i < scanRangeSkip.count; ++i)
if (scanRangeSkip.sample[i] == sample)
return true;
scanRangeSkip.sample[scanRangeSkip.next] = sample;
scanRangeSkip.next = (scanRangeSkip.next + 1) & (SCAN_RANGE_SKIP_MAX - 1);
if (scanRangeSkip.count < SCAN_RANGE_SKIP_MAX)
scanRangeSkip.count++;
#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER
ScanFastResetState();
#endif
return true;
}
bool CHFRSCANNER_HasScanRangeExcludedOrdinal(uint32_t first_ordinal, uint32_t last_ordinal)
{
if (first_ordinal == 0)
first_ordinal = 1;
if (last_ordinal < first_ordinal)
return false;
if (first_ordinal > 0xFFFFu)
return false;
if (last_ordinal > 0xFFFFu)
last_ordinal = 0xFFFFu;
for (uint8_t i = 0; i < scanRangeSkip.count; ++i) {
const uint16_t sample = scanRangeSkip.sample[i];
if (sample >= first_ordinal && sample <= last_ordinal)
return true;
}
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)
{
if (!FUNCTION_IsRx())
return;
AUDIO_AudioPathOff();
gEnableSpeaker = false;
gMonitor = false;
gRxReceptionMode = RX_MODE_NONE;
gScanPauseMode = false;
FUNCTION_Init();
FUNCTION_Select(FUNCTION_FOREGROUND);
}
#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER
#define SCAN_FAST_PRECHECK_STEPS 6
#define SCAN_FAST_RSSI_MARGIN 16
#define SCAN_FAST_SQUELCH_MARGIN 8
#define SCAN_FAST_WEAK_MARGIN 8
#define SCAN_FAST_RECHECK_DELAY_US 350
#define SCAN_FAST_FINE_STEP_LIMIT 250
#define SCAN_FAST_FINE_REFINE_SPAN 1000
#define SCAN_FAST_FINE_REFINE_MAX 80
#define SCAN_FAST_FINE_RSSI_DROP 8
#define SCAN_FAST_RSSI_MAX 65535u
// Settle loop guard: at most this many 1us waits while the BK4819 glitch
// indicator stays above SCAN_FAST_GLITCH_THRESHOLD. Caps the worst-case
// settling time per step at ~50us before we read the RSSI anyway.
#define SCAN_FAST_GLITCH_GUARD_MAX 50
#define SCAN_FAST_GLITCH_THRESHOLD 200
// HF/VHF boundary in Hz: BK4819_PickRXFilterPathBasedOnFrequency() switches
// the front-end filter path here, so we only re-run that (relatively
// expensive) call when we actually cross the boundary.
#define SCAN_FAST_HF_VHF_BOUNDARY_HZ 28000000u
typedef enum {
SCAN_FAST_DISABLED,
SCAN_FAST_QUIET_BATCH,
SCAN_FAST_CANDIDATE
} scan_fast_result_t;
static uint16_t scanFastReg30;
static uint16_t scanFastNoiseFloor = SCAN_FAST_RSSI_MAX;
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)
{
// Called on every scan (re)start, after a reception, and on each
// wraparound to the start of the channel list / range. The noise
// floor is re-warmed up from current conditions instead of carrying
// stale calibration into a later sweep.
scanFastNoiseFloor = SCAN_FAST_RSSI_MAX;
scanFastPrevFrequency = 0;
scanFastLastFullTuneCandidate = false;
scanFastDisplayVfoValid = false;
}
static void ScanFastResetNoiseFloor(void)
{
scanFastNoiseFloor = SCAN_FAST_RSSI_MAX;
}
static uint16_t ScanFastReadRssi(void)
{
uint8_t guard = SCAN_FAST_GLITCH_GUARD_MAX;
while (guard-- && BK4819_GetGlitchIndicator() >= SCAN_FAST_GLITCH_THRESHOLD)
{
SYSTICK_DelayUs(1);
}
// Discard first read: after fast tuning the RSSI/AGC value may still be stale.
BK4819_GetRSSI();
return BK4819_GetRSSI();
}
static void ScanFastTune(uint32_t frequency)
{
if (scanFastPrevFrequency == 0 ||
((frequency < SCAN_FAST_HF_VHF_BOUNDARY_HZ) !=
(scanFastPrevFrequency < SCAN_FAST_HF_VHF_BOUNDARY_HZ)))
{
BK4819_PickRXFilterPathBasedOnFrequency(frequency);
}
scanFastPrevFrequency = frequency;
BK4819_SetFrequency(frequency);
BK4819_WriteRegister(BK4819_REG_30, 0);
BK4819_WriteRegister(BK4819_REG_30, scanFastReg30);
}
const VFO_Info_t *CHFRSCANNER_GetScanDisplayVfo(void)
{
if (!ScanFastEnabled() || !scanFastDisplayVfoValid || gScanStateDir == SCAN_OFF || FUNCTION_IsRx())
return NULL;
return &scanFastDisplayVfo;
}
static bool ScanFastUpdateDisplayVfo(uint16_t channel, uint32_t *frequency, ModulationMode_t *modulation)
{
ChannelScanDisplayInfo_t info;
if (!SETTINGS_FetchChannelScanDisplayInfo(channel, &info))
{
scanFastDisplayVfoValid = false;
return false;
}
scanFastDisplayVfo = gEeprom.VfoInfo[gEeprom.RX_VFO];
scanFastDisplayVfo.CHANNEL_SAVE = channel;
scanFastDisplayVfo.freq_config_RX = info.rx;
scanFastDisplayVfo.freq_config_TX = info.tx;
scanFastDisplayVfo.TX_OFFSET_FREQUENCY = info.offset;
scanFastDisplayVfo.StepFrequency = info.stepFrequency;
scanFastDisplayVfo.STEP_SETTING = info.stepSetting;
scanFastDisplayVfo.Modulation = info.modulation;
scanFastDisplayVfo.TX_OFFSET_FREQUENCY_DIRECTION = info.txOffsetFrequencyDirection;
scanFastDisplayVfo.OUTPUT_POWER = info.outputPower;
scanFastDisplayVfo.FrequencyReverse = info.frequencyReverse;
scanFastDisplayVfo.CHANNEL_BANDWIDTH = info.channelBandwidth;
scanFastDisplayVfo.BUSY_CHANNEL_LOCK = info.busyChannelLock;
scanFastDisplayVfo.TX_LOCK = info.txLock;
#ifdef ENABLE_DTMF_CALLING
scanFastDisplayVfo.DTMF_DECODING_ENABLE = info.dtmfDecodingEnable;
#endif
scanFastDisplayVfo.DTMF_PTT_ID_TX_MODE = info.dtmfPttIdTxMode;
if (!scanFastDisplayVfo.FrequencyReverse)
{
scanFastDisplayVfo.pRX = &scanFastDisplayVfo.freq_config_RX;
scanFastDisplayVfo.pTX = &scanFastDisplayVfo.freq_config_TX;
}
else
{
scanFastDisplayVfo.pRX = &scanFastDisplayVfo.freq_config_TX;
scanFastDisplayVfo.pTX = &scanFastDisplayVfo.freq_config_RX;
}
scanFastDisplayVfoValid = true;
if (frequency)
*frequency = info.rx.Frequency;
if (modulation)
*modulation = info.modulation;
return true;
}
static uint16_t ScanFastSaturatingAdd(uint16_t value, uint16_t add)
{
return (value > SCAN_FAST_RSSI_MAX - add) ? SCAN_FAST_RSSI_MAX : (uint16_t)(value + add);
}
static uint16_t ScanFastSaturatingSub(uint16_t value, uint16_t sub)
{
return (value > sub) ? (uint16_t)(value - sub) : 0;
}
static uint16_t ScanFastGetNoiseTrigger(void)
{
return ScanFastSaturatingAdd(scanFastNoiseFloor, SCAN_FAST_RSSI_MARGIN);
}
static uint16_t ScanFastGetSquelchTrigger(void)
{
return ScanFastSaturatingSub(gRxVfo->SquelchOpenRSSIThresh, SCAN_FAST_SQUELCH_MARGIN);
}
static bool ScanFastIsNearCandidate(uint16_t rssi)
{
const uint16_t rssiWithMargin = ScanFastSaturatingAdd(rssi, SCAN_FAST_WEAK_MARGIN);
const uint16_t squelchTrigger = ScanFastGetSquelchTrigger();
if (scanFastNoiseFloor == SCAN_FAST_RSSI_MAX)
return gRxVfo->SquelchOpenRSSIThresh > 0 &&
rssiWithMargin >= gRxVfo->SquelchOpenRSSIThresh;
return rssiWithMargin >= ScanFastGetNoiseTrigger() &&
rssiWithMargin >= squelchTrigger;
}
static uint16_t ScanFastReadCandidateRssi(void)
{
uint16_t rssi = ScanFastReadRssi();
if (ScanFastIsNearCandidate(rssi))
{
SYSTICK_DelayUs(SCAN_FAST_RECHECK_DELAY_US);
const uint16_t retryRssi = ScanFastReadRssi();
if (retryRssi > rssi)
rssi = retryRssi;
}
return rssi;
}
static bool ScanFastIsCandidate(uint16_t rssi)
{
const uint16_t squelchTrigger = ScanFastGetSquelchTrigger();
if (scanFastNoiseFloor == SCAN_FAST_RSSI_MAX)
{
scanFastNoiseFloor = rssi;
return gRxVfo->SquelchOpenRSSIThresh > 0 &&
rssi >= gRxVfo->SquelchOpenRSSIThresh;
}
const uint16_t noiseTrigger = ScanFastGetNoiseTrigger();
const uint16_t rssiWithMargin = ScanFastSaturatingAdd(rssi, SCAN_FAST_WEAK_MARGIN);
if ((rssi >= noiseTrigger && rssi >= squelchTrigger) ||
(rssiWithMargin >= noiseTrigger && rssiWithMargin >= squelchTrigger))
{
return true;
}
if (rssi < scanFastNoiseFloor)
scanFastNoiseFloor = rssi;
else
scanFastNoiseFloor = (uint16_t)((7u * scanFastNoiseFloor + rssi + 4u) >> 3);
return false;
}
static void ScanFastApplyChannelShape(ModulationMode_t modulation)
{
const bool modulationChanged = gRxVfo->Modulation != modulation;
const bool bandwidthChanged = gRxVfo->CHANNEL_BANDWIDTH != BANDWIDTH_WIDE;
if (!modulationChanged && !bandwidthChanged)
return;
gRxVfo->Modulation = modulation;
gRxVfo->CHANNEL_BANDWIDTH = BANDWIDTH_WIDE;
if (modulationChanged)
RADIO_SetModulation(modulation);
if (modulation == MODULATION_AM)
{
BK4819_SetFilterBandwidth(RADIO_GetAMFilterBandwidth(gRxVfo), true);
}
else
{
BK4819_SetFilterBandwidth(BK4819_FILTER_BW_WIDE, false);
}
if (modulationChanged)
{
// AM and FM use different demod/AGC profiles, so their RSSI
// baselines are not directly comparable. Relearn the floor after
// crossing that boundary instead of treating the next FM block as
// a wall of candidates.
ScanFastResetNoiseFloor();
}
}
#endif
#if defined(ENABLE_FEAT_F4HWN_SCAN_FASTER) && defined(ENABLE_SCAN_RANGES)
static void ScanRangeFastRefineCandidate(uint16_t firstRssi)
{
const uint16_t step = gRxVfo->StepFrequency;
if (step == 0 || step >= SCAN_FAST_FINE_STEP_LIMIT)
return;
uint16_t maxSteps = SCAN_FAST_FINE_REFINE_SPAN / step;
if (maxSteps == 0)
maxSteps = 1;
if (maxSteps > SCAN_FAST_FINE_REFINE_MAX)
maxSteps = SCAN_FAST_FINE_REFINE_MAX;
uint16_t bestRssi = firstRssi;
uint32_t bestFrequency = gRxVfo->freq_config_RX.Frequency;
uint8_t fallingSteps = 0;
for (uint16_t i = 0; i < maxSteps; ++i)
{
const uint32_t prevRxFrequency = gRxVfo->pRX->Frequency;
gRxVfo->freq_config_RX.Frequency = ScanRangeNextFrequency();
RADIO_ApplyOffset(gRxVfo);
const uint32_t freq = gRxVfo->pRX->Frequency;
if ((gScanStateDir > 0 && freq < prevRxFrequency) ||
(gScanStateDir < 0 && freq > prevRxFrequency))
{
break;
}
ScanFastTune(freq);
const uint16_t rssi = ScanFastReadRssi();
if (rssi > bestRssi)
{
bestRssi = rssi;
bestFrequency = gRxVfo->freq_config_RX.Frequency;
fallingSteps = 0;
}
else if (bestRssi > rssi && bestRssi - rssi >= SCAN_FAST_FINE_RSSI_DROP)
{
if (++fallingSteps >= 3)
break;
}
}
gRxVfo->freq_config_RX.Frequency = bestFrequency;
RADIO_ApplyOffset(gRxVfo);
ScanFastTune(gRxVfo->pRX->Frequency);
}
static scan_fast_result_t ScanRangeFastPrecheck(void)
{
if (!gScanRangeStart)
return SCAN_FAST_DISABLED;
if (gRxVfo->SquelchOpenRSSIThresh == 0)
return SCAN_FAST_DISABLED;
// Mute AF DAC during the precheck sweep: avoids audio glitches and
// saves a few uA on each silent step. The bit is restored on the real
// tune by RADIO_SetupRegisters() in NextFreqChannel().
scanFastReg30 = BK4819_ReadRegister(BK4819_REG_30) & ~BK4819_REG_30_MASK_ENABLE_AF_DAC;
for (uint8_t i = 0; i < SCAN_FAST_PRECHECK_STEPS; ++i)
{
gRxVfo->freq_config_RX.Frequency = ScanRangeNextFrequency();
RADIO_ApplyOffset(gRxVfo);
const uint32_t freq = gRxVfo->pRX->Frequency;
// Detect wraparound: scanning forward but the new freq is lower
// than the previous one (or scanning backward but it's higher)
// means the range has wrapped from stop back to start. Reset the
// noise floor so the new pass re-warms up from current conditions.
if (scanFastPrevFrequency != 0 &&
((gScanStateDir > 0 && freq < scanFastPrevFrequency) ||
(gScanStateDir < 0 && freq > scanFastPrevFrequency)))
{
ScanFastResetState();
}
ScanFastTune(freq);
const uint16_t rssi = ScanFastReadCandidateRssi();
#ifdef ENABLE_FEAT_F4HWN_SCAN_RSSI
ScanRssiSparklinePush(rssi);
#endif
if (ScanFastIsCandidate(rssi))
{
ScanRangeFastRefineCandidate(rssi);
return SCAN_FAST_CANDIDATE;
}
}
return SCAN_FAST_QUIET_BATCH;
}
#endif
#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER
static bool MemChannelFastPrecheck(uint16_t channel)
{
uint32_t frequency;
ModulationMode_t modulation;
if (gRxVfo->SquelchOpenRSSIThresh == 0)
{
scanFastLastFullTuneCandidate = false;
return true;
}
if (!ScanFastUpdateDisplayVfo(channel, &frequency, &modulation))
{
scanFastLastFullTuneCandidate = false;
return true;
}
ScanFastApplyChannelShape(modulation);
// Mute AF DAC for the same reason as in ScanRangeFastPrecheck().
scanFastReg30 = BK4819_ReadRegister(BK4819_REG_30) & ~BK4819_REG_30_MASK_ENABLE_AF_DAC;
ScanFastTune(frequency);
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
}
// No signal here: still mirror the probed frequency in the VFO so the
// status line (channel name + frequency) keeps in sync as we skip.
// RADIO_ConfigureChannel() will overwrite these values cleanly when a
// candidate is eventually retained.
scanFastLastFullTuneCandidate = false;
gRxVfo->freq_config_RX.Frequency = frequency;
return false;
}
static void AdvanceMemScanList(const bool enabled)
{
if (enabled)
if (++currentScanList >= SCAN_NEXT_NUM)
currentScanList = SCAN_NEXT_CHAN_SCANLIST1;
}
static void SetMemScanProgressChannel(uint16_t channel)
{
gEeprom.MrChannel[ gEeprom.RX_VFO] = channel;
gEeprom.ScreenChannel[gEeprom.RX_VFO] = channel;
gRxVfo->CHANNEL_SAVE = channel;
}
#endif
#if defined(ENABLE_FEAT_F4HWN_RESUME_STATE) || defined(ENABLE_SCAN_RANGES)
void CHFRSCANNER_ScanRange(void) {
gScanRangeStart = gScanRangeStart ? 0 : gTxVfo->pRX->Frequency;
if (gScanRangeStart) {
gScanRangeStart = 0;
return;
}
gScanRangeStart = gTxVfo->pRX->Frequency;
gScanRangeStop = gEeprom.VfoInfo[!gEeprom.TX_VFO].freq_config_RX.Frequency;
#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER
ScanFastResetState();
#endif
if(gScanRangeStart > gScanRangeStop)
SWAP(gScanRangeStart, gScanRangeStop);
ScanRangeSkipSync();
}
#endif
@@ -55,18 +754,30 @@ void CHFRSCANNER_Start(const bool storeBackupSettings, const int8_t scan_directi
}
RADIO_SelectVfos();
CHFRSCANNER_AbortActiveReception();
gNextMrChannel = gRxVfo->CHANNEL_SAVE;
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);
scanListChanged = true;
}
if (storeBackupSettings || scanListChanged)
UI_MAIN_NotifyScanListChanged();
// channel mode
if (storeBackupSettings) {
initialFrqOrChan = gRxVfo->CHANNEL_SAVE;
@@ -93,6 +804,16 @@ void CHFRSCANNER_Start(const bool storeBackupSettings, const int8_t scan_directi
gScanPauseMode = false;
}
void CHFRSCANNER_ManualResume(const int8_t scan_direction)
{
CHFRSCANNER_Start(false, scan_direction);
gScanPauseDelayIn_10ms = (gRxVfo->SquelchOpenRSSIThresh == 0)
? scan_pause_delay_in_3_10ms
: 1;
gScheduleScanListen = false;
}
/*
void CHFRSCANNER_ContinueScanning(void)
{
@@ -119,6 +840,18 @@ void CHFRSCANNER_ContinueScanning(void)
void CHFRSCANNER_ContinueScanning(void)
{
#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER
if (scanFastLastFullTuneCandidate &&
gCurrentFunction != FUNCTION_INCOMING &&
!g_SquelchLost)
{
// A rejected full-tune candidate is just a false RSSI hit. Keep the
// learned floor; resetting here can make the next channel blind when
// it is the real signal, especially while scanning down.
scanFastLastFullTuneCandidate = false;
}
#endif
if (gCurrentFunction == FUNCTION_INCOMING &&
(IS_FREQ_CHANNEL(gNextMrChannel) || gCurrentCodeType == CODE_TYPE_OFF))
{
@@ -136,6 +869,15 @@ void CHFRSCANNER_ContinueScanning(void)
void CHFRSCANNER_Found(void)
{
#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER
// After a real reception the BK4819 AGC has shifted, biasing the next
// few RSSI readings high. Reset the precheck state so it warms up from
// the current noise floor instead of carrying stale calibration into
// the post-reception scan, which otherwise turns nearly every channel
// into a CANDIDATE and erases the speed gain.
ScanFastResetState();
#endif
if (gEeprom.SCAN_RESUME_MODE > 80) {
if (!gScanPauseMode) {
gScanPauseDelayIn_10ms = scan_pause_delay_in_5_10ms * (gEeprom.SCAN_RESUME_MODE - 80) * 5;
@@ -208,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;
@@ -243,11 +988,46 @@ static void NextFreqChannel(void)
{
#ifdef ENABLE_SCAN_RANGES
if(gScanRangeStart) {
gRxVfo->freq_config_RX.Frequency = APP_SetFreqByStepAndLimits(gRxVfo, gScanStateDir, gScanRangeStart, gScanRangeStop);
#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER
if (ScanFastEnabled())
{
const scan_fast_result_t fastResult = ScanRangeFastPrecheck();
if (fastResult == SCAN_FAST_QUIET_BATCH)
{
scanFastLastFullTuneCandidate = false;
gScanPauseDelayIn_10ms = 1;
gUpdateDisplay = true;
return;
}
if (fastResult == SCAN_FAST_DISABLED)
{
scanFastLastFullTuneCandidate = false;
gRxVfo->freq_config_RX.Frequency = ScanRangeNextFrequency();
}
else
{
scanFastLastFullTuneCandidate = true;
}
}
else
{
scanFastLastFullTuneCandidate = false;
gRxVfo->freq_config_RX.Frequency = ScanRangeNextFrequency();
}
#else
gRxVfo->freq_config_RX.Frequency = ScanRangeNextFrequency();
#endif
}
else
#endif
{
#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER
scanFastLastFullTuneCandidate = false;
#endif
gRxVfo->freq_config_RX.Frequency = APP_SetFrequencyByStep(gRxVfo, gScanStateDir);
}
RADIO_ApplyOffset(gRxVfo);
RADIO_ConfigureSquelchAndOutputPower(gRxVfo);
@@ -348,23 +1128,60 @@ 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
{ // no valid channel found -> wrapping back to the first channel
chan = MR_CHANNEL_FIRST;
#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER
// Wraparound: re-warm the precheck noise floor on the new pass
// so it tracks current RF conditions instead of an EMA that
// accumulated drift over the previous full sweep.
ScanFastResetState();
#endif
}
#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER
else if ((gScanStateDir > 0 && chan < searchStart) ||
(gScanStateDir < 0 && chan > searchStart))
{
// RADIO_FindNextChannel() wraps internally, so 0xFFFF is not
// returned on a normal full-sweep wrap. Detect that transition
// here and restart the RSSI floor learning for the new pass.
ScanFastResetState();
}
#endif
gNextMrChannel = chan;
//sprintf(str, "----> Chan %d\n", chan + 1);
//LogUart(str);
}
if (gNextMrChannel != prev_chan)
#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER
SetMemScanProgressChannel(gNextMrChannel);
if (ScanFastEnabled() && !MemChannelFastPrecheck(gNextMrChannel))
{
gScanPauseDelayIn_10ms = 1;
gUpdateDisplay = true;
AdvanceMemScanList(enabled);
return;
}
#endif
if (gNextMrChannel != prev_chan
#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER
|| scanFastLastFullTuneCandidate
#endif
)
{
#ifndef ENABLE_FEAT_F4HWN_SCAN_FASTER
gEeprom.MrChannel[ gEeprom.RX_VFO] = gNextMrChannel;
gEeprom.ScreenChannel[gEeprom.RX_VFO] = gNextMrChannel;
#endif
RADIO_ConfigureChannel(gEeprom.RX_VFO, VFO_CONFIGURE_RELOAD);
RADIO_SetupRegisters(true);
@@ -378,7 +1195,11 @@ static void NextMemChannel(void)
gScanPauseDelayIn_10ms = scan_pause_delay_in_3_10ms;
#endif
#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER
AdvanceMemScanList(enabled);
#else
if (enabled)
if (++currentScanList >= SCAN_NEXT_NUM)
currentScanList = SCAN_NEXT_CHAN_SCANLIST1; // back round we go
#endif
}
+22 -1
View File
@@ -4,6 +4,8 @@
#include <stdbool.h>
#include <stdint.h>
#include "radio.h"
// scan direction, if not equal SCAN_OFF indicates
// that we are in a process of scanning channels/frequencies
extern int8_t gScanStateDir;
@@ -13,12 +15,31 @@ 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);
void CHFRSCANNER_Stop(void);
void CHFRSCANNER_Start(const bool storeBackupSettings, const int8_t scan_direction);
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)
void CHFRSCANNER_ScanRange(void);
@@ -29,4 +50,4 @@ void CHFRSCANNER_ContinueScanning(void);
extern uint32_t lastFoundFrqOrChanOld;
#endif
#endif
#endif
+7 -3
View File
@@ -18,7 +18,7 @@
#include <stdio.h> // NULL
#include "app/chFrScanner.h"
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
#include "app/fm.h"
#endif
#include "app/scanner.h"
@@ -212,6 +212,10 @@ DTMF_CallMode_t DTMF_CheckGroupCall(const char *pMsg, const unsigned int size)
}
#endif
void DTMF_clear_input_box_memory() {
memset(gDTMF_RX_live, 0, sizeof(gDTMF_RX_live));
}
void DTMF_clear_input_box(void)
{
memset(gDTMF_InputBox, 0, sizeof(gDTMF_InputBox));
@@ -275,7 +279,7 @@ void DTMF_HandleRequest(void)
gDTMF_ReplyState = DTMF_REPLY_AB;
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
if (gFmRadioMode)
{
FM_TurnOff();
@@ -470,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
+1
View File
@@ -82,6 +82,7 @@ extern DTMF_ReplyState_t gDTMF_ReplyState;
bool DTMF_ValidateCodes(char *pCode, const unsigned int size);
char DTMF_GetCharacter(const unsigned int code);
void DTMF_clear_input_box_memory(void);
void DTMF_clear_input_box(void);
void DTMF_Append(const char code);
void DTMF_Reply(void);
+41 -23
View File
@@ -33,6 +33,7 @@
#include "ui/ui.h"
uint16_t gFM_Channels[FM_CHANNELS_MAX];
#ifdef ENABLE_FMRADIO_EMBEDDED
bool gFmRadioMode;
uint8_t gFmRadioCountdown_500ms;
volatile uint16_t gFmPlayCountdown_10ms;
@@ -40,7 +41,6 @@ volatile int8_t gFM_ScanState;
bool gFM_AutoScan;
uint8_t gFM_ChannelPosition;
bool gFM_FoundFrequency;
bool gFM_AutoScan;
uint16_t gFM_RestoreCountdown_10ms;
@@ -53,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)
{
@@ -95,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;
@@ -164,7 +170,12 @@ 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) {
AUDIO_AudioPathOn();
gEnableSpeaker = true;
}
void FM_PlayAndUpdate(void)
@@ -177,17 +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;
AUDIO_AudioPathOn();
gEnableSpeaker = true;
BACKLIGHT_TurnOn();
FM_AudioPathOn();
}
#endif
int FM_CheckFrequencyLock(uint16_t Frequency, uint16_t LowerLimit)
{
@@ -232,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 };
@@ -288,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;
}
@@ -308,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;
}
@@ -363,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
@@ -391,8 +403,14 @@ static void Key_FUNC(KEY_Code_t Key, uint8_t state)
static void Key_EXIT(uint8_t state)
{
if (state != BUTTON_EVENT_SHORT)
return;
if (gInputBoxIndex) {
if (state != BUTTON_EVENT_SHORT)
return;
}
else {
if (state != BUTTON_EVENT_PRESSED)
return;
}
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
@@ -471,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;
}
@@ -549,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;
}
@@ -603,20 +621,19 @@ void FM_Play(void)
if (!gEeprom.FM_IsMrMode)
gEeprom.FM_SelectedFrequency = gEeprom.FM_FrequencyPlaying;
AUDIO_AudioPathOn();
gEnableSpeaker = true;
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;
}
}
@@ -625,6 +642,7 @@ void FM_Play(void)
else
FM_Tune(gEeprom.FM_FrequencyPlaying, gFM_ScanState, false);
Display:
GUI_SelectNextDisplay(DISPLAY_FM);
}
@@ -639,9 +657,8 @@ void FM_Start(void)
// Disable UHF LNA, enable VHF LNA
BK4819_PickRXFilterPathBasedOnFrequency(10320000); // 103.2 MHz < 280 MHz
AUDIO_AudioPathOn();
FM_AudioPathOn();
gEnableSpeaker = true;
gUpdateStatus = true;
#ifdef ENABLE_FEAT_F4HWN_RESUME_STATE
@@ -649,5 +666,6 @@ void FM_Start(void)
SETTINGS_WriteCurrentState();
#endif
}
#endif
#endif
+13 -8
View File
@@ -19,9 +19,12 @@
#ifdef ENABLE_FMRADIO
#include "driver/keyboard.h"
#include "misc.h"
#ifdef ENABLE_FMRADIO_EMBEDDED
#include "driver/keyboard.h"
#endif
#define FM_CHANNEL_UP 0x01
#define FM_CHANNEL_DOWN 0xFF
@@ -30,32 +33,34 @@ enum {
};
extern uint16_t gFM_Channels[FM_CHANNELS_MAX];
bool FM_CheckValidChannel(uint8_t Channel);
// returns first valid channel starting at Channel
uint8_t FM_FindNextChannel(uint8_t Channel, uint8_t Direction);
int FM_ConfigureChannelState(void);
int FM_CheckFrequencyLock(uint16_t Frequency, uint16_t LowerLimit);
#ifdef ENABLE_FMRADIO_EMBEDDED
extern bool gFmRadioMode;
extern uint8_t gFmRadioCountdown_500ms;
extern volatile uint16_t gFmPlayCountdown_10ms;
extern volatile int8_t gFM_ScanState;
extern bool gFM_AutoScan;
extern uint8_t gFM_ChannelPosition;
// Doubts about whether this should be signed or not
extern uint16_t gFM_FrequencyDeviation;
extern bool gFM_FoundFrequency;
extern uint16_t gFM_RestoreCountdown_10ms;
bool FM_CheckValidChannel(uint8_t Channel);
// returns first valid channel starting at Channel
uint8_t FM_FindNextChannel(uint8_t Channel, uint8_t Direction);
int FM_ConfigureChannelState(void);
void FM_TurnOff(void);
void FM_EraseChannels(void);
void FM_Tune(uint16_t Frequency, int8_t Step, bool bFlag);
void FM_PlayAndUpdate(void);
int FM_CheckFrequencyLock(uint16_t Frequency, uint16_t LowerLimit);
void FM_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld);
void FM_Play(void);
void FM_Start(void);
#endif
#endif
+1504
View File
File diff suppressed because it is too large. Load diff
+66
View File
@@ -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
+9 -21
View File
@@ -20,7 +20,7 @@
#include "app/chFrScanner.h"
#include "app/common.h"
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
#include "app/fm.h"
#endif
@@ -56,7 +56,7 @@ void GENERIC_Key_F(bool bKeyPressed, bool bKeyHeld)
COMMON_KeypadLockToggle();
}
else { // released
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
if ((gFmRadioMode || gScreenToDisplay != DISPLAY_MAIN) && gScreenToDisplay != DISPLAY_FM)
return;
#else
@@ -77,7 +77,7 @@ void GENERIC_Key_F(bool bKeyPressed, bool bKeyHeld)
}
}
else { // short pressed
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
if (gScreenToDisplay != DISPLAY_FM)
#endif
{
@@ -85,13 +85,13 @@ 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;
}
#endif
gBeepToPlay = BEEP_440HZ_500MS;
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
gPttWasReleased = true;
}
}
@@ -103,20 +103,8 @@ void GENERIC_Key_PTT(bool bKeyPressed)
if (!bKeyPressed || SerialConfigInProgress())
{ // PTT released
if (gCurrentFunction == FUNCTION_TRANSMIT) {
// we are transmitting .. stop
if (gFlagEndTransmission) {
FUNCTION_Select(FUNCTION_FOREGROUND);
}
else {
APP_EndTransmission();
APP_HandleEndTransmission();
if (gEeprom.REPEATER_TAIL_TONE_ELIMINATION == 0)
FUNCTION_Select(FUNCTION_FOREGROUND);
else
gRTTECountdown_10ms = gEeprom.REPEATER_TAIL_TONE_ELIMINATION * 10;
}
gFlagEndTransmission = false;
#ifdef ENABLE_VOX
gVOX_NoiseDetected = false;
#endif
@@ -144,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
@@ -155,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
@@ -212,7 +200,7 @@ cancel_tx:
done:
gPttDebounceCounter = 0;
if (gScreenToDisplay != DISPLAY_MENU
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
&& gRequestDisplayScreen != DISPLAY_FM
#endif
) {
+110 -66
View File
@@ -20,7 +20,7 @@
#include "app/app.h"
#include "app/chFrScanner.h"
#include "app/common.h"
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
#include "app/fm.h"
#endif
#include "app/generic.h"
@@ -31,8 +31,11 @@
#include "app/spectrum.h"
#endif
#ifdef ENABLE_FEAT_F4HWN_GAME
#include "app/breakout.h"
#if defined(ENABLE_FEAT_F4HWN_GAME) && !defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS)
#include "app/breakout.h" // resident game only; the overlay path uses app_menu.h
#endif
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_APPS
#include "apps/app_menu.h"
#endif
#include "audio.h"
@@ -44,6 +47,8 @@
#include "radio.h"
#include "settings.h"
#include "ui/inputbox.h"
#include "ui/main.h"
#include "ui/menu.h"
#include "ui/ui.h"
#include <stdlib.h>
@@ -104,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;
@@ -224,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;
@@ -261,34 +272,57 @@ static void processFKeyFunction(const KEY_Code_t Key, const bool beep)
break;
case KEY_7:
#ifdef ENABLE_VOX
ACTION_Vox();
//#else
// toggle_chan_scanlist();
// 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) {
#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
ACTION_Vox();
//#else
// toggle_chan_scanlist();
#endif
#if defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) || defined(ENABLE_FEAT_F4HWN_GAME)
}
#endif
break;
case KEY_8:
gTxVfo->FrequencyReverse = gTxVfo->FrequencyReverse == false;
gRequestSaveChannel = 1;
if (!beep) {
ACTION_BackLightOnDemand();
}
else {
gTxVfo->FrequencyReverse = gTxVfo->FrequencyReverse == false;
gRequestSaveChannel = 1;
}
break;
case KEY_9:
if (RADIO_CheckValidChannel(gEeprom.CHAN_1_CALL, false, 0)) {
gEeprom.MrChannel[Vfo] = gEeprom.CHAN_1_CALL;
gEeprom.ScreenChannel[Vfo] = gEeprom.CHAN_1_CALL;
if (!beep) {
ACTION_BackLight();
}
else {
if (RADIO_CheckValidChannel(gEeprom.CHAN_1_CALL, false, 0)) {
gEeprom.MrChannel[Vfo] = gEeprom.CHAN_1_CALL;
gEeprom.ScreenChannel[Vfo] = gEeprom.CHAN_1_CALL;
#ifdef ENABLE_VOICE
AUDIO_SetVoiceID(0, VOICE_ID_CHANNEL_MODE);
AUDIO_SetDigitVoice(1, gEeprom.CHAN_1_CALL + 1);
gAnotherVoiceID = (VOICE_ID_t)0xFE;
AUDIO_SetVoiceID(0, VOICE_ID_CHANNEL_MODE);
AUDIO_SetDigitVoice(1, gEeprom.CHAN_1_CALL + 1);
gAnotherVoiceID = (VOICE_ID_t)0xFE;
#endif
gRequestSaveVFO = true;
gVfoConfigureMode = VFO_CONFIGURE_RELOAD;
break;
gRequestSaveVFO = true;
gVfoConfigureMode = VFO_CONFIGURE_RELOAD;
}
}
if (beep)
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
break;
#ifdef ENABLE_FEAT_F4HWN // Set Squelch F + UP or Down and Step F + SIDE1 or F + SIDE2
@@ -299,15 +333,16 @@ static void processFKeyFunction(const KEY_Code_t Key, const bool beep)
if (gScanStateDir != SCAN_OFF) {
RADIO_NextValidList(isKeyUp ? 1 : -1);
UI_MAIN_NotifyScanListChanged();
} else {
// Adjust squelch: UP increments, DOWN decrements
if (gSquelchLevelOriginal == 10)
gSquelchLevelOriginal = gEeprom.SQUELCH_LEVEL;
if (isKeyUp) {
gEeprom.SQUELCH_LEVEL = (gEeprom.SQUELCH_LEVEL < 9) ? gEeprom.SQUELCH_LEVEL + 1 : 9;
if (gEeprom.SQUELCH_LEVEL < 9) gEeprom.SQUELCH_LEVEL++;
} else {
gEeprom.SQUELCH_LEVEL = (gEeprom.SQUELCH_LEVEL > 0) ? gEeprom.SQUELCH_LEVEL - 1 : 0;
if (gEeprom.SQUELCH_LEVEL > 0) gEeprom.SQUELCH_LEVEL--;
}
gVfoConfigureMode = VFO_CONFIGURE;
}
@@ -429,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);
@@ -481,6 +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_NotifyScanListChanged();
#ifdef ENABLE_FEAT_F4HWN_RESUME_STATE
SETTINGS_WriteCurrentState();
#endif
@@ -499,6 +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_NotifyScanListChanged();
#ifdef ENABLE_FEAT_F4HWN_RESUME_STATE
SETTINGS_WriteCurrentState();
@@ -555,10 +602,10 @@ static void MAIN_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
return;
}
gKeyInputCountdown = (gInputBoxIndex >= totalDigits) ? (key_input_timeout_500ms / 16) : (key_input_timeout_500ms / 3);
gKeyInputCountdown = key_input_timeout_500ms / (gInputBoxIndex >= totalDigits ? 16 : 3);
if (gInputBoxIndex > totalDigits) {
gInputBoxIndex = totalDigits;
gInputBoxIndex = totalDigits;
return;
}
@@ -569,11 +616,8 @@ static void MAIN_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
// convert to int
uint32_t inputFreq = StrToUL(inputStr);
// how many zero to add
uint8_t zerosToAdd = totalDigits - inputLength;
// add missing zero
for (uint8_t i = 0; i < zerosToAdd; i++) {
for (uint8_t i = 0; i < totalDigits - inputLength; i++) {
inputFreq *= 10;
}
@@ -654,30 +698,6 @@ static void MAIN_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
HideFKeyIcon();
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
if(gEeprom.MENU_LOCK == true && Key != 2) {
return;
}
#endif
if(Key == 8)
{
ACTION_BackLightOnDemand();
return;
}
else if(Key == 9)
{
ACTION_BackLight();
return;
}
#ifdef ENABLE_FEAT_F4HWN_GAME
else if(Key == 7)
{
APP_RunBreakout();
return;
}
#endif
processFKeyFunction(Key, false);
}
@@ -708,7 +728,7 @@ static void MAIN_Key_EXIT(bool bKeyPressed, bool bKeyHeld)
}
#endif
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
if (!gFmRadioMode)
#endif
{
@@ -743,7 +763,7 @@ static void MAIN_Key_EXIT(bool bKeyPressed, bool bKeyHeld)
return;
}
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
ACTION_FM();
#endif
return;
@@ -758,15 +778,30 @@ static void MAIN_Key_MENU(bool bKeyPressed, bool bKeyHeld)
if (bKeyPressed) { // long press MENU key
#ifdef ENABLE_FEAT_F4HWN
// Exclude channel
// Exclude current scan entry
if(gScanStateDir != SCAN_OFF)
{
if(FUNCTION_IsRx() || gScanPauseDelayIn_10ms > 9)
{
#ifdef ENABLE_SCAN_RANGES
if(gScanRangeStart && !IS_MR_CHANNEL(gNextMrChannel))
{
if(CHFRSCANNER_ExcludeCurrentScanRange())
{
UI_MAIN_NotifyScanProgressDataChanged();
lastFoundFrqOrChan = lastFoundFrqOrChanOld;
CHFRSCANNER_ContinueScanning();
}
return;
}
#endif
ChannelAttributes_t *att = MR_GetChannelAttributes(lastFoundFrqOrChan);
att->exclude = true;
MR_SaveChannelAttributesToFlash(lastFoundFrqOrChan, att);
UI_MAIN_NotifyScanProgressDataChanged();
gVfoConfigureMode = VFO_CONFIGURE;
gFlagResetVfos = true;
@@ -820,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
@@ -909,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;
@@ -941,12 +987,12 @@ static void MAIN_Key_UP_DOWN(bool bKeyPressed, bool bKeyHeld, int8_t Direction)
uint16_t Channel = gEeprom.ScreenChannel[gEeprom.TX_VFO];
if (bKeyHeld || !bKeyPressed) { // key held or released
if (gInputBoxIndex > 0) {
gInputBoxIndex = 0;
gHasVfoBackup = false;
}
if (gInputBoxIndex > 0) {
gInputBoxIndex = 0;
gHasVfoBackup = false;
}
if (bKeyHeld || !bKeyPressed) { // key held or released
if (!bKeyPressed) {
if (!bKeyHeld || IS_FREQ_CHANNEL(Channel))
return;
@@ -1011,16 +1057,14 @@ static void MAIN_Key_UP_DOWN(bool bKeyPressed, bool bKeyHeld, int8_t Direction)
}
// jump to the next channel
CHFRSCANNER_Start(false, Direction);
gScanPauseDelayIn_10ms = 1;
gScheduleScanListen = false;
CHFRSCANNER_ManualResume(Direction);
gPttWasReleased = true;
}
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;
@@ -1079,4 +1123,4 @@ void MAIN_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
break;
}
}
}
+358 -124
View File
@@ -30,6 +30,10 @@
#include "driver/eeprom.h"
#include "driver/gpio.h"
#include "driver/keyboard.h"
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
#include "driver/mb_flash.h"
#include "ui/multiboot.h"
#endif
#include "frequencies.h"
#include "helper/battery.h"
#include "misc.h"
@@ -68,7 +72,7 @@ uint8_t gUnlockAllTxConfCnt;
//
EEPROM_ReadBuffer(0x1F88, &misc, 8);
misc.BK4819_XtalFreqLow = value;
EEPROM_WriteBuffer(0x1F88, &misc);
EEPROM_WriteBuffer(0x1F88, &misc, 8);
}
}
#endif
@@ -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,29 +208,31 @@ 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;
*pMax = ARRAY_SIZE(gSubMenu_RX_TX) - 1;
break;
#ifndef ENABLE_FEAT_F4HWN
#ifdef ENABLE_AM_FIX
case MENU_AM_FIX:
#endif
#endif
#if defined(ENABLE_FEAT_F4HWN) && defined(ENABLE_FEAT_F4HWN_LOGO_SAV)
case MENU_SET_SAV:
//*pMin = 0;
*pMax = SET_SAV_LEN - 1;
break;
#endif
#ifdef ENABLE_AUDIO_BAR
case MENU_MIC_BAR:
#endif
@@ -419,13 +425,19 @@ 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:
//*pMin = 0;
*pMax = ARRAY_SIZE(gSubMenu_SET_MET) - 1;
break;
#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER
case MENU_SET_SCN:
//*pMin = 0;
*pMax = ARRAY_SIZE(gSubMenu_SET_SCN) - 1;
break;
#endif
#ifdef ENABLE_FEAT_F4HWN_AUDIO
case MENU_SET_AUD:
//*pMin = 0;
@@ -457,6 +469,28 @@ int MENU_GetLimits(uint8_t menu_id, int32_t *pMin, int32_t *pMax)
#endif
#endif
case MENU_VOL: {
// SysInf paginates:
// page 0 = identity,
// page 1 = build date/time,
// page 2 = battery,
// +1 if F4HWN_MEM (Flash/SRAM),
// +2 if F4HWN_QRCODE (Code QR + Wiki QR).
int32_t vol_max = 0;
#ifdef ENABLE_FEAT_F4HWN
vol_max += 2;
#endif
#ifdef ENABLE_FEAT_F4HWN_MEM
vol_max += 1;
#endif
#ifdef ENABLE_FEAT_F4HWN_QRCODE
vol_max += 2;
#endif
if (vol_max == 0) return -1;
*pMax = vol_max;
break;
}
default:
return -1;
}
@@ -594,7 +628,7 @@ void MENU_AcceptSetting(void)
case MENU_MEM_NAME:
for (int i = 9; i >= 0; i--) {
if (edit[i] != ' ' && edit[i] != '_' && edit[i] != 0x00 && edit[i] != 0xff)
if (edit[i] != ' ' && edit[i] != 0x00 && edit[i] != 0xff)
break;
edit[i] = ' ';
}
@@ -690,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;
@@ -717,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;
@@ -735,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;
@@ -778,7 +806,7 @@ void MENU_AcceptSetting(void)
case MENU_D_LIVE_DEC:
gSetting_live_DTMF_decoder = gSubMenuSelection;
gDTMF_RX_live_timeout = 0;
memset(gDTMF_RX_live, 0, sizeof(gDTMF_RX_live));
DTMF_clear_input_box_memory();
if (!gSetting_live_DTMF_decoder)
BK4819_DisableDTMF();
gFlagReconfigureVfos = true;
@@ -811,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;
@@ -829,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;
@@ -967,6 +985,11 @@ void MENU_AcceptSetting(void)
case MENU_SET_GUI:
gSetting_set_gui = gSubMenuSelection;
break;
#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER
case MENU_SET_SCN:
gSetting_set_scn = gSubMenuSelection;
break;
#endif
#ifdef ENABLE_FEAT_F4HWN_AUDIO
case MENU_SET_AUD:
if(gTxVfo->Modulation == MODULATION_AM)
@@ -997,6 +1020,11 @@ void MENU_AcceptSetting(void)
case MENU_SET_TMR:
gSetting_set_tmr = gSubMenuSelection;
break;
#ifdef ENABLE_FEAT_F4HWN_LOGO_SAV
case MENU_SET_SAV:
gSetting_set_sav = gSubMenuSelection;
break;
#endif
case MENU_TX_LOCK:
gTxVfo->TX_LOCK = gSubMenuSelection;
gRequestSaveChannel = 1;
@@ -1030,6 +1058,11 @@ void MENU_ShowCurrentSetting(void)
gSubMenuSelection = gEeprom.SQUELCH_LEVEL;
break;
case MENU_VOL:
// SysInf is paginated; always start on page 0 (identity).
gSubMenuSelection = 0;
break;
case MENU_STEP:
gSubMenuSelection = FREQUENCY_GetSortedIdxFromStepIdx(gTxVfo->STEP_SETTING);
break;
@@ -1042,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:
{
@@ -1240,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;
@@ -1296,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;
@@ -1426,6 +1451,11 @@ void MENU_ShowCurrentSetting(void)
case MENU_SET_GUI:
gSubMenuSelection = gSetting_set_gui;
break;
#ifdef ENABLE_FEAT_F4HWN_SCAN_FASTER
case MENU_SET_SCN:
gSubMenuSelection = gSetting_set_scn;
break;
#endif
#ifdef ENABLE_FEAT_F4HWN_AUDIO
case MENU_SET_AUD:
if(gTxVfo->Modulation == MODULATION_AM)
@@ -1454,6 +1484,11 @@ void MENU_ShowCurrentSetting(void)
case MENU_SET_TMR:
gSubMenuSelection = gSetting_set_tmr;
break;
#ifdef ENABLE_FEAT_F4HWN_LOGO_SAV
case MENU_SET_SAV:
gSubMenuSelection = gSetting_set_sav;
break;
#endif
case MENU_TX_LOCK:
gSubMenuSelection = gTxVfo->TX_LOCK;
break;
@@ -1464,6 +1499,29 @@ void MENU_ShowCurrentSetting(void)
}
}
static KEY_Code_t edit_last_key = 255;
static uint8_t edit_char_index = 0;
static const char* const char_map[10] = {
" 0", // KEY_0
".,-()@/\\+=*#<>[]~1", // KEY_1
"abc2", // KEY_2
"def3", // KEY_3
"ghi4", // KEY_4
"jkl5", // KEY_5
"mno6", // KEY_6
"pqrs7", // KEY_7
"tuv8", // KEY_8
"wxyz9" // KEY_9
};
static bool MENU_IsEditingName() {
return !gCssBackgroundScan
&& UI_MENU_GetCurrentMenuId() == MENU_MEM_NAME
&& gIsInSubMenu
&& edit_index >= 0;
}
static void MENU_Key_0_to_9(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
{
uint8_t Offset;
@@ -1471,37 +1529,89 @@ static void MENU_Key_0_to_9(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
int32_t Max;
uint16_t Value = 0;
if (bKeyHeld || !bKeyPressed)
if (!bKeyPressed)
return;
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
if (UI_MENU_GetCurrentMenuId() == MENU_MEM_NAME && edit_index >= 0)
{ // currently editing the channel name
if (edit_index >= 10)
return;
if (edit_index < 10)
uint8_t key_idx = Key - KEY_0;
if (bKeyHeld)
{
if (Key <= KEY_9)
edit[edit_index] = '0' + key_idx;
edit_last_key = 255;
gRequestDisplayScreen = DISPLAY_MENU;
return;
}
if (Key != edit_last_key)
{
edit_last_key = Key;
edit_char_index = 0;
}
else
{
edit_char_index++;
if (char_map[key_idx][edit_char_index] == '\0')
{
edit[edit_index] = '0' + Key - KEY_0;
if (++edit_index >= 10)
{ // exit edit
gFlagAcceptSetting = false;
gAskForConfirmation = 1;
}
gRequestDisplayScreen = DISPLAY_MENU;
edit_char_index = 0;
}
}
char c = char_map[key_idx][edit_char_index];
if (edit_is_uppercase && c >= 'a' && c <= 'z')
{
c -= 32;
}
edit[edit_index] = c;
gRequestDisplayScreen = DISPLAY_MENU;
return;
}
if (bKeyHeld)
return;
INPUTBOX_Append(Key);
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)
@@ -1586,7 +1696,7 @@ static void MENU_Key_0_to_9(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
gInputBoxIndex = 0;
//Value = ((gInputBox[0] * 100) + (gInputBox[1] * 10) + gInputBox[2]) - 1;
Value = ((gInputBox[0] * 1000) + (gInputBox[1] * 100) + (gInputBox[2] * 10) + gInputBox[3]) - 1;
Value = (((gInputBox[0] * 10 + gInputBox[1]) * 10 + gInputBox[2]) * 10 + gInputBox[3]) - 1;
if (IS_MR_CHANNEL(Value))
{
@@ -1643,6 +1753,53 @@ static void MENU_Key_0_to_9(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
static void MENU_Key_EXIT(bool bKeyPressed, bool bKeyHeld)
{
if (MENU_IsEditingName())
{
if (!bKeyPressed)
{
if (bKeyHeld)
return; // release after a long press, keep editing
if (edit_index == 0)
goto Skip;
if (edit_index > 0)
{ // step back one character while editing the channel name
edit_index--;
edit_last_key = 255;
gAskForConfirmation = 0;
gRequestDisplayScreen = DISPLAY_MENU;
}
return;
}
if (!bKeyHeld)
{ // wait to see if the user wants a short exit or a long backspace
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
return;
}
Skip:
/* Backlight related menus set full brightness. Set it back to the configured value,
just in case we are editing from one of them. */
BACKLIGHT_TurnOn();
gAskForConfirmation = 0;
gIsInSubMenu = false;
gInputBoxIndex = 0;
gFlagRefreshSetting = true;
#ifdef ENABLE_VOICE
gAnotherVoiceID = VOICE_ID_CANCEL;
#endif
gRequestDisplayScreen = DISPLAY_MENU;
return;
}
if (bKeyHeld || !bKeyPressed)
return;
@@ -1650,32 +1807,41 @@ static void MENU_Key_EXIT(bool bKeyPressed, bool bKeyHeld)
if (!gCssBackgroundScan)
{
/* Backlight related menus set full brightness. Set it back to the configured value,
just in case we are exiting from one of them. */
BACKLIGHT_TurnOn();
if (gIsInSubMenu)
{
if (gInputBoxIndex == 0 || UI_MENU_GetCurrentMenuId() != MENU_OFFSET)
{
gAskForConfirmation = 0;
gIsInSubMenu = false;
gInputBoxIndex = 0;
gFlagRefreshSetting = true;
#ifdef ENABLE_VOICE
gAnotherVoiceID = VOICE_ID_CANCEL;
#endif
goto Skip;
}
else
{
/* Backlight related menus set full brightness. Set it back to the configured value,
just in case we are exiting from one of them. */
BACKLIGHT_TurnOn();
gInputBox[--gInputBoxIndex] = 10;
gRequestDisplayScreen = DISPLAY_MENU;
}
// ***********************
gRequestDisplayScreen = DISPLAY_MENU;
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
@@ -1703,19 +1869,34 @@ static void MENU_Key_EXIT(bool bKeyPressed, bool bKeyHeld)
static void MENU_Key_MENU(const bool bKeyPressed, const bool bKeyHeld)
{
if (bKeyHeld || !bKeyPressed)
if (!bKeyPressed || (bKeyHeld && (!MENU_IsEditingName() || gAskForConfirmation)))
return;
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
@@ -1751,12 +1932,18 @@ static void MENU_Key_MENU(const bool bKeyPressed, const bool bKeyHeld)
SETTINGS_FetchChannelName(edit, gSubMenuSelection);
// pad the channel name out with '_'
edit_index = strlen(edit);
while (edit_index < 10)
edit[edit_index++] = '_';
edit[edit_index] = 0;
// pad the channel name out with ' '
size_t len = strlen(edit);
if (len < 10)
{
memset(edit + len, ' ', 10 - len);
edit[10] = '\0';
}
edit_index = 0; // 'edit_index' is going to be used as the cursor position
edit_last_key = 255;
edit_char_index = 0;
edit_is_uppercase = false;
// make a copy so we can test for change when exiting the menu item
memcpy(edit_original, edit, sizeof(edit_original));
@@ -1766,9 +1953,14 @@ static void MENU_Key_MENU(const bool bKeyPressed, const bool bKeyHeld)
else
if (edit_index >= 0 && edit_index < 10)
{ // editing the channel name characters
edit_last_key = 255;
if (++edit_index < 10)
return; // next char
if (bKeyHeld) {
edit_index = 10;
}
else if (++edit_index < 10) {
return;
}
// exit
gFlagAcceptSetting = false;
@@ -1789,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)
@@ -1817,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;
@@ -1844,7 +2073,7 @@ static void MENU_Key_MENU(const bool bKeyPressed, const bool bKeyHeld)
static void MENU_Key_STAR(const bool bKeyPressed, const bool bKeyHeld)
{
if (bKeyHeld || !bKeyPressed)
if (!bKeyPressed)
return;
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
@@ -1854,13 +2083,8 @@ static void MENU_Key_STAR(const bool bKeyPressed, const bool bKeyHeld)
if (edit_index < 10)
{
edit[edit_index] = '-';
if (++edit_index >= 10)
{ // exit edit
gFlagAcceptSetting = false;
gAskForConfirmation = 1;
}
edit[edit_index] = !bKeyHeld ? '-' : '*';
edit_last_key = 255;
gRequestDisplayScreen = DISPLAY_MENU;
}
@@ -1868,6 +2092,9 @@ static void MENU_Key_STAR(const bool bKeyPressed, const bool bKeyHeld)
return;
}
if (bKeyHeld)
return;
RADIO_SelectVfos();
#ifdef ENABLE_NOAA
@@ -1898,15 +2125,33 @@ static void MENU_Key_UP_DOWN(bool bKeyPressed, bool bKeyHeld, int8_t Direction)
uint16_t Channel;
bool bCheckScanList;
if (!bKeyPressed)
return;
if (!bKeyHeld) {
gInputBoxIndex = 0;
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;
}
if (UI_MENU_GetCurrentMenuId() == MENU_MEM_NAME && gIsInSubMenu && edit_index >= 0)
{ // change the character
if (bKeyPressed && edit_index < 10 && Direction != 0)
if (edit_index < 10 && Direction != 0)
{
const char unwanted[] = "$%&!\"':;?^`|{}";
const char unwanted[] = "$%&!\"':;?^`|{}_";
char c = edit[edit_index] + Direction;
unsigned int i = 0;
while (i < sizeof(unwanted) && c >= 32 && c <= 126)
@@ -1918,25 +2163,13 @@ static void MENU_Key_UP_DOWN(bool bKeyPressed, bool bKeyHeld, int8_t Direction)
}
}
edit[edit_index] = (c < 32) ? 126 : (c > 126) ? 32 : c;
edit_last_key = 255;
gRequestDisplayScreen = DISPLAY_MENU;
}
return;
}
if (!bKeyHeld)
{
if (!bKeyPressed)
return;
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
gInputBoxIndex = 0;
}
else
if (!bKeyPressed)
return;
if (SCANNER_IsScanning()) {
return;
}
@@ -2067,19 +2300,20 @@ void MENU_ProcessKeys(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
case KEY_F:
if (UI_MENU_GetCurrentMenuId() == MENU_MEM_NAME && edit_index >= 0)
{ // currently editing the channel name
if (!bKeyHeld && bKeyPressed)
if (!bKeyPressed)
break;
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
if (edit_index < 10)
{
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
if (edit_index < 10)
{
edit[edit_index] = ' ';
if (++edit_index >= 10)
{ // exit edit
gFlagAcceptSetting = false;
gAskForConfirmation = 1;
}
gRequestDisplayScreen = DISPLAY_MENU;
}
if (bKeyHeld)
edit[edit_index] = '#';
edit_is_uppercase = !edit_is_uppercase;
edit_last_key = 255;
gRequestDisplayScreen = DISPLAY_MENU;
}
break;
}
-187
View File
@@ -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;
}
-65
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+89
View File
@@ -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
+122 -29
View File
@@ -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)
@@ -62,7 +157,7 @@ static void SCANNER_Key_DIGITS(KEY_Code_t Key, bool bKeyPressed, bool bKeyHeld)
gInputBoxIndex = 0;
// uint16_t chan = ((gInputBox[0] * 100) + (gInputBox[1] * 10) + gInputBox[2]) - 1;
uint16_t chan = ((gInputBox[0] * 1000) + (gInputBox[1] * 100) + (gInputBox[2] * 10) + gInputBox[3]) - 1;
uint16_t chan = (((gInputBox[0] * 10 + gInputBox[1]) * 10 + gInputBox[2]) * 10 + gInputBox[3]) - 1;
if (IS_MR_CHANNEL(chan)) {
#ifdef ENABLE_VOICE
gAnotherVoiceID = (VOICE_ID_t)Key;
@@ -243,16 +338,13 @@ static void SCANNER_Key_STAR(bool bKeyPressed, bool bKeyHeld)
return;
}
static void SCANNER_Key_UP_DOWN(bool bKeyPressed, bool pKeyHeld, int8_t Direction)
static void SCANNER_Key_UP_DOWN(bool bKeyPressed, bool bKeyHeld, int8_t Direction)
{
if (pKeyHeld) {
if (!bKeyPressed)
return;
if (!bKeyPressed) {
return;
}
else {
if (!bKeyPressed)
return;
if (!bKeyHeld) {
gInputBoxIndex = 0;
gBeepToPlay = BEEP_1KHZ_60MS_OPTIONAL;
}
@@ -266,7 +358,7 @@ static void SCANNER_Key_UP_DOWN(bool bKeyPressed, bool pKeyHeld, int8_t Directio
gShowChPrefix = RADIO_CheckValidChannel(gScanChannel, false, 0);
gRequestDisplayScreen = DISPLAY_SCANNER;
}
else
else if (!bKeyHeld)
gBeepToPlay = BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL;
}
@@ -344,8 +436,9 @@ void SCANNER_Start(bool singleFreq)
gScanCssState = SCAN_CSS_STATE_OFF;
gScanFrequency = 0xFFFFFFFF;
BK4819_PickRXFilterPathBasedOnFrequency(gScanFrequency);
BK4819_EnableFrequencyScan();
// 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;
}
@@ -369,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)
@@ -380,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
@@ -401,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 ?
@@ -418,27 +517,21 @@ void SCANNER_TimeSlice10ms(void)
else
scanHitCount = 0;
BK4819_DisableFrequencyScan();
BK4819_SetFrequencyScan(false);
if (scanHitCount < 3) {
BK4819_EnableFrequencyScan();
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;
}
@@ -509,8 +602,8 @@ void SCANNER_TimeSlice500ms(void)
if (SCANNER_IsScanning() && gScannerSaveState == SCAN_SAVE_NO_PROMPT && gScanCssState < SCAN_CSS_STATE_FOUND) {
gScanProgressIndicator++;
#ifndef ENABLE_NO_CODE_SCAN_TIMEOUT
if (gScanProgressIndicator > 32) {
if (gScanCssState == SCAN_CSS_STATE_SCANNING && !gScanSingleFrequency)
if (gScanCssState == SCAN_CSS_STATE_SCANNING && gScanProgressIndicator > 32) {
if (!gScanSingleFrequency)
gScanCssState = SCAN_CSS_STATE_FOUND;
else
gScanCssState = SCAN_CSS_STATE_FAILED;
@@ -528,4 +621,4 @@ void SCANNER_TimeSlice500ms(void)
bool SCANNER_IsScanning(void)
{
return gCssBackgroundScan || (gScreenToDisplay == DISPLAY_SCANNER);
}
}
+1133 -326
View File
File diff suppressed because it is too large. Load diff
+20 -13
View File
@@ -42,7 +42,8 @@
#include <stdint.h>
#include <string.h>
static const uint8_t DrawingEndY = 40;
static const uint8_t DrawingEndY = 40;
static const uint8_t DrawingTopY = 8; // Reserve top 8px for frequency display (gFrameBuffer[0])
static const uint8_t U8RssiMap[] = {
121,
@@ -78,29 +79,35 @@ static const uint16_t scanStepValues[] = {
static const uint16_t scanStepBWRegValues[] = {
// RX RXw TX BW
// 0b0 000 000 001 01 1000
// 1
// 1 (S_STEP_0_01kHz, index 0)
0b0000000001011000, // 6.25
// 10
// 10 (S_STEP_0_1kHz, index 1)
0b0000000001011000, // 6.25
// 50
// 50 (S_STEP_0_5kHz, index 2)
0b0000000001011000, // 6.25
// 100
// 100 (S_STEP_1_0kHz, index 3)
0b0000000001011000, // 6.25
// 250
// 250 (S_STEP_2_5kHz, index 4)
0b0000000001011000, // 6.25
// 500
// 500 (S_STEP_5_0kHz, index 5)
0b0010010001011000, // 6.25
// 625
// 625 (S_STEP_6_25kHz, index 6)
0b0100100001011000, // 6.25
// 833
// 833 (S_STEP_8_33kHz, index 7)
0b0110110001001000, // 6.25
// 1000
// 1000 (S_STEP_10_0kHz, index 8)
0b0110110001001000, // 6.25
// 1250
// 1250 (S_STEP_12_5kHz, index 9)
0b0111111100001000, // 6.25
// 2500
// 1500 (S_STEP_15_0kHz, index 10)
0b0011011000101000, // 25
// 10000
// 2000 (S_STEP_20_0kHz, index 11)
0b0011011000101000, // 25
// 2500 (S_STEP_25_0kHz, index 12)
0b0011011000101000, // 25
// 5000 (S_STEP_50_0kHz, index 13)
0b0011011000101000, // 25
// 10000 (S_STEP_100_0kHz, index 14)
0b0011011000101000, // 25
};
+333 -30
View File
@@ -20,7 +20,7 @@
#if !defined(ENABLE_OVERLAY)
#include "py32f0xx.h"
#endif
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
#include "app/fm.h"
#endif
#include "app/uart.h"
@@ -45,6 +45,14 @@
#include "settings.h"
#include "version.h"
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
#include "driver/mb_flash.h"
#endif
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_APPS
#include "apps/app_overlay.h"
#endif
#if defined(ENABLE_OVERLAY)
#include "sram-overlay.h"
#endif
@@ -193,7 +201,8 @@ typedef union
#ifdef ENABLE_USB
static void SendReply_VCP(void *pReply, uint16_t Size)
{
static uint8_t VCP_ReplyBuf[MAX_REPLY_SIZE + sizeof(Header_t) + sizeof(Footer_t)];
static uint8_t VCP_ReplyBuf[MAX_REPLY_SIZE + sizeof(Header_t) + sizeof(Footer_t)]
__attribute__((aligned(4)));
// !!
if (Size > MAX_REPLY_SIZE)
@@ -201,11 +210,11 @@ static void SendReply_VCP(void *pReply, uint16_t Size)
return;
}
memcpy(VCP_ReplyBuf + sizeof(Header_t), pReply, Size);
uint8_t *pBody = VCP_ReplyBuf + sizeof(Header_t);
uint8_t *pFooter = pBody + Size;
Header_t *pHeader = (Header_t *)VCP_ReplyBuf;
Footer_t *pFooter = (Footer_t *)(VCP_ReplyBuf + sizeof(Header_t) + Size);
pReply = VCP_ReplyBuf + sizeof(Header_t);
memcpy(pBody, pReply, Size);
pReply = pBody;
if (bIsEncrypted)
{
@@ -215,23 +224,29 @@ static void SendReply_VCP(void *pReply, uint16_t Size)
pBytes[i] ^= Obfuscation[i % 16];
}
pHeader->ID = 0xCDAB;
pHeader->Size = Size;
/* Build the transport header/footer byte by byte. The reply body may have
* an odd size, so pFooter is not necessarily half-word aligned; casting it
* to Footer_t and storing ID as uint16_t can HardFault on Cortex-M0+. */
VCP_ReplyBuf[0] = 0xAB;
VCP_ReplyBuf[1] = 0xCD;
VCP_ReplyBuf[2] = (uint8_t)(Size & 0xFFu);
VCP_ReplyBuf[3] = (uint8_t)(Size >> 8);
// VCP_Send((uint8_t *)&Header, sizeof(Header));
// VCP_Send(pReply, Size);
if (bIsEncrypted)
{
pFooter->Padding[0] = Obfuscation[(Size + 0) % 16] ^ 0xFF;
pFooter->Padding[1] = Obfuscation[(Size + 1) % 16] ^ 0xFF;
pFooter[0] = Obfuscation[(Size + 0) % 16] ^ 0xFF;
pFooter[1] = Obfuscation[(Size + 1) % 16] ^ 0xFF;
}
else
{
pFooter->Padding[0] = 0xFF;
pFooter->Padding[1] = 0xFF;
pFooter[0] = 0xFF;
pFooter[1] = 0xFF;
}
pFooter->ID = 0xBADC;
pFooter[2] = 0xDC;
pFooter[3] = 0xBA;
// VCP_Send((uint8_t *)&Footer, sizeof(Footer));
@@ -249,6 +264,7 @@ static void SendReply(uint32_t Port, void *pReply, uint16_t Size)
}
#endif
#if defined(ENABLE_UART)
Header_t Header;
Footer_t Footer;
@@ -279,15 +295,17 @@ static void SendReply(uint32_t Port, void *pReply, uint16_t Size)
Footer.ID = 0xBADC;
UART_Send(&Footer, sizeof(Footer));
#endif
}
static void SendVersion(uint32_t Port)
{
REPLY_0514_t Reply;
Reply.Data.Padding[0] = Reply.Data.Padding[1] = 0;
Reply.Header.ID = 0x0515;
Reply.Header.Size = sizeof(Reply.Data);
strcpy(Reply.Data.Version, Version);
strncpy(Reply.Data.Version, Version, sizeof(Reply.Data.Version));
Reply.Data.bHasCustomAesKey = bHasCustomAesKey;
Reply.Data.bIsInLockScreen = bIsInLockScreen;
Reply.Data.Challenge[0] = gChallenge[0];
@@ -342,14 +360,14 @@ static void CMD_0514(uint32_t Port, const uint8_t *pBuffer)
}
#endif
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
gFmRadioCountdown_500ms = fm_radio_countdown_500ms;
#endif
gSerialConfigCountDown_500ms = 12; // 6 sec
// turn the LCD backlight off
BACKLIGHT_TurnOff();
// 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
// turn the LCD backlight off
BACKLIGHT_TurnOff();
// 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
View File
@@ -32,6 +32,6 @@ enum
bool UART_IsCommandAvailable(uint32_t Port);
void UART_HandleCommand(uint32_t Port);
void UART_ServiceCommands(void);
#endif
+291
View File
@@ -0,0 +1,291 @@
/* Copyright 2026 Armel F4HWN
* https://github.com/armel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Overlay-app ABI (POC).
*
* A leaf, modal "overlay app" is a self-contained code blob stored in the
* external SPI flash and copied into the 4 KiB overlay RAM (the PY25Q16 sector
* cache) to run, then discarded. It is linked with NOCROSSREFS and must not
* reference resident firmware symbols: every service it needs is reached through
* this table, which the loader fills and passes to the entry point.
*
* void app_main(const app_api_t *api); // entry, at blob offset 0
*
* Both the firmware loader and the app include THIS header. Within one ABI
* major, app_api_t is append-only: existing fields may never move, disappear or
* change signature. Append a service and bump APP_API_LEVEL; only apps using
* that service need the new level. A breaking layout change bumps
* APP_ABI_MAJOR and resets APP_API_LEVEL to 1.
*/
#ifndef APPS_APP_API_H
#define APPS_APP_API_H
#include <stdint.h>
#include <stdbool.h>
/* First unpublished/public baseline: all services currently present below are
* ABI major 1, API level 1. */
#define APP_ABI_MAJOR 1u
#define APP_API_LEVEL 1u
/* KEY codes mirrored from driver/keyboard.h (enum KEY_Code_e). Kept in sync by
* value so the app stays independent of the firmware headers. */
enum {
APP_KEY_0 = 0,
APP_KEY_1 = 1,
APP_KEY_2 = 2,
APP_KEY_3 = 3,
APP_KEY_4 = 4,
APP_KEY_5 = 5,
APP_KEY_6 = 6,
APP_KEY_7 = 7,
APP_KEY_8 = 8,
APP_KEY_9 = 9,
APP_KEY_MENU = 10,
APP_KEY_UP = 11,
APP_KEY_DOWN = 12,
APP_KEY_EXIT = 13,
APP_KEY_STAR = 14,
APP_KEY_F = 15,
APP_KEY_PTT = 16,
APP_KEY_INVALID = 19,
APP_KEY_SAVER = 0xFD, /* virtual: saver active, skip this app frame */
APP_KEY_WAKE = 0xFE, /* virtual: saver dismissed, redraw without action */
};
/* The framebuffer is the resident gFrameBuffer[FRAME_LINES][128]; the app draws
* into it and calls a blit_* to push it to the LCD. */
typedef uint8_t (*app_fb_t)[128];
/* Broadcast FM shared state (mirrors gEeprom.FM_*), read/written via fm_state. */
#define APP_FM_CH_MAX 48
typedef struct {
uint16_t freq_playing; /* current tuned frequency (0.1 MHz) */
uint16_t sel_freq; /* last VFO frequency */
uint8_t band; /* 0..3 */
uint8_t is_mr; /* memory mode */
uint8_t sel_ch; /* selected memory channel 0..47 */
} app_fm_state_t;
/* Compact, pointer-free description of one receiver in the resident triple-VFO
* service. Overlay apps must never see VFO_Info_t directly: its layout varies
* with firmware features and contains resident pointers. */
typedef struct {
uint32_t frequency; /* RX frequency, x10 Hz */
uint16_t channel; /* memory channel, zero based */
uint16_t step; /* step, 10 Hz units */
uint16_t code_value; /* CTCSS x0.1 Hz or DCS octal source value */
int16_t rssi_dbm; /* last/current corrected RSSI */
uint8_t modulation;
uint8_t power;
uint8_t bandwidth;
uint8_t code_type;
uint8_t code;
uint8_t offset_direction;
uint8_t reverse;
uint8_t squelch;
uint8_t flags; /* APP_TRIVFO_* below */
char name[11]; /* channel name, trimmed and NUL terminated */
} app_trivfo_info_t;
enum {
APP_TRIVFO_SELECTED = 1u << 0,
APP_TRIVFO_TUNED = 1u << 1,
APP_TRIVFO_RECEIVING = 1u << 2,
APP_TRIVFO_TX = 1u << 3,
APP_TRIVFO_USER_POWER = 1u << 4,
APP_TRIVFO_AUDIO_BAR = 1u << 5,
APP_TRIVFO_GUI_CLASSIC = 1u << 6,
APP_TRIVFO_PTT_ONEPUSH = 1u << 7,
};
enum {
APP_TRIVFO_SCAN = 0,
APP_TRIVFO_RX = 1,
APP_TRIVFO_HOLD = 2,
APP_TRIVFO_TX_STATE = 3,
};
/* Pointer-free BEAM channel description. The resident bridge translates this
* stable ABI type to/from feature-dependent VFO_Info_t. */
typedef struct {
uint32_t rx_frequency;
uint32_t tx_offset_frequency;
uint8_t rx_code;
uint8_t tx_code;
uint8_t rx_codetype;
uint8_t tx_codetype;
uint8_t modulation;
uint8_t tx_offset_direction;
uint8_t tx_lock;
uint8_t busy_channel_lock;
uint8_t output_power;
uint8_t channel_bandwidth;
uint8_t frequency_reverse;
uint8_t dtmf_ptt_id_mode;
uint8_t dtmf_decoding_enable;
uint8_t step_setting;
uint8_t scrambling_type;
uint8_t band;
uint8_t scanlist;
uint8_t compander;
char name[16]; /* fixed-width wire field; NUL termination is not required */
} app_beam_channel_t;
_Static_assert(sizeof(app_beam_channel_t) == 44u,
"BEAM ABI/wire channel layout changed");
enum {
APP_BEAM_RX_WAIT = 0,
APP_BEAM_RX_READY = 1,
APP_BEAM_RX_ERROR = 2,
};
/* CRITICAL PERSISTENCE RULE
*
* The app executes from the RAM buffer also used as PY25Q16's sector cache.
* Therefore no API callback may erase or write external flash while app_main()
* is running: a read-modify-write would overwrite the executing app. Services
* such as cfg_save, fm_commit and beam_save must only stage resident RAM state;
* APP_LaunchOverlay commits it after app_main() returns. */
typedef struct app_api {
/* Fixed four-byte prefix; services remain naturally pointer-aligned. */
uint8_t abi_major; /* == APP_ABI_MAJOR */
uint8_t api_level; /* == APP_API_LEVEL */
uint16_t api_size; /* sizeof(app_api_t), for optional probing */
app_fb_t fb; /* -> gFrameBuffer */
/* ---- display ---- */
void (*display_clear)(void); /* UI_DisplayClear */
void (*status_clear)(void); /* UI_StatusClear */
void (*draw_line)(app_fb_t fb, int16_t x1, int16_t y1,
int16_t x2, int16_t y2, bool black); /* UI_DrawLineBuffer */
void (*draw_rect)(app_fb_t fb, int16_t x1, int16_t y1,
int16_t x2, int16_t y2, bool black); /* UI_DrawRectangleBuffer */
void (*print_bold)(const char *s, uint8_t start,
uint8_t end, uint8_t line); /* UI_PrintStringSmallBold */
void (*print_tiny)(const char *s, uint8_t x, uint8_t y,
bool statusbar, bool fill); /* GUI_DisplaySmallest */
void (*blit_full)(void); /* ST7565_BlitFullScreen */
void (*blit_line)(unsigned line); /* ST7565_BlitLine */
void (*blit_status)(void); /* ST7565_BlitStatusLine */
/* ---- input / system ---- */
uint8_t (*get_key)(void); /* KEYBOARD_GetKey, returns an APP_KEY_* code */
void (*delay_ms)(uint32_t ms); /* SYSTEM_DelayMs */
/* ---- audio / indicator ---- */
void (*play_tone)(uint16_t tone, uint16_t ms); /* full BK4819 tone burst + AF path */
void (*led)(bool on); /* green GPIO indicator */
/* ---- extra text drawing ---- */
void (*print_normal)(const char *s, uint8_t start, uint8_t end, uint8_t line); /* UI_PrintStringSmallNormal */
void (*print_inverse)(const char *s, uint8_t x, uint8_t line,
bool statusbar, bool fill, uint8_t endX); /* GUI_DisplaySmallestInverse */
void (*display_freq)(const char *s, uint8_t x, uint8_t y, bool statusbar); /* UI_DisplayFrequency (big font) */
/* ---- BK4819 radio access ---- */
int16_t (*rssi_dbm)(void); /* corrected RSSI of the RX VFO, dBm */
uint16_t (*bk_read)(uint8_t reg); /* BK4819_ReadRegister */
void (*bk_write)(uint8_t reg, uint16_t v);/* BK4819_WriteRegister */
void (*set_agc)(bool on); /* BK4819_SetAGC */
void (*set_af)(uint8_t mode); /* BK4819_SetAF (APP_AF_* below) */
void (*audio_path)(bool on); /* AUDIO_AudioPathOn/Off */
void (*prepare_tone)(void); /* BK4819_PrepareToPlayTone(true) */
void (*play_tone_raw)(uint16_t hz, uint16_t ms); /* BK4819_PlayToneRaw */
void (*tones_off_rx)(void); /* BK4819_TurnsOffTones_TurnsOnRX */
uint32_t (*rx_freq)(void); /* current RX VFO frequency (x10 Hz) */
/* ---- config persistence (deferred: staged now, committed on exit) ---- */
void (*cfg_load)(uint8_t *buf, uint8_t len); /* read the app's saved config bytes */
void (*cfg_save)(const uint8_t *buf, uint8_t len); /* stage bytes; resident commits after the app returns */
/* ---- battery + backlight ---- */
void (*draw_battery)(void); /* UI_DrawStatusBattery into the status line */
void (*battery_sample)(void); /* periodic ADC sample so the level stays live */
void (*backlight_on)(void); /* BACKLIGHT_TurnOn */
void (*backlight_update)(void); /* resident fade + BLTime service */
void (*audio_scope)(uint8_t line, bool active); /* shared MAIN microphone scope */
uint8_t *status_line; /* -> gStatusLine (for status-bar icons) */
/* ---- TX (beacon) ---- */
uint8_t (*tx_state)(void); /* 0 = OK to transmit, else a denial code */
void (*tx_set_params)(void); /* RADIO_SetTxParameters (key up: carrier+PA) */
void (*tx_tone)(uint16_t hz);/* BK4819_TransmitTone prime (MCW tone) */
void (*tx_mute)(bool on); /* key the tone on(false)/off(true) via TxMute */
void (*tx_end)(void); /* PA off + RADIO_SetupRegisters (back to RX) */
void (*tx_carrier)(bool on); /* gate the PA on/off (beacon CARR carrier keying) */
uint32_t (*tx_freq)(void); /* current TX VFO frequency (x10 Hz) */
void (*boot_callsign)(char *buf, uint8_t len); /* sanitised callsign from the boot message */
void (*print_string)(const char *s, uint8_t start, uint8_t end,
uint8_t line, uint8_t width); /* UI_PrintString (big font) */
/* ---- broadcast FM (BK1080), sovereign: no BK4819 dual-watch ---- */
void (*fm_enter)(uint16_t freq, uint8_t band); /* BK1080_Init + antenna filter + audio on */
void (*fm_exit)(void); /* audio off + BK1080_Init0 + restore filter */
void (*fm_set_freq)(uint16_t freq, uint8_t band);/* BK1080_SetFrequency (freq in 0.1 MHz) */
uint16_t (*fm_lo)(uint8_t band); /* band low limit (0.1 MHz) */
uint16_t (*fm_hi)(uint8_t band); /* band high limit (0.1 MHz) */
void (*fm_mute)(bool mute); /* BK1080_Mute */
int8_t (*fm_valid)(uint16_t freq, uint16_t lo); /* FM_CheckFrequencyLock: 0 = station */
uint16_t *fm_channels; /* -> gFM_Channels[APP_FM_CH_MAX], shared RAM r/w */
void (*fm_state)(app_fm_state_t *s, bool write);/* read/write the resident gEeprom.FM_* */
void (*fm_commit)(void); /* deferred SETTINGS_SaveFM (config + channels) */
/* ---- navigation (API level 1 baseline) ----
* Convert a raw APP_KEY_UP/DOWN into a semantic value direction:
* UV-K5 UP/DOWN -> +1/-1
* UV-K1 LEFT/RIGHT -> -1/+1
* Returns 0 for any other key. Keep get_key() raw for spatial controls. */
int8_t (*nav_dir)(uint8_t key);
/* ---- triple VFO (optional resident capability APP_CAP_TRIVFO) ----
* A and B are the live Main Display VFOs. C is a resident temporary VFO
* loaded from c_channel (or the first valid memory after B when invalid).
* tick is called every 20 ms by the app and returns APP_TRIVFO_* state. */
uint16_t (*trivfo_enter)(uint16_t c_channel);
void (*trivfo_leave)(void);
void (*trivfo_get)(uint8_t vfo, app_trivfo_info_t *info);
void (*trivfo_select)(uint8_t vfo);
uint16_t (*trivfo_step)(uint8_t vfo, int8_t direction);
uint8_t (*trivfo_tick)(void);
uint8_t (*trivfo_ptt)(bool pressed); /* physical PTT edge; resident applies SetPTT */
/* ---- BEAM channel transfer (optional resident capability APP_CAP_BEAM) ---- */
void (*beam_prepare)(void); /* tune the fixed narrow-band FSK channel */
void (*beam_leave)(void); /* defensively stop FSK before app return */
void (*beam_get)(app_beam_channel_t *channel); /* export selected VFO */
uint16_t (*beam_save)(const app_beam_channel_t *channel); /* first free MR, or 0xffff */
void (*beam_send)(uint16_t *packet); /* transmit one 36-word FSK packet */
void (*beam_rx)(bool start); /* arm or stop FSK reception */
uint8_t (*beam_rx_poll)(uint16_t *packet); /* APP_BEAM_RX_* */
void (*beam_draw)(const char *status); /* MAIN display with one BEAM center line */
} app_api_t;
/* BK4819 AF modes for set_af (mirror driver/bk4819.h values). */
enum { APP_AF_MUTE = 0, APP_AF_FM = 1, APP_AF_AM = 7 };
/* Register ids the apps use (mirror driver/bk4819-regs.h). */
enum { APP_BK_REG_13 = 0x13 };
/* Entry point every app blob exports, placed at blob offset 0. */
typedef void (*app_entry_t)(const app_api_t *api);
#endif /* APPS_APP_API_H */
+335
View File
@@ -0,0 +1,335 @@
/* Copyright 2026 Armel F4HWN
* https://github.com/armel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "apps/app_overlay.h"
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_APPS
#include <string.h>
#include "apps/app_menu.h"
#include "app/app.h"
#include "driver/backlight.h"
#include "driver/st7565.h"
#include "driver/keyboard.h"
#include "driver/system.h"
#include "driver/gpio.h"
#include "ui/helper.h"
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
#include "k5viewer.h"
#endif
/* "F4HWN APPS" banner and the same thin separator used by the multiboot
* selector. Bit 3 leaves room for the selected-row capsule's top edge. */
static void app_status_bar(void)
{
UI_StatusClear();
GUI_DisplaySmallestInverse("F4HWN APPS", 44, 0, true, true, 84);
for (uint8_t x = 2u; x < LCD_WIDTH - 2u; x++)
gFrameBuffer[0][x] |= 0x08u;
}
/* Bottom key hints, matching the multiboot selector. */
static void app_key_hints(void)
{
const uint8_t sp = 6u;
const char *act_exit = "QUIT";
const uint8_t ae = (uint8_t)strlen(act_exit);
const uint8_t xm = 4u;
const uint8_t xe = (uint8_t)(124u - ae * 4u - sp - 16u);
GUI_DisplaySmallestInverse("MENU", xm, 6, false, true, (uint8_t)(xm + 16u));
GUI_DisplaySmallest("RUN", (uint8_t)(xm + 16u + sp), 49, false, true);
GUI_DisplaySmallestInverse("EXIT", xe, 6, false, true, (uint8_t)(xe + 16u));
GUI_DisplaySmallest(act_exit, (uint8_t)(xe + 16u + sp), 49, false, true);
}
/* Fixed selection capsule around the primary information (the app name).
* Slot number stays in the normal font; size is plain 3x5 metadata. */
#define APP_NAME_BOX_START 12u
#define APP_NAME_BOX_END 102u
#define APP_NAME_TEXT_X 14u
static void app_wait_release(void);
static void app_invert_name(uint8_t line)
{
gFrameBuffer[line][APP_NAME_BOX_START] ^= 0x7Fu;
for (uint8_t x = APP_NAME_BOX_START + 1u; x < APP_NAME_BOX_END; x++)
{
gFrameBuffer[line][x] ^= 0xFFu;
gFrameBuffer[line - 1u][x] ^= 0x80u;
}
gFrameBuffer[line][APP_NAME_BOX_END] ^= 0x7Fu;
}
/* Debounced blocking key read, then wait for release (mirrors mb_get_key). */
static KEY_Code_t app_get_key(void)
{
for (;;)
{
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
/* APP_MenuOpen() is modal and does not return to APP_Update(). Keep
* serial key injection and the viewer connection alive while waiting. */
K5VIEWER_ParseInput();
#endif
APP_ModalBacklightTick(true);
if (APP_IsScreenSaverDisplayed())
{
if (KEYBOARD_GetKey() != KEY_INVALID)
{
APP_ModalScreenSaverExit();
BACKLIGHT_TurnOn();
app_wait_release();
return KEY_INVALID;
}
SYSTEM_DelayMs(10);
continue;
}
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
K5VIEWER_Update(false);
#endif
KEY_Code_t key = KEYBOARD_Poll();
if (key != KEY_INVALID)
{
SYSTEM_DelayMs(30);
if (KEYBOARD_Poll() == key)
{
BACKLIGHT_TurnOn();
while (KEYBOARD_Poll() != KEY_INVALID)
{
SYSTEM_DelayMs(10);
APP_ModalBacklightTick(false);
}
return key;
}
}
SYSTEM_DelayMs(10);
}
}
static void app_wait_release(void)
{
uint8_t stable = 0;
while (stable < 10u)
{
if (!GPIO_IsPttPressed() && KEYBOARD_Poll() == KEY_INVALID)
stable++;
else
stable = 0;
SYSTEM_DelayMs(10);
APP_ModalBacklightTick(true);
}
}
static void app_copy(char *dst, uint8_t cap, const char *src, uint8_t src_cap)
{
uint8_t n = 0;
while (n + 1u < cap && n < src_cap && src[n])
{
dst[n] = src[n];
n++;
}
dst[n] = '\0';
}
/* Display the code payload rounded up to 0.1 KiB, so the compact value never
* understates the space occupied by the app. The space before "KB" is omitted
* because this secondary value is rendered in the tiny 3x5 font. */
static void app_format_size(char out[6], uint32_t bytes)
{
if (bytes > APP_OVERLAY_MAX)
{
memcpy(out, "--KB", 5u);
return;
}
const uint16_t tenths = (uint16_t)((bytes * 10u + 1023u) / 1024u);
out[0] = (char)('0' + tenths / 10u);
out[1] = '.';
out[2] = (char)('0' + tenths % 10u);
out[3] = 'K';
out[4] = 'B';
out[5] = '\0';
}
/* Human-readable action for an APP_LaunchOverlay / APP_ValidateSlot failure. */
static const char *app_err_text(const app_header_t *header, uint8_t rc)
{
switch (rc)
{
case APP_ERR_SLOT: return "BAD SLOT";
case APP_ERR_MAGIC: return "NO APP";
case APP_ERR_ABI:
/* api_min == 0 also identifies pre-reset development blobs whose
* former uint16_t ABI value occupies these two bytes. */
if (header->api_min == 0u || header->abi_major < APP_ABI_MAJOR)
return "UPDATE APP";
return "UPDATE FIRMWARE";
case APP_ERR_NOT_COMMITTED: return "REINSTALL APP";
case APP_ERR_SIZE: return "UPDATE APP";
case APP_ERR_CRC: return "REINSTALL APP";
case APP_ERR_VMA: return "UPDATE APP";
case APP_ERR_AUTH: return "AUTH";
case APP_ERR_CAP: return "NOT SUPPORTED";
default: return "ERROR";
}
}
/* A launch failed: name the app and the action to take, then wait for a key. Without this
* an incompatible app would silently "do nothing" when selected. */
static void app_show_error(const app_header_t *header, uint8_t rc)
{
char nm[19];
app_copy(nm, sizeof(nm), header->name, APP_NAME_LEN);
UI_DisplayClear();
UI_StatusClear();
GUI_DisplaySmallestInverse("APP ERROR", 46, 0, true, true, 82);
UI_PrintStringSmallNormal(nm, 2, 0, 2); /* which app */
UI_PrintStringSmallNormal(app_err_text(header, rc), 2, 0, 4); /* action */
UI_PrintStringSmallNormal("Press any key", 2, 0, 6);
ST7565_BlitStatusLine();
ST7565_BlitFullScreen();
app_get_key(); /* blocking: dismiss on any key */
}
/* Five visible slots; line 0 holds the separator and line 6 the key hints. */
#define APP_MENU_ROWS 5u
#define APP_MENU_SLOT_COUNT 8u
_Static_assert(APP_MENU_SLOT_COUNT <= APP_SLOT_COUNT,
"APP_MENU_SLOT_COUNT exceeds the physical app slot count");
void APP_MenuOpen(void)
{
APP_ModalScreenSaverExit();
BACKLIGHT_TurnOn();
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
/* Detach the modal selector from the key state that triggered F+7. The
* normal K5Viewer updater suppresses frames while a key is held; without
* clearing this stale state, the selector could never publish its first
* frame to the viewer. */
gKeyReading0 = KEY_INVALID;
gKeyReading1 = KEY_INVALID;
#endif
/* Apps are installed from UV Studio (0x073x) into physical slots 0..N-1,
* shown here as 1..N. Keep empty slots in the list so their location is
* visible and selectable while scrolling. */
app_header_t hdr[APP_MENU_SLOT_COUNT];
bool installed[APP_MENU_SLOT_COUNT];
for (uint8_t slot = 0; slot < APP_MENU_SLOT_COUNT; slot++)
{
installed[slot] = APP_SlotInfo(slot, &hdr[slot]) == APP_OK &&
(hdr[slot].flags & APP_FLAG_COMMITTED);
}
/* Remember the physical slot and scrolling window across menu openings. */
static uint8_t sel = 0;
static uint8_t top = 0; /* first visible row of the scrolling window */
if (sel >= APP_MENU_SLOT_COUNT || top > APP_MENU_SLOT_COUNT - APP_MENU_ROWS)
sel = top = 0u;
app_wait_release();
for (;;)
{
UI_DisplayClear();
app_status_bar(); /* also wipes the VFO status line (DW, battery, ...) */
/* Slide [top, top+APP_MENU_ROWS) so it always contains the selection. */
if (sel < top)
top = sel;
else if (sel >= (uint8_t)(top + APP_MENU_ROWS))
top = (uint8_t)(sel - APP_MENU_ROWS + 1u);
for (uint8_t slot = top;
slot < APP_MENU_SLOT_COUNT && (uint8_t)(slot - top) < APP_MENU_ROWS;
slot++)
{
char number[2];
char name[14];
char size[6];
const uint8_t visible_number = (uint8_t)(slot + 1u);
const uint8_t fbLine = (uint8_t)(slot - top + 1u);
number[0] = (char)('0' + visible_number);
number[1] = '\0';
UI_PrintStringSmallNormal(number, 2u, 0, fbLine);
if (installed[slot])
{
app_copy(name, sizeof(name), hdr[slot].name, APP_NAME_LEN);
app_format_size(size, hdr[slot].code_size);
UI_PrintStringSmallNormal(name, APP_NAME_TEXT_X, 0, fbLine);
GUI_DisplaySmallest(size,
(uint8_t)(LCD_WIDTH - 2u - strlen(size) * 4u),
(uint8_t)(fbLine * 8u + 1u), false, true);
}
else
{
UI_PrintStringSmallNormal("Empty", APP_NAME_TEXT_X, 0, fbLine);
}
if (slot == sel)
app_invert_name(fbLine);
}
app_key_hints();
ST7565_BlitStatusLine();
ST7565_BlitFullScreen();
#ifdef ENABLE_FEAT_F4HWN_K5VIEWER
K5VIEWER_Update(false);
#endif
const KEY_Code_t key = app_get_key();
if (key == KEY_EXIT)
return;
switch (key)
{
case KEY_UP:
sel = (sel == 0u) ? (uint8_t)(APP_MENU_SLOT_COUNT - 1u) : (uint8_t)(sel - 1u);
break;
case KEY_DOWN:
sel = (uint8_t)((sel + 1u) % APP_MENU_SLOT_COUNT);
break;
case KEY_MENU:
{
if (!installed[sel])
break;
const uint8_t rc = APP_LaunchOverlay(sel); /* runs until the app exits */
if (rc != APP_OK)
app_show_error(&hdr[sel], rc); /* no longer silent */
app_wait_release();
break;
}
default:
break;
}
}
}
#endif /* ENABLE_FEAT_F4HWN_OVERLAY_APPS */
+9 -19
View File
@@ -1,6 +1,5 @@
/* Copyright 2023 OneOfEleven
* https://github.com/DualTachyon
/* Copyright 2026 Armel F4HWN
* https://github.com/armel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,21 +14,12 @@
* limitations under the License.
*/
#ifndef AM_FIXH
#ifndef APPS_APP_MENU_H
#define APPS_APP_MENU_H
#include <stdint.h>
#include <stdbool.h>
/* Blocking "Apps" selector: scans the overlay-app slots, lists the committed
* ones by name, and launches the chosen one. UP/DOWN move, MENU launches, EXIT
* closes. Returns when the user leaves. */
void APP_MenuOpen(void);
#ifdef ENABLE_AM_FIX
void AM_fix_init(void);
void AM_fix_reset(const unsigned vfo);
void AM_fix_10ms(const unsigned vfo);
#ifdef ENABLE_AM_FIX_SHOW_DATA
void AM_fix_print_data(const unsigned vfo, char *s);
#endif
int8_t AM_fix_get_gain_diff();
void AM_fix_enable(bool on);
#endif
#endif
#endif /* APPS_APP_MENU_H */
File diff suppressed because it is too large. Load diff
+135
View File
@@ -0,0 +1,135 @@
/* Copyright 2026 Armel F4HWN
* https://github.com/armel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Overlay-app loader (POC).
*
* A leaf, modal app is a self-contained blob held in the external SPI flash. To
* run it the loader validates the header + code CRC, copies the code into the
* 4 KiB overlay (the PY25Q16 sector cache, shared VMA with the multiboot RAM
* stub), and calls its entry point. The blob runs from RAM and reaches every
* resident service through app_api_t. The loader never touches the internal
* application flash, so a corrupt or incompatible blob is only refused (header/CRC
* mismatch) or crashes into a watchdog reset.
*
* This is NOT a security sandbox. The CRC is an integrity check, not authentication,
* and a launched app is TRUSTED NATIVE CODE: it runs privileged with full access to
* memory and peripherals (no MPU, no crypto auth), so a malicious or buggy app CAN
* brick the radio - e.g. by driving the internal-flash controller itself. Only
* install apps you trust.
*
* External-flash "Apps" region (carved from the free space in the PY25Q16 map,
* after the multiboot markers, before the RX/TX log):
*
* 0x102000 slot 0 ] 16 slots x 8 KiB = 128 KiB
* 0x104000 slot 1 ] each slot: 4 KiB header sector + 4 KiB code
* ... ]
* 0x120000 slot 15 ]
*
* Host tooling (APP_SlotErase/Write/Info) touches the external flash only and is
* never brick-critical; a bad slot is simply refused at launch by the CRC check.
*/
#ifndef APPS_APP_OVERLAY_H
#define APPS_APP_OVERLAY_H
#include <stdint.h>
#include <stdbool.h>
#include "app_api.h"
/* ---- external-flash Apps region ---- */
#define APP_REGION_BASE 0x00102000u /* first app slot */
#define APP_SLOT_STRIDE 0x00002000u /* 8 KiB per slot */
#define APP_CODE_OFFSET 0x00001000u /* code starts after the 4 KiB header sector */
#define APP_SECTOR_SIZE 0x00001000u /* external NOR erase granularity */
#define APP_SLOT_COUNT 16u
#define APP_SLOT_BASE(s) (APP_REGION_BASE + (uint32_t)(s) * APP_SLOT_STRIDE)
/* ---- overlay RAM budget: the PY25Q16 4 KiB sector cache reused as workspace.
* The link VMA itself is pinned in Core/py32f071xb.ld (ORIGIN+0x280) and passed
* via compile-app.sh; the loader checks each blob's link_vma against it. ---- */
#define APP_OVERLAY_MAX 0x00001000u /* 4 KiB */
/* ---- blob header (64 bytes, little-endian; see App/apps/pack_app.py) ---- */
#define APP_MAGIC 0x31504146u /* "FAP1" */
#define APP_HDR_VERSION 1u
#define APP_FLAG_COMMITTED 0x0001u
#define APP_FLAG_SCREEN_SAVER 0x0002u
#define APP_FLAG_SHORTCUT_SHIFT 8u
#define APP_FLAG_SHORTCUT_MASK 0x0F00u
#define APP_NAME_LEN 16
#define APP_VERSION_LEN 16
#define APP_SHORTCUT_FM 0x01u
#define APP_SHORTCUT_FOXHUNT 0x02u
#define APP_SHORTCUT_BEACON 0x04u
#define APP_SHORTCUT_BEAM 0x08u
/* Optional resident facilities an app may require. Requirements live in the
* previously reserved header bytes, so app_header_t remains 64 bytes. */
#define APP_CAP_FM 0x00000001u
#define APP_CAP_TRIVFO 0x00000002u
#define APP_CAP_BEAM 0x00000004u
typedef struct __attribute__((packed)) {
uint32_t magic; /* APP_MAGIC */
uint16_t hdr_version; /* APP_HDR_VERSION */
uint8_t abi_major; /* required ABI family */
uint8_t api_min; /* minimum append-only API level */
uint32_t code_size; /* bytes of code, <= APP_OVERLAY_MAX */
uint32_t code_crc32; /* CRC-32 (zlib) over code_size bytes */
uint16_t entry_off; /* entry offset within the code (0) */
uint16_t flags; /* APP_FLAG_COMMITTED, ... */
char name[APP_NAME_LEN]; /* human-readable, NUL-terminated */
char version[APP_VERSION_LEN];/* app version string */
uint32_t link_vma; /* RAM VMA the code was linked at */
uint32_t required_caps; /* APP_CAP_* required by this app */
uint8_t reserved[4]; /* pad to 64 bytes */
} app_header_t;
enum {
APP_OK = 0,
APP_ERR_SLOT, /* slot index out of range */
APP_ERR_MAGIC, /* no/invalid header */
APP_ERR_ABI, /* ABI family/API level mismatch */
APP_ERR_NOT_COMMITTED, /* image not marked complete */
APP_ERR_SIZE, /* code_size out of range */
APP_ERR_CRC, /* code CRC-32 mismatch */
APP_ERR_VMA, /* overlay buffer not at the link VMA */
APP_ERR_AUTH, /* host write refused: timestamp mismatch */
APP_ERR_CAP, /* required firmware capability missing */
};
/* Read + validate a slot header (no CRC of the code). */
uint8_t APP_ValidateSlot(uint8_t slot, app_header_t *out_header);
/* Validate, load into the overlay, verify the code CRC, and run the app.
* Returns when the app exits; the internal flash is never touched. */
uint8_t APP_LaunchOverlay(uint8_t slot);
/* Launch the first installed app advertising this shortcut bit. */
uint8_t APP_LaunchOverlayShortcut(uint8_t shortcut);
/* Cached availability of apps exposed as resident quick actions. */
uint8_t APP_OverlayShortcutMask(void);
/* Host-tool slot management (external flash only, never brick-critical). */
uint8_t APP_SlotInfo(uint8_t slot, app_header_t *out_header);
uint8_t APP_SlotErase(uint8_t slot);
uint8_t APP_SlotWrite(uint8_t slot, uint32_t offset, const uint8_t *data, uint32_t len);
#endif /* APPS_APP_OVERLAY_H */
+43
View File
@@ -0,0 +1,43 @@
/* Overlay-app link script (POC).
*
* The blob is linked to run at the PY25Q16 sector-cache overlay VMA (shared with
* the multiboot RAM stub — never both active at once). Code + rodata + data +
* bss must all fit in the 4 KiB overlay. objcopy -O binary emits text+rodata+
* data only; the loader zeroes the overlay before copying, so bss starts clean.
*
* VMA must match ADDR(.mb_workspace) in Core/py32f071xb.ld. If the firmware RAM
* layout shifts, update APP_VMA and the loader's compile-time assert catches a
* mismatch. */
/* APP_VMA is the target firmware's __mb_workspace_start (read from its .map). It
* varies with the RAM layout, so pass it from the build: -Wl,--defsym,APP_VMA=0x...
* The default is only a fallback for a standalone size check. */
APP_VMA = DEFINED(APP_VMA) ? APP_VMA : 0x20000280;
APP_LENGTH = 0x1000; /* 4 KiB sector-cache overlay */
ENTRY(app_main)
MEMORY {
APP (rwx) : ORIGIN = APP_VMA, LENGTH = APP_LENGTH
}
SECTIONS {
.app APP_VMA : {
KEEP(*(.text.entry)) /* app_main pinned to offset 0 */
*(.text .text.*)
*(.rodata .rodata.*)
. = ALIGN(4);
*(.data .data.*)
. = ALIGN(4);
__app_bss_start = .;
*(.bss .bss.* COMMON)
. = ALIGN(4);
__app_bss_end = .;
} > APP
__app_end = .;
ASSERT(__app_end <= APP_VMA + APP_LENGTH,
"Beacon overlay app overflows the 4 KiB overlay")
/DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.eh_frame*) *(.comment) *(.note.*) }
}
+333
View File
@@ -0,0 +1,333 @@
/* Copyright 2026 Armel F4HWN
* https://github.com/armel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Beacon (fox) — overlay app. Ported from App/app/foxhunt.c, beacon sub-mode only.
* Turns the radio into a hidden ARDF transmitter: keys up on the TX VFO and repeats
* a CW fox identifier (MOE..MO5 / MO / "<call> MOE") for the TX window, then stays
* silent for the idle gap. Two keying modes (key 4): TONE (default) keeps the carrier
* up and keys only the tone (MCW / F2A); CARR keys the PA together with the tone, so
* between elements the carrier itself is gone (carrier interruption, ARDF field pattern).
*
* Keys: 1 TX window · 2 idle gap · 3 fox id · 4 keying mode TONE/CARR (F reverses)
* MENU restart idle · long F keypad lock · EXIT quit.
*/
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include "../app_api.h"
#define LCD_WIDTH 128
#define TICK_MS 50
#define LOCK_HOLD_MS 500
#define MORSE_UNIT 100
#define TONE_HZ 1000
#define CALL_MAX 12
#define IDLE_DEF 30
#define IDLE_MIN 5
#define IDLE_MAX 240
#define IDLE_STEP 5
#define TX_DEF 30
#define TX_MIN 5
#define TX_MAX 60
#define TX_STEP 5
#define FOX_MO 5
#define FOX_CALL 6
#define FOX_COUNT 7
#define MSG_CALL 1
#define MSG_ID 3
#define MSG_ONE 2
#define CFG_MAGIC 0xF4
/* Sentinel-prefixed Morse (leading 1 sentinel, then elements MSB-first: 0 dit, 1 dah). */
static const uint8_t M_LET[26] = {0x05,0x18,0x1A,0x0C,0x02,0x12,0x0E,0x10,0x04,0x17,0x0D,0x14,0x07,0x06,0x0F,0x16,0x1D,0x0A,0x08,0x03,0x09,0x11,0x0B,0x19,0x1B,0x1C};
static const uint8_t M_DIG[10] = {0x3F,0x2F,0x27,0x23,0x21,0x20,0x30,0x38,0x3C,0x3E};
static const char FOX_TAIL[5] = {'E','I','S','H','5'};
static const uint8_t BMP_TX[16] = {0x1c,0x22,0x41,0x1c,0x22,0x00,0x08,0x1c,0x1c,0x08,0x00,0x22,0x1c,0x41,0x22,0x1c};
static const uint8_t FONT_LOCK[9] = {0x7c,0x46,0x45,0x45,0x45,0x45,0x45,0x46,0x7c};
static const uint8_t FONT_F[9] = {0x3e,0x7f,0x41,0x75,0x75,0x75,0x7d,0x7f,0x3e};
static const app_api_t *A;
static bool foxLocked, fArm, fLongDone, running, phaseTx, carrier;
static uint16_t fHoldMs;
static uint8_t beaconIdle, idleLeft, idleTick, beaconTx, secShown, charsSent, foxFox;
static uint16_t txMsLeft;
static char foxCall[CALL_MAX+1];
static char msg[17];
static uint8_t prevKey;
static char str[16];
/* ---- tiny formatting ---- */
static uint8_t slen(const char *s){ uint8_t n=0; while(s[n])n++; return n; }
static char *put(char *o,const char *s){ while(*s)*o++=*s++; return o; }
static char *putu(char *o,uint32_t v){ char t[6]; int8_t n=0; do{t[n++]=(char)('0'+v%10);v/=10;}while(v&&n<6); while(n--)*o++=t[n]; return o; }
static void put2(char **o,uint8_t v){ if(v<10)*(*o)++='0'; *o=putu(*o,v); }
static void cpy(uint8_t *d,const uint8_t *s,uint8_t n){ while(n--)*d++=*s++; }
static const char *findsp(const char *s){ while(*s){ if(*s==' ')return s; s++; } return NULL; }
static uint8_t mmin(uint8_t a,uint8_t b){ return a<b?a:b; }
/* ---- settings cycles ---- */
static uint8_t rangeStep(uint8_t v,uint8_t lo,uint8_t hi,uint8_t step,int8_t dir){
if(dir>0) return (v>=hi)?lo:(uint8_t)(v+step);
return (v<=lo)?hi:(uint8_t)(v-step);
}
static bool settingKey(uint8_t key,int8_t dir){
switch(key){
case APP_KEY_1: beaconTx =rangeStep(beaconTx, TX_MIN, TX_MAX, TX_STEP, dir); return true;
case APP_KEY_2: beaconIdle=rangeStep(beaconIdle,IDLE_MIN,IDLE_MAX,IDLE_STEP,dir); return true;
case APP_KEY_3:
if(dir>0){ if(++foxFox>=FOX_COUNT) foxFox=0; }
else foxFox=foxFox?(uint8_t)(foxFox-1u):(uint8_t)(FOX_COUNT-1u);
return true;
case APP_KEY_4: carrier=!carrier; return true; /* TONE <-> CARR (dir moot) */
default: return false;
}
}
/* ---- message ---- */
static void buildMsg(void){
char *o=msg;
if(foxFox==FOX_CALL){
if(foxCall[0]){ o=put(o,foxCall); o=put(o," MOE"); }
else o=put(o,"MOE");
} else if(foxFox==FOX_MO){ o=put(o,"MO"); }
else { o=put(o,"MO"); *o++=FOX_TAIL[foxFox]; }
*o='\0';
}
static void foxLabel(char *out){
char *o=put(out,"FOX ");
if(foxFox==FOX_CALL) o=put(o,"CALL");
else if(foxFox==FOX_MO) o=put(o,"MO");
else { o=put(o,"MO"); *o++=FOX_TAIL[foxFox]; }
*o='\0';
}
static uint8_t morseByte(char c){
if(c>='a'&&c<='z') c-=32;
if(c>='A'&&c<='Z') return M_LET[c-'A'];
if(c>='0'&&c<='9') return M_DIG[c-'0'];
if(c=='/') return 0x32;
return 0;
}
/* ---- drawing ---- */
static void tag(const char *s,uint8_t x,uint8_t line){ A->print_inverse(s,x,line,false,true,(uint8_t)(x+slen(s)*4)); }
static void chrome(void){
A->display_clear();
A->status_clear();
A->print_inverse("BEACON",2,0,true,true,26);
A->draw_battery();
if(foxLocked) cpy(A->status_line+70,FONT_LOCK,sizeof(FONT_LOCK));
else if(fArm) cpy(A->status_line+70,FONT_F,sizeof(FONT_F));
{ char *o=putu(str,A->tx_freq()/100000u); *o++='.'; uint32_t fr=A->tx_freq()%100000u;
for(int8_t d=4;d>=0;d--){ uint32_t p=1; for(int8_t k=0;k<d;k++)p*=10; *o++=(char)('0'+(fr/p)%10);} *o='\0'; }
A->print_normal(str,(uint8_t)(126-slen(str)*7),0,6);
}
static void drawTxSeconds(void){
uint8_t sec=(uint8_t)((txMsLeft+999u)/1000u);
for(uint8_t x=0;x<LCD_WIDTH;x++) A->fb[0][x]=0;
A->print_normal("TX",1,0,0);
char *o=str; put2(&o,sec); *o++='s'; *o='\0';
A->print_normal(str,(uint8_t)(127-slen(str)*7),0,0);
secShown=sec;
}
static void drawMessage(uint8_t vis){
const char *sp=findsp(msg);
if(!sp){
uint8_t idLen=slen(msg), v=mmin(vis,idLen);
char id[17]; cpy((uint8_t*)id,(const uint8_t*)msg,v); id[v]='\0';
A->print_string(id,(uint8_t)((LCD_WIDTH-idLen*8u)/2u),0,MSG_ONE,8);
return;
}
uint8_t callLen=(uint8_t)(sp-msg), vc=mmin(vis,callLen);
char call[CALL_MAX+1]; cpy((uint8_t*)call,(const uint8_t*)msg,vc); call[vc]='\0';
A->print_string(call,(uint8_t)((LCD_WIDTH-callLen*8u)/2u),0,MSG_CALL,8);
if(vis>callLen+1){
const char *id=sp+1; uint8_t idLen=slen(id), vi=mmin((uint8_t)(vis-callLen-1),idLen);
char idb[8]; cpy((uint8_t*)idb,(const uint8_t*)id,vi); idb[vi]='\0';
A->print_string(idb,(uint8_t)((LCD_WIDTH-idLen*8u)/2u),0,MSG_ID,8);
}
}
static void updateProgress(uint8_t vis){
const char *sp=findsp(msg); uint8_t top;
charsSent=vis;
if(!sp) top=MSG_ONE;
else { uint8_t callLen=(uint8_t)(sp-msg); if(vis==callLen+1) return; top=(vis<=callLen)?MSG_CALL:MSG_ID; }
for(uint8_t x=0;x<LCD_WIDTH;x++){ A->fb[top][x]=0; A->fb[top+1][x]=0; }
drawMessage(charsSent);
A->blit_line(top); A->blit_line(top+1);
}
static void beaconDraw(bool txNow,uint8_t il){
chrome();
if(txNow){
cpy(A->status_line+48,BMP_TX,16);
drawTxSeconds();
drawMessage(charsSent);
} else {
A->print_string("IDLE",0,127,1,10);
char *o=str; put2(&o,il); *o++='s'; *o='\0';
A->print_string(str,0,127,3,10);
}
char *o=put(str,"TX "); o=putu(o,beaconTx); o=put(o,"s"); *o='\0'; tag(str,4,5);
o=put(str,"IDLE "); o=putu(o,beaconIdle); o=put(o,"s"); *o='\0'; tag(str,4,6);
foxLabel(str); tag(str,66,5);
{ const char *m=carrier?"CARR":"TONE"; tag(m,(uint8_t)(125-slen(m)*4),5); }
}
static void blit(void){ A->blit_status(); A->blit_full(); }
/* ---- keypad-lock long-press ---- */
/* true exactly on the lock/unlock toggle, so a caller that does not redraw every tick
* (the TX loop) can refresh the padlock at once. */
static bool lockTrack(uint8_t key,uint16_t ms){
if(key!=APP_KEY_F){ fHoldMs=0; fLongDone=false; return false; }
if(fLongDone) return false;
fHoldMs+=ms;
if(fHoldMs>=LOCK_HOLD_MS){ fLongDone=true; foxLocked=!foxLocked; fArm=false; A->backlight_on(); return true; }
return false;
}
/* ---- interactive TX delay: drains the window, keys, live seconds. true = abort ---- */
static bool txDelay(uint16_t ms){
while(ms){
uint16_t slice=(ms>10)?10:ms;
A->delay_ms(slice); ms-=slice;
txMsLeft=(txMsLeft>slice)?(uint16_t)(txMsLeft-slice):0;
A->backlight_update();
if((uint8_t)((txMsLeft+999u)/1000u)!=secShown){ drawTxSeconds(); A->blit_line(0); }
uint8_t key=A->get_key();
if(lockTrack(key,slice)){ beaconDraw(true,0); blit(); } /* show the padlock at once, mid-TX */
if(key==APP_KEY_INVALID||key==prevKey){ prevKey=key; continue; }
prevKey=key;
A->backlight_on();
if(foxLocked) continue;
if(key==APP_KEY_EXIT){ running=false; return true; }
if(key==APP_KEY_MENU) return true; /* cut short, restart idle */
if(key==APP_KEY_F){ fArm=!fArm; beaconDraw(true,0); blit(); continue; }
if(settingKey(key,fArm?-1:1)){ fArm=false; beaconDraw(true,0); blit(); }
}
return false;
}
/* Keying: both modes key the tone; CARR gates the PA in lockstep so the carrier is truly
* gone between elements (tone muted too, else it bleeds through residual PA leakage). */
static void keyOn(void){ A->tx_mute(false); A->tx_carrier(true); } /* carrier up both modes; re-arms after a mid-window switch */
static void keyOff(void){ A->tx_mute(true); if(carrier) A->tx_carrier(false); }
static bool morseChar(char c,uint8_t vis){
uint8_t code=morseByte(c);
if(code==0){ if(txDelay(MORSE_UNIT*4)) return true; updateProgress(vis); return false; }
uint8_t bit=0x80; while(!(code&bit))bit>>=1;
for(bit>>=1;bit;bit>>=1){
uint16_t on=(code&bit)?(MORSE_UNIT*3):MORSE_UNIT;
keyOn();
if(txDelay(on)){ keyOff(); return true; }
keyOff();
if(txDelay(MORSE_UNIT)) return true;
}
updateProgress(vis);
return txDelay(MORSE_UNIT*2);
}
static void transmit(void){
A->tx_set_params();
A->tx_tone(TONE_HZ);
A->tx_mute(true); /* open muted (both modes) */
if(carrier) A->tx_carrier(false); /* CARR: carrier off until first element */
txMsLeft=(uint16_t)beaconTx*1000u;
bool stop=false;
while(!stop && txMsLeft>0){
buildMsg();
charsSent=0;
beaconDraw(true,0); blit();
for(uint8_t i=0;!stop && msg[i];i++) stop=morseChar(msg[i],(uint8_t)(i+1));
if(!stop && txMsLeft>0) stop=txDelay(MORSE_UNIT*7);
}
A->tx_mute(true);
A->tx_end(); /* PA off + restore RX */
}
/* ---- idle-phase keys ---- */
static void idleKeys(void){
uint8_t key=A->get_key();
lockTrack(key,TICK_MS);
if(key==APP_KEY_INVALID||key==prevKey){ prevKey=key; return; }
prevKey=key;
A->backlight_on();
if(foxLocked) return;
if(key==APP_KEY_F){ fArm=!fArm; return; }
switch(key){
case APP_KEY_EXIT: running=false; break;
case APP_KEY_MENU: idleLeft=beaconIdle; idleTick=0; break;
default: settingKey(key,fArm?-1:1); break;
}
fArm=false;
}
static void tickDelay(void){ for(uint8_t i=0;i<TICK_MS/10;i++){ A->delay_ms(10); A->backlight_update(); } }
/* ---- config (deferred) ---- */
static void loadConfig(void){
uint8_t c[5]; A->cfg_load(c,5);
if(c[0]==CFG_MAGIC){
if(c[1]>=IDLE_MIN&&c[1]<=IDLE_MAX&&(c[1]%IDLE_STEP)==0) beaconIdle=c[1];
if(c[2]<FOX_COUNT) foxFox=c[2];
if(c[3]>=TX_MIN&&c[3]<=TX_MAX&&(c[3]%TX_STEP)==0) beaconTx=c[3];
if(c[4]<=1) carrier=c[4]; /* erased (0xFF) legacy config -> keep default */
}
}
static void saveConfig(void){ uint8_t c[5]={CFG_MAGIC,beaconIdle,foxFox,beaconTx,(uint8_t)carrier}; A->cfg_save(c,5); }
static void txDenied(void){
chrome();
A->print_string("TX OFF",0,127,1,8);
blit();
for(uint8_t i=0;i<20;i++) tickDelay();
}
__attribute__((section(".text.entry"),used))
void app_main(const app_api_t *api){
A=api;
foxLocked=fArm=fLongDone=carrier=false; fHoldMs=0;
beaconIdle=IDLE_DEF; beaconTx=TX_DEF; foxFox=FOX_CALL;
prevKey=APP_KEY_INVALID;
A->boot_callsign(foxCall,sizeof(foxCall));
loadConfig();
A->backlight_on();
phaseTx=true; idleLeft=beaconIdle; idleTick=0;
running=true;
while(running){
if(phaseTx){
if(A->tx_state()!=0){ txDenied(); phaseTx=false; idleLeft=beaconIdle; idleTick=0; continue; }
transmit();
phaseTx=false; idleLeft=beaconIdle; idleTick=0;
continue;
}
idleKeys();
if(!running) break;
beaconDraw(false,idleLeft); blit();
if(++idleTick>=(1000/TICK_MS)){ idleTick=0; if(idleLeft>0)idleLeft--; if(idleLeft==0)phaseTx=true; }
A->battery_sample();
tickDelay();
}
saveConfig();
A->tx_end(); /* safety: PA off + RX restored */
A->audio_path(false);
}
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# Build one overlay-app blob (.app). Invoked by ../../../compile-app.sh inside
# the uvk1-uvk5v3 Docker image, run from this app's directory. Everything is
# derived from the directory name; only APP_NAME - the human label baked into
# the 64-byte blob header - is per-app. The .app file is named after it (spaces
# stripped), so "Broadcast FM" -> BroadcastFM.app.
set -euo pipefail
APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ...
APP_NAME="Beacon" # <-- the only per-app line
APP_VER="1.0"
APP_API_MIN=1
APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld)
OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM)
CC=/opt/toolchain/bin/arm-none-eabi-gcc
OBJCOPY=/opt/toolchain/bin/arm-none-eabi-objcopy
command -v arm-none-eabi-gcc >/dev/null 2>&1 && { CC=arm-none-eabi-gcc; OBJCOPY=arm-none-eabi-objcopy; }
CFLAGS="-mcpu=cortex-m0plus -mthumb -Os -std=gnu11 -ffreestanding -fno-builtin -fno-common \
-fomit-frame-pointer -ffunction-sections -fdata-sections -Wall -Wextra"
LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \
-Wl,--gc-sections -Wl,-Map=${APP}.map -Wl,--build-id=none -Wl,--no-warn-rwx-segments"
rm -f ./*.app ./*.elf ./*.bin # drop stale artifacts so discovery is unambiguous
step() { printf '\r 🔨 %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; }
trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
--name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" \
--shortcut beacon >/dev/null
trap - ERR
BYTES=$(wc -c < "${APP}.bin")
if [ "$BYTES" -gt 4096 ]; then
printf '\r 🚨 %-13s OVERFLOWS 4 KiB (%d B) \n' "$APP_NAME" "$BYTES"; exit 1
fi
printf '\r ✅ %-13s %4d B (%d%% of 4 KiB) -> %s.app \n' \
"$APP_NAME" "$BYTES" "$(( BYTES * 100 / 4096 ))" "$OUT"
+30
View File
@@ -0,0 +1,30 @@
/* Overlay-app link script. */
APP_VMA = DEFINED(APP_VMA) ? APP_VMA : 0x20000280;
APP_LENGTH = 0x1000;
ENTRY(app_main)
MEMORY {
APP (rwx) : ORIGIN = APP_VMA, LENGTH = APP_LENGTH
}
SECTIONS {
.app APP_VMA : {
KEEP(*(.text.entry))
*(.text .text.*)
*(.rodata .rodata.*)
. = ALIGN(4);
*(.data .data.*)
. = ALIGN(4);
__app_bss_start = .;
*(.bss .bss.* COMMON)
. = ALIGN(4);
__app_bss_end = .;
} > APP
__app_end = .;
ASSERT(__app_end <= APP_VMA + APP_LENGTH,
"BEAM overlay app overflows the 4 KiB overlay")
/DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.eh_frame*) *(.comment) *(.note.*) }
}
+244
View File
@@ -0,0 +1,244 @@
/* Copyright 2026 Armel F4HWN
* Licensed under the Apache License, Version 2.0.
*
* BEAM overlay app. Packet version 2 remains wire-compatible with the
* resident implementation: one selected VFO is sent and a received VFO is
* stored in the first free memory channel.
*/
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include "../app_api.h"
#define PACKET_MAGIC 0xBEA5u
#define PACKET_VERSION 2u
#define PACKET_START 0xABCDu
#define PACKET_END 0xDCBAu
#define PACKET_WORDS 36u
enum {
STATUS_READY = 0,
STATUS_TX_WAIT,
STATUS_TX_DONE,
STATUS_RX_WAIT,
STATUS_RX_SAVED,
STATUS_RX_FULL,
STATUS_ERROR,
};
typedef struct {
uint16_t magic;
uint8_t version;
uint8_t pad;
app_beam_channel_t channel;
} beam_payload_t;
_Static_assert(sizeof(beam_payload_t) == 48u,
"BEAM v2 wire payload layout changed");
static const uint16_t OBFUSCATION[8] = {
0x6C16, 0xE614, 0x912E, 0x400D, 0x3521, 0x40D5, 0x0313, 0x80E9
};
static const app_api_t *A;
static uint32_t packet_store[18]; /* 72 bytes, 4-byte aligned for the payload */
static uint16_t copiedChannel;
static uint8_t mode, status;
static bool receiving, running, dirty;
static uint16_t *packet(void) { return (uint16_t *)packet_store; }
static beam_payload_t *payload(void) { return (beam_payload_t *)&packet()[2]; }
static void clear_bytes(void *ptr, uint8_t count)
{
uint8_t *p = (uint8_t *)ptr;
while (count--) *p++ = 0;
}
static uint16_t crc16(const void *buffer, uint16_t size)
{
const uint8_t *data = (const uint8_t *)buffer;
uint16_t crc = 0;
while (size--) {
crc ^= (uint16_t)*data++ << 8;
for (uint8_t bit = 0; bit < 8u; bit++)
crc = (crc & 0x8000u) ? (uint16_t)((crc << 1) ^ 0x1021u)
: (uint16_t)(crc << 1);
}
return crc;
}
static void obfuscate(void)
{
uint16_t *p = packet();
for (uint8_t i = 0; i < 32u; i++)
p[i + 1u] ^= OBFUSCATION[i & 7u];
}
static void draw(void)
{
const char *state;
switch (status) {
case STATUS_TX_WAIT: state = "SENDING"; break;
case STATUS_TX_DONE: state = "SENT"; break;
case STATUS_RX_WAIT: state = "WAITING"; break;
case STATUS_RX_SAVED: state = "RECEIVED"; break;
case STATUS_RX_FULL: state = "MEM FULL"; break;
case STATUS_ERROR: state = "ERROR"; break;
default: state = mode ? "BEAM RX" : "BEAM TX"; break;
}
A->beam_draw(state);
A->blit_status();
A->blit_full();
}
static void stop_rx(void)
{
if (receiving) {
A->beam_rx(false);
receiving = false;
}
}
static void send_packet(void)
{
uint16_t *p = packet();
clear_bytes(packet_store, sizeof(packet_store));
p[0] = PACKET_START;
payload()->magic = PACKET_MAGIC;
payload()->version = PACKET_VERSION;
A->beam_get(&payload()->channel);
p[34] = crc16(&p[1], 2u + 64u);
p[35] = PACKET_END;
obfuscate();
status = STATUS_TX_WAIT;
draw();
A->beam_send(p);
status = STATUS_TX_DONE;
for (uint8_t i = 0; i < 3u; i++) {
A->play_tone(880, 60);
if (i < 2u) A->delay_ms(20);
}
dirty = true;
}
static void start(void)
{
stop_rx();
/* A channel save is committed only after app_main() returns. Do not let a
second RX overwrite that pending save; exit and relaunch to receive more. */
if (mode && copiedChannel != 0xFFFFu)
return;
A->beam_prepare();
if (!mode) {
send_packet();
return;
}
A->beam_rx(true);
receiving = true;
status = STATUS_RX_WAIT;
dirty = true;
}
static bool valid_packet(void)
{
uint16_t *p = packet();
if (p[0] != PACKET_START || p[35] != PACKET_END)
return false;
obfuscate();
if (p[34] != crc16(&p[1], 2u + 64u))
return false;
return payload()->magic == PACKET_MAGIC && payload()->version == PACKET_VERSION;
}
static void poll_rx(void)
{
if (!receiving)
return;
const uint8_t result = A->beam_rx_poll(packet());
if (result == APP_BEAM_RX_WAIT)
return;
if (result == APP_BEAM_RX_ERROR || !valid_packet()) {
status = STATUS_ERROR;
A->backlight_on();
dirty = true;
return;
}
copiedChannel = A->beam_save(&payload()->channel);
A->beam_rx(false);
receiving = false;
status = copiedChannel == 0xFFFFu ? STATUS_RX_FULL : STATUS_RX_SAVED;
A->backlight_on();
dirty = true;
}
static void key_press(uint8_t key)
{
switch (key) {
case APP_KEY_UP:
case APP_KEY_DOWN:
stop_rx();
mode ^= 1u;
status = STATUS_READY;
dirty = true;
break;
case APP_KEY_MENU:
start();
break;
case APP_KEY_EXIT:
running = false;
break;
case APP_KEY_PTT:
break;
default:
A->play_tone(500, 60);
break;
}
}
__attribute__((section(".text.entry"), used))
void app_main(const app_api_t *api)
{
A = api;
mode = 0;
status = STATUS_READY;
copiedChannel = 0xFFFFu;
receiving = false;
running = true;
dirty = true;
A->backlight_on();
uint8_t previous = APP_KEY_INVALID;
uint8_t batteryTicks = 0;
while (running) {
const uint8_t key = A->get_key();
if (key == APP_KEY_SAVER) {
previous = APP_KEY_INVALID;
} else if (key == APP_KEY_WAKE) {
previous = APP_KEY_INVALID;
dirty = true;
} else {
if (key != previous && key != APP_KEY_INVALID) {
A->backlight_on();
key_press(key);
}
previous = key;
}
poll_rx();
if (dirty) { draw(); dirty = false; }
A->delay_ms(10);
A->backlight_update();
if (++batteryTicks >= 50u) {
batteryTicks = 0;
A->battery_sample();
dirty = true;
}
}
stop_rx();
A->beam_leave();
}
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
set -euo pipefail
APP="$(basename "$PWD")"
APP_NAME="Beam"
APP_VER="1.0"
APP_API_MIN=1
APP_VMA=${APP_VMA:-0x20000280}
OUT="${APP_NAME// /}"
CC=/opt/toolchain/bin/arm-none-eabi-gcc
OBJCOPY=/opt/toolchain/bin/arm-none-eabi-objcopy
command -v arm-none-eabi-gcc >/dev/null 2>&1 && { CC=arm-none-eabi-gcc; OBJCOPY=arm-none-eabi-objcopy; }
CFLAGS="-mcpu=cortex-m0plus -mthumb -Os -std=gnu11 -ffreestanding -fno-builtin -fno-common \
-fomit-frame-pointer -ffunction-sections -fdata-sections -Wall -Wextra"
LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \
-Wl,--gc-sections -Wl,-Map=${APP}.map -Wl,--build-id=none -Wl,--no-warn-rwx-segments"
rm -f ./*.app ./*.elf ./*.bin
step() { printf '\r 🔨 %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; }
trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
--name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" \
--shortcut beam --require beam >/dev/null
trap - ERR
BYTES=$(wc -c < "${APP}.bin")
if [ "$BYTES" -gt 4096 ]; then
printf '\r 🚨 %-13s OVERFLOWS 4 KiB (%d B) \n' "$APP_NAME" "$BYTES"; exit 1
fi
printf '\r ✅ %-13s %4d B (%d%% of 4 KiB) -> %s.app \n' \
"$APP_NAME" "$BYTES" "$(( BYTES * 100 / 4096 ))" "$OUT"
+43
View File
@@ -0,0 +1,43 @@
/* Overlay-app link script (POC).
*
* The blob is linked to run at the PY25Q16 sector-cache overlay VMA (shared with
* the multiboot RAM stub — never both active at once). Code + rodata + data +
* bss must all fit in the 4 KiB overlay. objcopy -O binary emits text+rodata+
* data only; the loader zeroes the overlay before copying, so bss starts clean.
*
* VMA must match ADDR(.mb_workspace) in Core/py32f071xb.ld. If the firmware RAM
* layout shifts, update APP_VMA and the loader's compile-time assert catches a
* mismatch. */
/* APP_VMA is the target firmware's __mb_workspace_start (read from its .map). It
* varies with the RAM layout, so pass it from the build: -Wl,--defsym,APP_VMA=0x...
* The default is only a fallback for a standalone size check. */
APP_VMA = DEFINED(APP_VMA) ? APP_VMA : 0x20000280;
APP_LENGTH = 0x1000; /* 4 KiB sector-cache overlay */
ENTRY(app_main)
MEMORY {
APP (rwx) : ORIGIN = APP_VMA, LENGTH = APP_LENGTH
}
SECTIONS {
.app APP_VMA : {
KEEP(*(.text.entry)) /* app_main pinned to offset 0 */
*(.text .text.*)
*(.rodata .rodata.*)
. = ALIGN(4);
*(.data .data.*)
. = ALIGN(4);
__app_bss_start = .;
*(.bss .bss.* COMMON)
. = ALIGN(4);
__app_bss_end = .;
} > APP
__app_end = .;
ASSERT(__app_end <= APP_VMA + APP_LENGTH,
"Breakout overlay app overflows the 4 KiB overlay")
/DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.eh_frame*) *(.comment) *(.note.*) }
}
+269
View File
@@ -0,0 +1,269 @@
/* Copyright 2025 Armel F4HWN
* https://github.com/armel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Breakout — overlay-app port (POC).
*
* Self-contained: no firmware headers, no libc. Every resident service is
* reached through the app_api_t table; the entry point app_main() is forced to
* blob offset 0 via section ".text.entry". The loader zeroes the 4 KiB overlay,
* copies this blob in, and calls app_main(&api).
*/
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include "../app_api.h"
#define LCD_WIDTH 128
#define BRICK_NUMBER 18
#define BALL_NUMBER 5
#define BRICK_WIDTH 14
#define BRICK_HEIGHT 5
#define BALL_WIDTH 3
#define BALL_HEIGHT 3
#define RACKET_WIDTH 24
#define RACKET_HEIGHT 2
#define RACKET_Y 50
typedef struct { uint8_t x; uint8_t y; bool destroy; } Brick;
typedef struct { int8_t x; uint8_t p; } Racket;
typedef struct { int16_t x; int8_t y; int8_t dx; int8_t dy; } Ball;
/* ---- tiny freestanding helpers (replace libc / libgcc-free where cheap) ---- */
void *memset(void *d, int c, size_t n) {
uint8_t *p = d; while (n--) *p++ = (uint8_t)c; return d;
}
void *memcpy(void *d, const void *s, size_t n) {
uint8_t *p = d; const uint8_t *q = s; while (n--) *p++ = *q++; return d;
}
static int iabs(int v) { return v < 0 ? -v : v; }
static int imin(int a, int b) { return a < b ? a : b; }
/* zero-padded unsigned -> string, width w (<= 5). Replaces sprintf. */
static void u2str(char *out, const char *label, uint16_t v, uint8_t w) {
char *o = out;
while (*label) *o++ = *label++;
char tmp[6]; int8_t n = 0;
do { tmp[n++] = (char)('0' + v % 10); v /= 10; } while (v && n < 6);
while (n < w) tmp[n++] = '0';
while (n--) *o++ = tmp[n];
*o = '\0';
}
/* ---- app-wide handle to the resident ABI, set once at entry ---- */
static const app_api_t *A;
/* ---- game state (lives in the overlay; re-initialised at entry) ---- */
static uint32_t randSeed;
static uint8_t blockAnim;
static bool isInitialized;
static bool isPaused;
static uint8_t levelCountBreakout;
static uint16_t tone;
static uint16_t score;
static int16_t ballCount;
static char str[12];
static uint8_t kbdPrev, kbdCur;
static Brick brick[BRICK_NUMBER];
static Racket racket;
static Ball ball;
static const uint8_t BRICK_ANIM_PATTERNS[4] =
{0b00110001, 0b00101001, 0b00100101, 0b00100011};
static void srand_custom(uint32_t seed) { randSeed = seed ? seed : 1; }
static int rand_custom(void) { randSeed = randSeed * 1103515245u + 12345u; return (randSeed >> 16) & 0x7FFF; }
static int randInt(int min, int max) { return min + (rand_custom() % (max - min + 1)); }
static void reset(void) { ballCount = BALL_NUMBER; levelCountBreakout = 1; score = 0; }
static void playBeep(uint16_t t) { A->play_tone(t, 100); }
static void drawScore(void) {
A->status_clear();
u2str(str, "Level ", levelCountBreakout, 2); A->print_tiny(str, 0, 1, true, true);
u2str(str, "Ball ", (ballCount < 0) ? 0 : ballCount, 2); A->print_tiny(str, 45, 1, true, true);
u2str(str, "Score ", score, 4); A->print_tiny(str, 88, 1, true, true);
}
static void renderBall(bool state) {
A->draw_rect(A->fb, ball.x, ball.y, ball.x + BALL_WIDTH - 1, ball.y + BALL_HEIGHT - 1, state);
A->draw_line(A->fb, ball.x - 1, ball.y + 1, ball.x + BALL_WIDTH, ball.y + 1, state);
}
static void initBall(void) { ball.x = 62; ball.y = 30; ball.dx = 0; ball.dy = 1; renderBall(true); }
static void directionBall(int16_t x, uint8_t w, int8_t num) {
ball.dx = (int16_t)(x + w - ball.x) * (-num - num) / w + num;
ball.dy *= -1;
}
static void initWall(void) {
Brick *current = brick;
for (uint8_t y = 0; y < 24; y += 8)
for (uint8_t x = 6; x < 126; x += 20) {
current->x = x; current->y = y; current->destroy = false; current++;
}
}
static void initRacket(void);
static void drawBall(void) {
renderBall(false);
ball.x += ball.dx; ball.y += ball.dy;
if (ball.y <= 0) { ball.dx = randInt(-3, 3); ball.dy = 1; }
else if (ball.x <= 2) { ball.dx = iabs(ball.dx); }
else if (ball.x >= 124) { ball.dx = -iabs(ball.dx); }
if (ball.y == 47) {
if (ball.x + 1 >= racket.x && ball.x - 1 <= racket.x + RACKET_WIDTH) {
directionBall(racket.x, RACKET_WIDTH, 3); tone = 400;
}
} else if (ball.y > 49) {
ballCount--;
A->display_clear();
drawScore();
tone = 800;
if (ballCount < 0) {
reset(); initWall(); /* drawWall drawn by main loop */
isPaused = true;
A->print_bold("GAME OVER", 0, LCD_WIDTH - 1, 4);
}
initRacket(); initBall();
}
renderBall(true);
}
static void drawWall(void) {
for (uint8_t i = 0; i < BRICK_NUMBER; i++) {
if (brick[i].destroy) continue;
uint8_t *fb_ptr = A->fb[brick[i].y / 8] + brick[i].x;
fb_ptr[0] = 0b00011110;
fb_ptr[14] = 0b00011110;
if ((ball.x + 1 >= brick[i].x && ball.x - 1 <= brick[i].x + BRICK_WIDTH) &&
(ball.y + 1 >= brick[i].y && ball.y - 1 <= brick[i].y + BRICK_HEIGHT)) {
brick[i].destroy = true; score++;
directionBall(brick[i].x, BRICK_WIDTH, 2);
A->led(true);
memset(fb_ptr + 1, 0b00111111, 13);
A->blit_line(brick[i].y / 8);
playBeep(600);
memset(fb_ptr + 0, 0b00000000, 15);
A->blit_line(brick[i].y / 8);
A->led(false);
if (score % BRICK_NUMBER == 0) {
levelCountBreakout++; ballCount++; initWall(); return;
}
} else {
for (uint8_t k = 0; k < 13; k++)
fb_ptr[k + 1] = BRICK_ANIM_PATTERNS[(blockAnim + k) % 4];
}
}
}
static void renderRacket(int x, bool state) {
A->draw_rect(A->fb, x + 1, RACKET_Y, x + RACKET_WIDTH - 2, RACKET_Y + RACKET_HEIGHT, state);
A->draw_line(A->fb, x, RACKET_Y + 1, x + RACKET_WIDTH - 1, RACKET_Y + 1, state);
}
static void initRacket(void) { racket.x = 64 - (RACKET_WIDTH / 2); racket.p = racket.x; renderRacket(racket.x, true); }
static void drawRacket(void) {
if (racket.p != racket.x) {
renderRacket(racket.p, false);
racket.p = racket.x;
renderRacket(racket.x, true);
}
}
static void OnKeyDown(uint8_t key) {
bool wasPaused = isPaused;
switch (key) {
case APP_KEY_4:
case APP_KEY_UP: if (!isPaused && racket.x > 0) racket.x -= 2; isPaused = false; break;
case APP_KEY_0:
case APP_KEY_DOWN: if (!isPaused && racket.x < 102) racket.x += 2; isPaused = false; break;
case APP_KEY_MENU:
isPaused = !isPaused;
if (isPaused) A->print_bold("PAUSE", 0, LCD_WIDTH - 1, 4);
break;
case APP_KEY_EXIT: isPaused = false; isInitialized = false; break;
}
if (wasPaused && !isPaused)
for (uint8_t i = 0; i < 8; i++)
A->draw_line(A->fb, 32, 32 + i, 96, 32 + i, false);
}
static void handleInput(void) {
kbdPrev = kbdCur;
kbdCur = A->get_key();
if (kbdCur == APP_KEY_INVALID) return;
if (kbdCur == APP_KEY_UP || kbdCur == APP_KEY_DOWN ||
kbdCur == APP_KEY_4 || kbdCur == APP_KEY_0 || kbdCur != kbdPrev)
OnKeyDown(kbdCur);
}
/* ---- entry point, pinned to blob offset 0 ---- */
__attribute__((section(".text.entry"), used))
void app_main(const app_api_t *api) {
A = api;
#ifdef APP_POC_HELLO
/* Minimal bisection test: prove jump + ABI + return work, no game body. */
A->display_clear();
A->print_bold("OVL HELLO", 0, LCD_WIDTH - 1, 3);
A->blit_full();
A->delay_ms(2000);
return;
#endif
/* reset all state (the overlay is not persistent across launches) */
blockAnim = 0; isPaused = false; tone = 0; score = 0;
kbdPrev = APP_KEY_INVALID; kbdCur = APP_KEY_INVALID;
uint8_t swap = 0;
/* The low BK4819 counter bits vary continuously; mix them with the tuned
* frequency instead of carrying a launch-only seed field in every ABI
* table. */
srand_custom(((uint32_t)A->bk_read(0x67u) << 16) ^ A->rx_freq());
A->led(false);
A->backlight_on();
A->display_clear();
reset(); initWall(); initRacket(); initBall();
A->status_clear();
isInitialized = true;
while (isInitialized) {
handleInput();
if (!isPaused) {
if (swap == 0) blockAnim = (blockAnim + 1) % 4;
swap = (swap + 1) % 4;
drawScore(); drawWall(); drawRacket(); drawBall();
if (tone != 0) { playBeep(tone); tone = 0; }
else A->delay_ms(40 - imin(levelCountBreakout - 1, 20));
}
A->blit_status();
A->blit_full();
}
}
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# Build one overlay-app blob (.app). Invoked by ../../../compile-app.sh inside
# the uvk1-uvk5v3 Docker image, run from this app's directory. Everything is
# derived from the directory name; only APP_NAME - the human label baked into
# the 64-byte blob header - is per-app. The .app file is named after it (spaces
# stripped), so "Broadcast FM" -> BroadcastFM.app.
set -euo pipefail
APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ...
APP_NAME="Breakout" # <-- the only per-app line
APP_VER="1.0"
APP_API_MIN=1
APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld)
OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM)
CC=/opt/toolchain/bin/arm-none-eabi-gcc
OBJCOPY=/opt/toolchain/bin/arm-none-eabi-objcopy
command -v arm-none-eabi-gcc >/dev/null 2>&1 && { CC=arm-none-eabi-gcc; OBJCOPY=arm-none-eabi-objcopy; }
CFLAGS="-mcpu=cortex-m0plus -mthumb -Os -std=gnu11 -ffreestanding -fno-builtin -fno-common \
-fomit-frame-pointer -ffunction-sections -fdata-sections -Wall -Wextra"
LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \
-Wl,--gc-sections -Wl,-Map=${APP}.map -Wl,--build-id=none -Wl,--no-warn-rwx-segments"
rm -f ./*.app ./*.elf ./*.bin # drop stale artifacts so discovery is unambiguous
step() { printf '\r 🔨 %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; }
trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
--name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" >/dev/null
trap - ERR
BYTES=$(wc -c < "${APP}.bin")
if [ "$BYTES" -gt 4096 ]; then
printf '\r 🚨 %-13s OVERFLOWS 4 KiB (%d B) \n' "$APP_NAME" "$BYTES"; exit 1
fi
printf '\r ✅ %-13s %4d B (%d%% of 4 KiB) -> %s.app \n' \
"$APP_NAME" "$BYTES" "$(( BYTES * 100 / 4096 ))" "$OUT"
+43
View File
@@ -0,0 +1,43 @@
/* Overlay-app link script (POC).
*
* The blob is linked to run at the PY25Q16 sector-cache overlay VMA (shared with
* the multiboot RAM stub — never both active at once). Code + rodata + data +
* bss must all fit in the 4 KiB overlay. objcopy -O binary emits text+rodata+
* data only; the loader zeroes the overlay before copying, so bss starts clean.
*
* VMA must match ADDR(.mb_workspace) in Core/py32f071xb.ld. If the firmware RAM
* layout shifts, update APP_VMA and the loader's compile-time assert catches a
* mismatch. */
/* APP_VMA is the target firmware's __mb_workspace_start (read from its .map). It
* varies with the RAM layout, so pass it from the build: -Wl,--defsym,APP_VMA=0x...
* The default is only a fallback for a standalone size check. */
APP_VMA = DEFINED(APP_VMA) ? APP_VMA : 0x20000280;
APP_LENGTH = 0x1000; /* 4 KiB sector-cache overlay */
ENTRY(app_main)
MEMORY {
APP (rwx) : ORIGIN = APP_VMA, LENGTH = APP_LENGTH
}
SECTIONS {
.app APP_VMA : {
KEEP(*(.text.entry)) /* app_main pinned to offset 0 */
*(.text .text.*)
*(.rodata .rodata.*)
. = ALIGN(4);
*(.data .data.*)
. = ALIGN(4);
__app_bss_start = .;
*(.bss .bss.* COMMON)
. = ALIGN(4);
__app_bss_end = .;
} > APP
__app_end = .;
ASSERT(__app_end <= APP_VMA + APP_LENGTH,
"Cube3D overlay app overflows the 4 KiB overlay")
/DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.eh_frame*) *(.comment) *(.note.*) }
}
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# Build one overlay-app blob (.app). Invoked by ../../../compile-app.sh inside
# the uvk1-uvk5v3 Docker image, run from this app's directory. Everything is
# derived from the directory name; only APP_NAME - the human label baked into
# the 64-byte blob header - is per-app. The .app file is named after it (spaces
# stripped), so "Broadcast FM" -> BroadcastFM.app.
set -euo pipefail
APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ...
APP_NAME="Cube3D" # <-- the only per-app line
APP_VER="1.0"
APP_API_MIN=1
APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld)
OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM)
CC=/opt/toolchain/bin/arm-none-eabi-gcc
OBJCOPY=/opt/toolchain/bin/arm-none-eabi-objcopy
command -v arm-none-eabi-gcc >/dev/null 2>&1 && { CC=arm-none-eabi-gcc; OBJCOPY=arm-none-eabi-objcopy; }
CFLAGS="-mcpu=cortex-m0plus -mthumb -Os -std=gnu11 -ffreestanding -fno-builtin -fno-common \
-fomit-frame-pointer -ffunction-sections -fdata-sections -Wall -Wextra"
LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \
-Wl,--gc-sections -Wl,-Map=${APP}.map -Wl,--build-id=none -Wl,--no-warn-rwx-segments"
rm -f ./*.app ./*.elf ./*.bin # drop stale artifacts so discovery is unambiguous
step() { printf '\r 🔨 %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; }
trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
--name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" --screensaver >/dev/null
trap - ERR
BYTES=$(wc -c < "${APP}.bin")
if [ "$BYTES" -gt 4096 ]; then
printf '\r 🚨 %-13s OVERFLOWS 4 KiB (%d B) \n' "$APP_NAME" "$BYTES"; exit 1
fi
printf '\r ✅ %-13s %4d B (%d%% of 4 KiB) -> %s.app \n' \
"$APP_NAME" "$BYTES" "$(( BYTES * 100 / 4096 ))" "$OUT"
+299
View File
@@ -0,0 +1,299 @@
/* Copyright 2026 Armel F4HWN
* https://github.com/armel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Cube3D — overlay app. A real-time rotating solid on the 1-bit 128x64 LCD.
* Vertices are spun by three axis rotations in Q14 fixed point (Cortex-M0+ has
* no FPU and no hardware divide), then perspective-projected with a single
* divide per vertex. Two looks, toggled with F:
* - SOLID: hidden-line removal by back-face culling (a face is drawn only when
* the signed area of its projected polygon shows it facing us).
* - WIRE : every edge, with the far hemisphere dotted for a depth cue.
* Edges use a self-clipped Bresenham writing the full 64 rows directly, since the
* resident pixel helper does not bound-check. The eight solids' faces (with a
* uniform outward winding) are generated offline by a convex-hull extractor, so
* nothing here has to be hand-wound. Pure compute, no radio.
*
* Keys: UP/DOWN speed · 1-8 shape · STAR next shape · F solid/wire ·
* MENU pause · EXIT quit.
*/
#include <stdint.h>
#include <stdbool.h>
#include "../app_api.h"
#define W 128
#define H 64
#define CX 64 /* projection centre x */
#define CY 32 /* projection centre y */
#define DIST 150 /* camera distance along +z (keeps zc > 0) */
#define FOCAL 80 /* focal length / field-of-view scale */
#define MAXV 12 /* largest vertex count across the solids */
static const app_api_t *A;
/* One Q14 sine quadrant. Symmetry recovers the full 256-step wave while saving
* 382 bytes for the renderer. */
static const int16_t SIN_Q[65] = {
0, 402, 804, 1205, 1606, 2006, 2404, 2801, 3196, 3590, 3981,
4370, 4756, 5139, 5520, 5897, 6270, 6639, 7005, 7366, 7723, 8076,
8423, 8765, 9102, 9434, 9760,10080,10394,10702,11003,11297,11585,
11866,12140,12406,12665,12916,13160,13395,13623,13842,14053,14256,
14449,14635,14811,14978,15137,15286,15426,15557,15679,15791,15893,
15986,16069,16143,16207,16261,16305,16340,16364,16379,16384
};
static int sin8(uint8_t angle)
{
const uint8_t quadrant = angle >> 6;
uint8_t i = angle & 63u;
if (quadrant & 1u)
i = (uint8_t)(64u - i);
const int value = SIN_Q[i];
return quadrant >= 2u ? -value : value;
}
/* A face is a polygon of up to 6 vertex indices, wound CCW as seen from outside
* (generated offline by the hull extractor, so the signed-area cull sign is the
* same for every solid). Unused slots are 0-padded and ignored (n gives length). */
typedef struct { uint8_t n; uint8_t v[6]; } face_t;
static const int8_t CUBE_V[8][3]={{-26,-26,-26},{26,-26,-26},{26,26,-26},{-26,26,-26},{-26,-26,26},{26,-26,26},{26,26,26},{-26,26,26}};
static const face_t CUBE_F[6]={{4,{2,1,0,3,0,0}},{4,{4,0,1,5,0,0}},{4,{7,3,0,4,0,0}},{4,{5,1,2,6,0,0}},{4,{6,2,3,7,0,0}},{4,{7,4,5,6,0,0}}};
static const int8_t OCTAHEDRON_V[6][3]={{38,0,0},{-38,0,0},{0,38,0},{0,-38,0},{0,0,38},{0,0,-38}};
static const face_t OCTAHEDRON_F[8]={{3,{4,0,2,0,0,0}},{3,{2,0,5,0,0,0}},{3,{3,0,4,0,0,0}},{3,{5,0,3,0,0,0}},{3,{2,1,4,0,0,0}},{3,{5,1,2,0,0,0}},{3,{4,1,3,0,0,0}},{3,{3,1,5,0,0,0}}};
static const int8_t TETRAHEDRON_V[4][3]={{28,28,28},{28,-28,-28},{-28,28,-28},{-28,-28,28}};
static const face_t TETRAHEDRON_F[4]={{3,{2,0,1,0,0,0}},{3,{1,0,3,0,0,0}},{3,{3,0,2,0,0,0}},{3,{2,1,3,0,0,0}}};
static const int8_t DIAMOND_V[8][3]={{30,0,0},{14,26,0},{-14,26,0},{-30,0,0},{-14,-26,0},{14,-26,0},{0,0,40},{0,0,-40}};
static const face_t DIAMOND_F[12]={{3,{6,0,1,0,0,0}},{3,{1,0,7,0,0,0}},{3,{5,0,6,0,0,0}},{3,{7,0,5,0,0,0}},{3,{6,1,2,0,0,0}},{3,{2,1,7,0,0,0}},{3,{6,2,3,0,0,0}},{3,{3,2,7,0,0,0}},{3,{6,3,4,0,0,0}},{3,{4,3,7,0,0,0}},{3,{6,4,5,0,0,0}},{3,{5,4,7,0,0,0}}};
static const int8_t ICOSAHEDRON_V[12][3]={{0,18,29},{0,18,-29},{0,-18,29},{0,-18,-29},{18,29,0},{18,-29,0},{-18,29,0},{-18,-29,0},{29,0,18},{29,0,-18},{-29,0,18},{-29,0,-18}};
static const face_t ICOSAHEDRON_F[20]={{3,{8,0,2,0,0,0}},{3,{2,0,10,0,0,0}},{3,{6,0,4,0,0,0}},{3,{4,0,8,0,0,0}},{3,{10,0,6,0,0,0}},{3,{3,1,9,0,0,0}},{3,{11,1,3,0,0,0}},{3,{4,1,6,0,0,0}},{3,{9,1,4,0,0,0}},{3,{6,1,11,0,0,0}},{3,{5,2,7,0,0,0}},{3,{8,2,5,0,0,0}},{3,{7,2,10,0,0,0}},{3,{7,3,5,0,0,0}},{3,{5,3,9,0,0,0}},{3,{11,3,7,0,0,0}},{3,{9,4,8,0,0,0}},{3,{8,5,9,0,0,0}},{3,{10,6,11,0,0,0}},{3,{11,7,10,0,0,0}}};
static const int8_t CUBOCTA_V[12][3]={{-24,-24,0},{-24,24,0},{24,-24,0},{24,24,0},{-24,0,-24},{-24,0,24},{24,0,-24},{24,0,24},{0,-24,-24},{0,-24,24},{0,24,-24},{0,24,24}};
static const face_t CUBOCTA_F[14]={{4,{4,0,5,1,0,0}},{4,{2,9,0,8,0,0}},{3,{8,0,4,0,0,0}},{3,{5,0,9,0,0,0}},{4,{10,1,11,3,0,0}},{3,{4,1,10,0,0,0}},{3,{11,1,5,0,0,0}},{4,{3,7,2,6,0,0}},{3,{6,2,8,0,0,0}},{3,{9,2,7,0,0,0}},{3,{10,3,6,0,0,0}},{3,{7,3,11,0,0,0}},{4,{8,4,10,6,0,0}},{4,{7,11,5,9,0,0}}};
static const int8_t HEXPRISM_V[12][3]={{26,0,24},{12,22,24},{-12,22,24},{-26,0,24},{-12,-22,24},{12,-22,24},{26,0,-24},{12,22,-24},{-12,22,-24},{-26,0,-24},{-12,-22,-24},{12,-22,-24}};
static const face_t HEXPRISM_F[8]={{6,{4,5,0,1,2,3}},{4,{7,1,0,6,0,0}},{4,{6,0,5,11,0,0}},{4,{8,2,1,7,0,0}},{4,{9,3,2,8,0,0}},{4,{10,4,3,9,0,0}},{4,{11,5,4,10,0,0}},{6,{9,8,7,6,11,10}}};
static const int8_t PENTAGEM_V[7][3]={{28,0,0},{8,26,0},{-22,16,0},{-22,-16,0},{8,-26,0},{0,0,42},{0,0,-42}};
static const face_t PENTAGEM_F[10]={{3,{5,0,1,0,0,0}},{3,{1,0,6,0,0,0}},{3,{4,0,5,0,0,0}},{3,{6,0,4,0,0,0}},{3,{5,1,2,0,0,0}},{3,{2,1,6,0,0,0}},{3,{5,2,3,0,0,0}},{3,{3,2,6,0,0,0}},{3,{5,3,4,0,0,0}},{3,{4,3,6,0,0,0}}};
typedef struct {
const int8_t (*v)[3];
const face_t *f;
uint8_t nv;
uint8_t nf;
const char *name;
uint8_t namelen;
} shape_t;
#define NSHAPE 8
static const shape_t SHAPES[NSHAPE] = {
{ CUBE_V, CUBE_F, 8, 6, "CUBE", 4 },
{ OCTAHEDRON_V, OCTAHEDRON_F, 6, 8, "OCTAHEDRON", 10 },
{ TETRAHEDRON_V, TETRAHEDRON_F, 4, 4, "TETRAHEDRON", 11 },
{ DIAMOND_V, DIAMOND_F, 8, 12, "DIAMOND", 7 },
{ ICOSAHEDRON_V, ICOSAHEDRON_F, 12, 20, "ICOSAHEDRON", 11 },
{ CUBOCTA_V, CUBOCTA_F, 12, 14, "CUBOCTA", 7 },
{ HEXPRISM_V, HEXPRISM_F, 12, 8, "HEXPRISM", 8 },
{ PENTAGEM_V, PENTAGEM_F, 7, 10, "PENTAGEM", 8 },
};
/* Per-frame projected screen coords + rotated depth of each vertex. */
static int16_t px[MAXV], py[MAXV], pz[MAXV];
/* Quarter-half-units per frame. The low end has fractional angular steps;
* level 16 reaches the old 16 half-units/frame once divided by four. */
static const uint8_t ROT_RATE[16] = {
1, 2, 3, 4, 6, 8, 10, 12,
16, 20, 24, 30, 36, 44, 52, 64
};
/* Set one pixel across the full 64 rows: 0..7 -> status line, 8..63 -> fb. */
static void set_pixel(int x, int y)
{
if ((unsigned)x >= W || (unsigned)y >= H)
return;
const uint8_t bit = (uint8_t)(1u << (y & 7));
if (y < 8)
A->status_line[x] |= bit;
else
A->fb[(y >> 3) - 1][x] |= bit;
}
/* Integer Bresenham; dotted skips every other step for the depth cue. */
static void draw_edge(int x0, int y0, int x1, int y1, bool dotted)
{
const int dx = (x1 > x0 ? x1 - x0 : x0 - x1);
const int dy = -(y1 > y0 ? y1 - y0 : y0 - y1);
const int sx = (x0 < x1 ? 1 : -1);
const int sy = (y0 < y1 ? 1 : -1);
int err = dx + dy;
unsigned step = 0;
for (;;) {
if (!dotted || (step & 1u) == 0u)
set_pixel(x0, y0);
if (x0 == x1 && y0 == y1)
break;
const int e2 = 2 * err;
if (e2 >= dy) { err += dy; x0 += sx; }
if (e2 <= dx) { err += dx; y0 += sy; }
step++;
}
}
static void clear_screen(void)
{
for (uint8_t x = 0; x < W; x++) {
A->status_line[x] = 0;
for (uint8_t p = 0; p < 7u; p++)
A->fb[p][x] = 0;
}
}
/* Signed area of a face's projected polygon (<0 == facing us, calibrated). */
static int face_area(const face_t *f)
{
int sa = 0;
for (uint8_t k = 0; k < f->n; k++) {
const uint8_t a = f->v[k];
const uint8_t b = f->v[(k + 1u == f->n) ? 0u : k + 1u];
sa += (int)px[a] * py[b] - (int)px[b] * py[a];
}
return sa;
}
__attribute__((section(".text.entry"), used))
void app_main(const app_api_t *api)
{
A = api;
A->backlight_on();
A->status_clear();
uint16_t ax = 0, ay = 0, az = 0; /* Q2 half-units: 2048 = full turn */
uint8_t shape = 0;
uint8_t speed = 4; /* 1..16, shared by all three rotation axes */
bool paused = false;
bool wire = true; /* false = solid (hidden-line) */
bool running = true;
uint8_t prevKey = APP_KEY_INVALID;
while (running) {
uint8_t key = A->get_key();
if (key == APP_KEY_SAVER) {
prevKey = APP_KEY_INVALID;
A->delay_ms(10);
A->backlight_update();
continue;
}
if (key == APP_KEY_WAKE)
key = APP_KEY_INVALID;
if (key != prevKey && key != APP_KEY_INVALID) {
A->backlight_on();
switch (key) {
case APP_KEY_EXIT:
running = false;
break;
case APP_KEY_UP:
case APP_KEY_DOWN: {
const int8_t dir = A->nav_dir(key);
if (dir > 0 && speed < 16u) speed++;
if (dir < 0 && speed > 1u) speed--;
break;
}
case APP_KEY_MENU:
paused = !paused;
break;
case APP_KEY_STAR:
shape = (uint8_t)((shape + 1u) % NSHAPE);
break;
case APP_KEY_F:
wire = !wire;
break;
case APP_KEY_1: case APP_KEY_2: case APP_KEY_3: case APP_KEY_4:
case APP_KEY_5: case APP_KEY_6: case APP_KEY_7: case APP_KEY_8:
if ((uint8_t)(key - APP_KEY_1) < NSHAPE)
shape = (uint8_t)(key - APP_KEY_1);
break;
default:
break;
}
}
prevKey = key;
if (!running)
break;
const shape_t *s = &SHAPES[shape];
const uint8_t ia = (uint8_t)(ax >> 3), ib = (uint8_t)(ay >> 3), ic = (uint8_t)(az >> 3);
const int cx = sin8((uint8_t)(ia + 64u)), sxr = sin8(ia);
const int cy = sin8((uint8_t)(ib + 64u)), syr = sin8(ib);
const int cz = sin8((uint8_t)(ic + 64u)), szr = sin8(ic);
for (uint8_t i = 0; i < s->nv; i++) {
int x = s->v[i][0], y = s->v[i][1], z = s->v[i][2];
int ny = (y * cx - z * sxr) >> 14; /* Rx */
int nz = (y * sxr + z * cx) >> 14;
y = ny; z = nz;
int nx = (x * cy + z * syr) >> 14; /* Ry */
nz = (z * cy - x * syr) >> 14;
x = nx; z = nz;
nx = (x * cz - y * szr) >> 14; /* Rz */
ny = (x * szr + y * cz) >> 14;
x = nx; y = ny;
const int zc = z + DIST; /* always > 0 */
px[i] = (int16_t)(CX + (x * FOCAL) / zc);
py[i] = (int16_t)(CY + (y * FOCAL) / zc);
pz[i] = (int16_t)z;
}
clear_screen();
for (uint8_t i = 0; i < s->nf; i++) {
const face_t *f = &s->f[i];
if (!wire && face_area(f) >= 0)
continue; /* hidden face culled */
for (uint8_t k = 0; k < f->n; k++) {
const uint8_t a = f->v[k];
const uint8_t b = f->v[(k + 1u == f->n) ? 0u : k + 1u];
const bool dotted = wire && (pz[a] + pz[b] > 0); /* far half */
draw_edge(px[a], py[a], px[b], py[b], dotted);
}
}
/* Shape name: inverse label, top-left of the status bar (scan-list look). */
const uint8_t end = (uint8_t)(2u + 4u * s->namelen);
for (uint8_t i = 0; i <= end; i++)
A->status_line[i] = 0;
A->print_inverse(s->name, 2, 0, true, true, end);
A->blit_status();
A->blit_full();
if (!paused) {
const uint16_t rate = ROT_RATE[speed - 1u];
ax += rate;
ay += (uint16_t)(rate + rate / 2u);
az += (uint16_t)((rate + 1u) / 2u);
}
A->backlight_update();
A->delay_ms((uint32_t)(32u - speed * 2u)); /* slow low end, no added delay at level 16 */
}
}
+43
View File
@@ -0,0 +1,43 @@
/* Overlay-app link script (POC).
*
* The blob is linked to run at the PY25Q16 sector-cache overlay VMA (shared with
* the multiboot RAM stub — never both active at once). Code + rodata + data +
* bss must all fit in the 4 KiB overlay. objcopy -O binary emits text+rodata+
* data only; the loader zeroes the overlay before copying, so bss starts clean.
*
* VMA must match ADDR(.mb_workspace) in Core/py32f071xb.ld. If the firmware RAM
* layout shifts, update APP_VMA and the loader's compile-time assert catches a
* mismatch. */
/* APP_VMA is the target firmware's __mb_workspace_start (read from its .map). It
* varies with the RAM layout, so pass it from the build: -Wl,--defsym,APP_VMA=0x...
* The default is only a fallback for a standalone size check. */
APP_VMA = DEFINED(APP_VMA) ? APP_VMA : 0x20000280;
APP_LENGTH = 0x1000; /* 4 KiB sector-cache overlay */
ENTRY(app_main)
MEMORY {
APP (rwx) : ORIGIN = APP_VMA, LENGTH = APP_LENGTH
}
SECTIONS {
.app APP_VMA : {
KEEP(*(.text.entry)) /* app_main pinned to offset 0 */
*(.text .text.*)
*(.rodata .rodata.*)
. = ALIGN(4);
*(.data .data.*)
. = ALIGN(4);
__app_bss_start = .;
*(.bss .bss.* COMMON)
. = ALIGN(4);
__app_bss_end = .;
} > APP
__app_end = .;
ASSERT(__app_end <= APP_VMA + APP_LENGTH,
"Broadcast FM overlay app overflows the 4 KiB overlay")
/DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.eh_frame*) *(.comment) *(.note.*) }
}
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# Build one overlay-app blob (.app). Invoked by ../../../compile-app.sh inside
# the uvk1-uvk5v3 Docker image, run from this app's directory. Everything is
# derived from the directory name; only APP_NAME - the human label baked into
# the 64-byte blob header - is per-app. The .app file is named after it (spaces
# stripped), so "Broadcast FM" -> BroadcastFM.app.
set -euo pipefail
APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ...
APP_NAME="Broadcast FM" # <-- the only per-app line
APP_VER="1.0"
APP_API_MIN=1
APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld)
OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM)
CC=/opt/toolchain/bin/arm-none-eabi-gcc
OBJCOPY=/opt/toolchain/bin/arm-none-eabi-objcopy
command -v arm-none-eabi-gcc >/dev/null 2>&1 && { CC=arm-none-eabi-gcc; OBJCOPY=arm-none-eabi-objcopy; }
CFLAGS="-mcpu=cortex-m0plus -mthumb -Os -std=gnu11 -ffreestanding -fno-builtin -fno-common \
-fomit-frame-pointer -ffunction-sections -fdata-sections -Wall -Wextra"
LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \
-Wl,--gc-sections -Wl,-Map=${APP}.map -Wl,--build-id=none -Wl,--no-warn-rwx-segments"
rm -f ./*.app ./*.elf ./*.bin # drop stale artifacts so discovery is unambiguous
step() { printf '\r 🔨 %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; }
trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
--name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" \
--shortcut fm --require fm --screensaver >/dev/null
trap - ERR
BYTES=$(wc -c < "${APP}.bin")
if [ "$BYTES" -gt 4096 ]; then
printf '\r 🚨 %-13s OVERFLOWS 4 KiB (%d B) \n' "$APP_NAME" "$BYTES"; exit 1
fi
printf '\r ✅ %-13s %4d B (%d%% of 4 KiB) -> %s.app \n' \
"$APP_NAME" "$BYTES" "$(( BYTES * 100 / 4096 ))" "$OUT"
+378
View File
@@ -0,0 +1,378 @@
/* Copyright 2026 Armel F4HWN
* https://github.com/armel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Broadcast FM — overlay app. Sovereign: drives the BK1080 directly and owns the
* radio while running (NO BK4819 dual-watch, unlike the resident FM). Reimplements
* the full resident FM (fm.c / UI_DisplayFM) - VFO + 48 memories - sharing the
* resident channel table (gFM_Channels) and config (gEeprom.FM_*), committed to
* EEPROM on exit. The BK1080 audio is a hardware path, so playback needs no CPU loop.
*
* Keys (iso legacy): 0-9 = frequency (VFO) / channel (MR) entry · UP/DOWN = tune /
* channel step · STAR = manual scan · F+STAR = auto-scan (stores 48) · MENU = save
* (VFO) / delete (MR) · F+1 = band · F+3 = VFO<->MR · F+0 / EXIT = quit.
*/
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include "../app_api.h"
#define STEP 1
#define SCAN_SETTLE 100
#define CHMAX APP_FM_CH_MAX
static const char BAND_NAME[4][10] = { "87.5-108M", "76-108M", "76-90M", "64-76M" };
static const uint8_t FONT_F[9] = {0x3e,0x7f,0x41,0x75,0x75,0x75,0x7d,0x7f,0x3e};
static void cpy(uint8_t *d, const uint8_t *s, uint8_t n){ while(n--)*d++=*s++; }
static const app_api_t *A;
static uint16_t *ch; /* shared gFM_Channels[48] */
static app_fm_state_t st; /* freq_playing, sel_freq, band, is_mr, sel_ch */
static uint16_t lo, hi;
static int8_t scanState; /* 0 = off, +1 up, -1 down (non-blocking) */
static bool autoScan;
static uint8_t chPos;
static uint16_t scanTimer; /* ms until the next scan step */
static bool foundFreq;
static bool askSave, askDelete;
static uint8_t savePos;
static bool fArm;
static uint8_t inBox[4], inIdx;
static char str[16];
static char *putu(char *o, uint32_t v){ char t[6]; int8_t n=0; do{t[n++]=(char)('0'+v%10);v/=10;}while(v&&n<6); while(n--)*o++=t[n]; return o; }
static char *put(char *o, const char *s){ while(*s)*o++=*s++; return o; }
static void bandLimits(void){ lo=A->fm_lo(st.band); hi=A->fm_hi(st.band); }
static uint16_t wrap(uint16_t f){ if(f<lo)return hi; if(f>hi)return lo; return f; }
static void tune(void){ A->fm_set_freq(st.freq_playing, st.band); }
static void dirty(void){ /* nothing yet; committed on exit */ }
static bool validCh(uint8_t c){ return c<CHMAX && ch[c]>=lo && ch[c]<hi; }
static uint8_t findNext(uint8_t c, int8_t dir)
{
for(uint8_t i=0;i<CHMAX;i++){
if(c==0xFF) c=CHMAX-1;
else if(c>=CHMAX) c=0;
if(validCh(c)) return c;
c=(uint8_t)(c+dir);
}
return 0xFF;
}
static int configChannel(void)
{
st.freq_playing=st.sel_freq;
if(st.is_mr){
uint8_t c=findNext(st.sel_ch,+1);
if(c==0xFF){ st.is_mr=false; return -1; }
st.sel_ch=c; st.freq_playing=ch[c];
}
return 0;
}
/* ---- drawing (mirror UI_DisplayFM) ---- */
static void freqBig(uint16_t f)
{
uint16_t mhz=f/10u; char *o=str;
if(mhz<100u)*o++=' ';
o=putu(o,mhz); *o++='.'; *o++=(char)('0'+f%10u); *o='\0';
A->display_freq(str,36,1,true);
}
/* append a zero-padded 2-digit number (1..99), return the end pointer */
static char *num2(char *o, uint8_t n){ if(n<10)*o++='0'; return putu(o,n); }
static void draw(void)
{
char b[12], *o;
A->display_clear();
A->status_clear();
A->draw_battery();
if(fArm) cpy(A->status_line+70,FONT_F,sizeof(FONT_F)); /* F armed indicator */
A->print_string("FM",2,0,0,8);
A->print_normal(BAND_NAME[st.band],1,0,6);
/* ---- status (line 3) ---- */
if(askSave) { o=put(b,"SAVE?"); *o='\0'; }
else if(askDelete) { o=put(b,"DEL?"); *o='\0'; }
else if(scanState) {
if(autoScan){ o=put(b,"A-SCAN("); o=putu(o,chPos); *o++=')'; } /* count like UI_DisplayFM */
else o=put(b,"M-SCAN");
*o='\0';
}
else if(st.is_mr) { o=put(b,"MR(CH"); o=num2(o,(uint8_t)(st.sel_ch+1)); *o++=')'; *o='\0'; }
else {
o=put(b,"VFO"); *o='\0';
for(uint8_t i=0;i<CHMAX;i++) if(ch[i]==st.freq_playing){ o=put(b,"VFO(CH"); o=num2(o,(uint8_t)(i+1)); *o++=')'; *o='\0'; break; }
}
A->print_string(b,0,127,3,10);
/* ---- line 1: input in progress / channel prompt / frequency ---- */
if(inIdx>0){
if(st.is_mr||askSave){ /* channel number: "CH-XX" */
o=put(b,"CH-");
*o++=(char)('0'+inBox[0]);
*o++=(inIdx>1)?(char)('0'+inBox[1]):'-';
*o='\0';
A->print_string(b,0,127,1,10);
} else { /* VFO frequency: "ddd.d" */
b[0]=(char)('0'+inBox[0]);
b[1]=(inIdx>1)?(char)('0'+inBox[1]):'-';
b[2]=(inIdx>2)?(char)('0'+inBox[2]):'-';
b[3]='.';
b[4]=(inIdx>3)?(char)('0'+inBox[3]):'-';
b[5]='\0';
A->display_freq(b,36,1,false);
}
} else if(askSave){ /* pick a save slot */
o=put(b,"CH-"); o=num2(o,(uint8_t)(savePos+1)); *o='\0';
A->print_string(b,0,127,1,10);
} else if(askDelete){
o=put(b,"CH-"); o=num2(o,(uint8_t)(st.sel_ch+1)); *o='\0';
A->print_string(b,0,127,1,10);
} else {
freqBig(st.freq_playing);
}
}
static void show(void){ draw(); A->blit_status(); A->blit_full(); }
/* ---- non-blocking scan state machine (mirrors ACTION_Scan_FM / FM_Play / FM_Tune) ---- */
static void scanTuneNext(void) /* FM_Tune(freq, scanState, false): step + mute + settle */
{
A->fm_mute(true);
foundFreq=false;
st.freq_playing=wrap((uint16_t)(st.freq_playing+scanState));
tune();
scanTimer=SCAN_SETTLE;
}
static void stopScan(void) /* FM_PlayAndUpdate */
{
if(autoScan){ st.is_mr=true; st.sel_ch=0; }
scanState=0; autoScan=false;
configChannel();
tune();
A->fm_mute(false);
}
static void scanStep(void) /* FM_Play: one step when the settle timer expires */
{
if(A->fm_valid(st.freq_playing,lo)==0){ /* station locked */
if(!autoScan){
scanState=0; foundFreq=true;
if(!st.is_mr) st.sel_freq=st.freq_playing;
A->fm_mute(false); /* audio on */
return;
}
if(chPos<CHMAX) ch[chPos++]=st.freq_playing;
if(chPos>=CHMAX){ stopScan(); return; }
}
if(autoScan && st.freq_playing>=A->fm_hi(1)){ stopScan(); return; }
scanTuneNext();
}
static void startManual(int8_t dir)
{
if(scanState){ stopScan(); return; } /* STAR toggles */
autoScan=false; chPos=0; scanState=dir;
scanTuneNext();
}
static void startAuto(void)
{
if(scanState){ stopScan(); return; }
autoScan=true; chPos=0; scanState=+1;
for(uint8_t i=0;i<CHMAX;i++) ch[i]=0xFFFF;
st.freq_playing=lo;
A->fm_mute(true);
tune(); /* FM_Tune(lo,1,true): no step */
scanTimer=SCAN_SETTLE;
}
/* ---- digit entry ---- */
static void digit(uint8_t d)
{
if(askDelete||scanState) return;
if(askSave || st.is_mr){ /* 2-digit channel */
if(inIdx<2) inBox[inIdx++]=d;
if(inIdx>=2){
uint8_t c=(uint8_t)(inBox[0]*10+inBox[1]-1); inIdx=0;
if(askSave){ if(c<CHMAX) savePos=c; }
else if(validCh(c)){ st.sel_ch=c; st.freq_playing=ch[c]; tune(); }
}
return;
}
/* VFO frequency: 4 digits, with the leading-digit>1 zero-pad trick */
inBox[inIdx++]=d;
if(inIdx==1 && inBox[0]>1){ inBox[1]=inBox[0]; inBox[0]=0; inIdx=2; }
else if(inIdx>3){
uint16_t f=(uint16_t)(inBox[0]*1000+inBox[1]*100+inBox[2]*10+inBox[3]); inIdx=0;
if(f>=lo && f<=hi){ st.sel_freq=f; st.freq_playing=f; tune(); }
}
}
/* ---- MENU: save (VFO) / delete (MR) ---- */
static void menu(void)
{
inIdx=0;
if(!st.is_mr){ /* VFO: save */
if(askSave){ ch[savePos]=st.freq_playing; dirty(); }
askSave=!askSave;
} else { /* MR: delete */
if(askDelete){ ch[st.sel_ch]=0xFFFF; configChannel(); tune(); dirty(); }
askDelete=!askDelete;
}
}
/* ---- UP/DOWN: tune (VFO) or channel step (MR) ---- */
static void upDown(int8_t step)
{
if(scanState){ scanState=step; scanTuneNext(); return; } /* continue scan, new direction */
if(askSave){
if(step>0){ if(++savePos>=CHMAX) savePos=0; }
else savePos=savePos?(uint8_t)(savePos-1u):(uint8_t)(CHMAX-1u);
return;
}
if(st.is_mr){
uint8_t c=findNext((uint8_t)(st.sel_ch+step),step);
if(c!=0xFF && c!=st.sel_ch){ st.sel_ch=c; st.freq_playing=ch[c]; tune(); }
} else {
st.sel_freq=wrap((uint16_t)(st.sel_freq+step));
st.freq_playing=st.sel_freq; tune();
}
}
static void toggleMr(void)
{
st.is_mr=!st.is_mr;
if(configChannel()!=0){ /* no valid channel: stayed VFO */ }
tune();
askSave=askDelete=false; inIdx=0;
}
static bool running;
static void cycleBand(void)
{
st.band=(uint8_t)((st.band+1)&3u); bandLimits();
if(st.freq_playing<lo||st.freq_playing>hi){ st.freq_playing=lo; st.sel_freq=lo; }
tune();
}
/* F+key and long-press functions (band / VFO<->MR / auto-scan / quit). */
static void func(uint8_t key)
{
switch(key){
case APP_KEY_0: running=false; break;
case APP_KEY_1: cycleBand(); break;
case APP_KEY_3: toggleMr(); break;
case APP_KEY_STAR: startAuto(); break;
default: break;
}
}
static void onShort(uint8_t key)
{
if(key==APP_KEY_F){ fArm=!fArm; return; }
if(fArm){ fArm=false; func(key); return; } /* F + key */
if(key<=APP_KEY_9){ digit(key); return; }
switch(key){
case APP_KEY_UP:
case APP_KEY_DOWN: upDown(A->nav_dir(key)); break;
case APP_KEY_STAR: startManual(+1); break;
case APP_KEY_MENU: menu(); break;
case APP_KEY_EXIT:
if(inIdx) inIdx--;
else if(askSave||askDelete) askSave=askDelete=false;
else running=false;
break;
default: break;
}
}
static void onLong(uint8_t key){ fArm=false; func(key); } /* long band / MR / scan / quit */
__attribute__((section(".text.entry"),used))
void app_main(const app_api_t *api)
{
A=api;
A->backlight_on();
ch=A->fm_channels;
A->fm_state(&st,false); /* inherit the resident FM state */
if(st.band>3) st.band=0;
bandLimits();
if(st.freq_playing<lo||st.freq_playing>hi) st.freq_playing=lo;
if(st.sel_freq<lo||st.sel_freq>hi) st.sel_freq=st.freq_playing;
if(st.is_mr) configChannel();
askSave=askDelete=fArm=false; inIdx=0; savePos=0;
A->fm_enter(st.freq_playing,st.band);
/* short = on release (held < ~0.5 s) · long = at the 0.5 s threshold ·
UP/DOWN = immediate step on press, then auto-repeat while held. */
running=true;
uint8_t held=APP_KEY_INVALID;
uint16_t heldMs=0;
bool firedLong=false;
bool dirty=true; /* redraw only on change */
while(running){
uint8_t key=A->get_key();
bool rep=(key==APP_KEY_UP||key==APP_KEY_DOWN);
if(key==APP_KEY_SAVER){
held=APP_KEY_INVALID; heldMs=0; firedLong=false;
A->delay_ms(10); A->backlight_update();
continue;
} else if(key==APP_KEY_WAKE||key==APP_KEY_PTT){
held=APP_KEY_INVALID; heldMs=0; firedLong=false; dirty=true;
} else if(key==APP_KEY_INVALID){
if(held!=APP_KEY_INVALID && !firedLong &&
held!=APP_KEY_UP && held!=APP_KEY_DOWN){
onShort(held); dirty=true;
}
held=APP_KEY_INVALID; heldMs=0; firedLong=false;
} else if(key!=held){
A->backlight_on();
held=key; heldMs=0; firedLong=false;
if(rep){ onShort(key); dirty=true; } /* immediate first step */
} else {
heldMs=(uint16_t)(heldMs+50u);
if(rep){
if(heldMs>=300){ onShort(held); dirty=true; } /* auto-repeat */
} else if(!firedLong && heldMs>=400){
firedLong=true; onLong(held); dirty=true;
}
}
/* non-blocking scan: one step each time the settle timer expires */
if(scanState){
/* Match resident FM: an active scan keeps the display awake. */
A->backlight_on();
if(scanTimer<=50) scanStep();
else scanTimer=(uint16_t)(scanTimer-50);
dirty=true;
}
if(dirty){ show(); dirty=false; }
/* Keep the resident 10 ms fade cadence while retaining this app's
* existing 50 ms key/scan state-machine tick. */
for(uint8_t i=0;i<5u;i++){
A->delay_ms(10);
A->backlight_update();
}
}
/* push our state back to the resident FM and persist (config + 48 channels) */
if(!st.is_mr) st.sel_freq=st.freq_playing;
A->fm_state(&st,true);
A->fm_commit();
A->fm_exit();
}
+43
View File
@@ -0,0 +1,43 @@
/* Overlay-app link script (POC).
*
* The blob is linked to run at the PY25Q16 sector-cache overlay VMA (shared with
* the multiboot RAM stub — never both active at once). Code + rodata + data +
* bss must all fit in the 4 KiB overlay. objcopy -O binary emits text+rodata+
* data only; the loader zeroes the overlay before copying, so bss starts clean.
*
* VMA must match ADDR(.mb_workspace) in Core/py32f071xb.ld. If the firmware RAM
* layout shifts, update APP_VMA and the loader's compile-time assert catches a
* mismatch. */
/* APP_VMA is the target firmware's __mb_workspace_start (read from its .map). It
* varies with the RAM layout, so pass it from the build: -Wl,--defsym,APP_VMA=0x...
* The default is only a fallback for a standalone size check. */
APP_VMA = DEFINED(APP_VMA) ? APP_VMA : 0x20000280;
APP_LENGTH = 0x1000; /* 4 KiB sector-cache overlay */
ENTRY(app_main)
MEMORY {
APP (rwx) : ORIGIN = APP_VMA, LENGTH = APP_LENGTH
}
SECTIONS {
.app APP_VMA : {
KEEP(*(.text.entry)) /* app_main pinned to offset 0 */
*(.text .text.*)
*(.rodata .rodata.*)
. = ALIGN(4);
*(.data .data.*)
. = ALIGN(4);
__app_bss_start = .;
*(.bss .bss.* COMMON)
. = ALIGN(4);
__app_bss_end = .;
} > APP
__app_end = .;
ASSERT(__app_end <= APP_VMA + APP_LENGTH,
"FoxHunt overlay app overflows the 4 KiB overlay")
/DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.eh_frame*) *(.comment) *(.note.*) }
}
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# Build one overlay-app blob (.app). Invoked by ../../../compile-app.sh inside
# the uvk1-uvk5v3 Docker image, run from this app's directory. Everything is
# derived from the directory name; only APP_NAME - the human label baked into
# the 64-byte blob header - is per-app. The .app file is named after it (spaces
# stripped), so "Broadcast FM" -> BroadcastFM.app.
set -euo pipefail
APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ...
APP_NAME="FoxHunt" # <-- the only per-app line
APP_VER="1.0"
APP_API_MIN=1
APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld)
OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM)
CC=/opt/toolchain/bin/arm-none-eabi-gcc
OBJCOPY=/opt/toolchain/bin/arm-none-eabi-objcopy
command -v arm-none-eabi-gcc >/dev/null 2>&1 && { CC=arm-none-eabi-gcc; OBJCOPY=arm-none-eabi-objcopy; }
CFLAGS="-mcpu=cortex-m0plus -mthumb -Os -std=gnu11 -ffreestanding -fno-builtin -fno-common \
-fomit-frame-pointer -ffunction-sections -fdata-sections -Wall -Wextra"
LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \
-Wl,--gc-sections -Wl,-Map=${APP}.map -Wl,--build-id=none -Wl,--no-warn-rwx-segments"
rm -f ./*.app ./*.elf ./*.bin # drop stale artifacts so discovery is unambiguous
step() { printf '\r 🔨 %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; }
trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
--name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" \
--shortcut foxhunt >/dev/null
trap - ERR
BYTES=$(wc -c < "${APP}.bin")
if [ "$BYTES" -gt 4096 ]; then
printf '\r 🚨 %-13s OVERFLOWS 4 KiB (%d B) \n' "$APP_NAME" "$BYTES"; exit 1
fi
printf '\r ✅ %-13s %4d B (%d%% of 4 KiB) -> %s.app \n' \
"$APP_NAME" "$BYTES" "$(( BYTES * 100 / 4096 ))" "$OUT"
+375
View File
@@ -0,0 +1,375 @@
/* Copyright 2026 Armel F4HWN
* https://github.com/armel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* FoxHunt (RX) — overlay app. Ported from App/app/foxhunt.c, hunt sub-mode only
* (the beacon becomes a separate app). Signal-strength hunter: corrected dBm with
* a big number, an IARU S-meter staircase or a scrolling history, peak/min hold,
* a 1 s trend, a front-end attenuator ladder, and Geiger/station audio.
*
* Keys: 1 gauge (bar/history) · 2 audio (off/beep/station) · 3 + UP/DOWN attenuator
* MENU reset holds · long F keypad lock · EXIT quit.
*/
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include "../app_api.h"
#define LCD_WIDTH 128
#define BK_REG_13 0x13
/* --- tuning (from foxhunt.c) --- */
#define DBM_FLOOR (-141)
#define DBM_CEIL (-53)
#define TICK_MS 50
#define TREND_TICKS 20
#define LOCK_HOLD_MS 500
#define SILENCE_DBM (-120)
#define TONE_MIN 400
#define TONE_MAX 2400
#define RATE_SLOW 20
#define RATE_FAST 2
#define BLIP_MS 50
#define AUDIO_SETTLE 60
#define ATT_SETTLE 40
#define ATT_COUNT 6
#define ATT_BYP0 4
#define SEG_COUNT 13
#define BAR_X0 6
#define SEG_PITCH 9
#define SEG_W 8
#define SEG_BOTTOM 37
#define HIST_LEN 120
#define HIST_DECIM 3
#define GRAPH_X0 4
#define GRAPH_TOP 27
#define GRAPH_BOT 45
#define GRAPH_FLOOR 2
#define AUDIO_OFF 0
#define AUDIO_BEEP 1
#define AUDIO_STATION 2
#define GRAPH_BAR 0
#define GRAPH_HIST 1
#define CFG_MAGIC 0xF1 /* config validity marker */
static const uint16_t ATT_REG13[ATT_COUNT] = { 0x03DF, 0x03DD, 0x03DB, 0x03D9, 0x0379, 0x0139 };
static const uint8_t ATT_DB[ATT_BYP0] = { 0, 6, 15, 27 };
/* Status-bar / trend icons (bytes copied verbatim from App/bitmaps.c). */
static const uint8_t BMP_BARS[11] = {0x40,0x40,0x00,0x70,0x70,0x00,0x7c,0x7c,0x00,0x7f,0x7f};
static const uint8_t BMP_GRAPH[15] = {0x08,0x04,0x02,0x04,0x08,0x10,0x20,0x10,0x08,0x04,0x02,0x04,0x08,0x10,0x20};
static const uint8_t BMP_SIGNAL[10] = {0x08,0x1c,0x1c,0x08,0x00,0x22,0x1c,0x41,0x22,0x1c};
static const uint8_t BMP_SPEAKER[10] = {0x1c,0x1c,0x3e,0x7f,0x00,0x22,0x1c,0x41,0x22,0x1c};
static const uint8_t BMP_UP[11] = {0x20,0x30,0x38,0x3c,0x3e,0x3f,0x3e,0x3c,0x38,0x30,0x20};
static const uint8_t BMP_DOWN[11] = {0x01,0x03,0x07,0x0f,0x1f,0x3f,0x1f,0x0f,0x07,0x03,0x01};
static const uint8_t BMP_FLAT[11] = {0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12,0x12};
static const uint8_t FONT_F[9] = {0x3e,0x7f,0x41,0x75,0x75,0x75,0x7d,0x7f,0x3e};
static const uint8_t FONT_LOCK[9] = {0x7c,0x46,0x45,0x45,0x45,0x45,0x45,0x46,0x7c};
static void cpy(uint8_t *d, const uint8_t *s, uint8_t n){ while(n--)*d++=*s++; }
static const app_api_t *A;
static bool foxLocked, fArm, fLongDone;
static uint16_t fHoldMs;
static uint8_t foxAudioMode, foxGraphMode, attStep, audioTick;
static int16_t curDbm, peakDbm, minDbm, trendRef, trendDelta;
static uint8_t trendTick;
static uint8_t histBuf[HIST_LEN];
static uint8_t histHead, histTick;
static int16_t histEma;
static uint8_t prevKey;
static bool running;
static char str[16];
/* ---- tiny formatting ---- */
static uint8_t slen(const char *s){ uint8_t n=0; while(s[n])n++; return n; }
static char *put(char *o,const char *s){ while(*s)*o++=*s++; return o; }
static char *puti(char *o,int v){
uint32_t u;
if(v<0){*o++='-';u=(uint32_t)(-v);} else u=(uint32_t)v;
char t[6]; int8_t n=0;
do{t[n++]=(char)('0'+u%10u);u/=10u;}while(u&&n<6);
while(n--)*o++=t[n];
return o;
}
static void i2str(char *out,int v){ *puti(out,v)='\0'; }
/* ---- radio helpers ---- */
static int16_t iabs16(int16_t v){ return v<0?-v:v; }
static void applyAtt(void){
uint16_t reg = A->bk_read(BK_REG_13);
reg = (uint16_t)((reg & ~0x03FFu) | ATT_REG13[attStep]);
A->bk_write(BK_REG_13, reg);
}
static void setAudio(void){
if(foxAudioMode==AUDIO_OFF){ A->audio_path(false); return; }
A->audio_path(true);
A->delay_ms(AUDIO_SETTLE);
A->set_af(APP_AF_MUTE);
}
static int32_t lerp(int16_t dbm,int32_t lo,int32_t hi){
if(dbm<=DBM_FLOOR) return lo;
if(dbm>=DBM_CEIL) return hi;
const int32_t delta = hi - lo;
const uint32_t distance = ((uint32_t)(dbm-DBM_FLOOR) *
(uint32_t)(delta < 0 ? -delta : delta)) /
(uint32_t)(DBM_CEIL-DBM_FLOOR);
return lo + (delta < 0 ? -(int32_t)distance : (int32_t)distance);
}
static void blip(uint16_t freq){
A->prepare_tone();
A->play_tone_raw(freq, BLIP_MS);
A->tones_off_rx();
A->set_agc(false);
applyAtt();
}
static uint8_t fillCount(int16_t dbm){
int16_t n;
if(dbm<-141) return 0;
if(dbm<=-93) n=(int16_t)(1u+(uint16_t)(dbm+141)/6u);
else n=(int16_t)(9u+(uint16_t)(dbm+93)/10u);
if(n>SEG_COUNT) n=SEG_COUNT;
return (uint8_t)n;
}
static void buildS(char *out,int16_t dbm){
if(dbm>=-93){ int16_t o=dbm-(-93); if(o>40)o=40; char *p=put(out,"S9+"); if(o<10)*p++='0'; i2str(p,o); }
else if(dbm<-141) put(out,"S0")[0]='\0';
else { char *p=put(out,"S"); i2str(p,(int)((uint16_t)(dbm+147)/6u)); }
}
static int16_t div_trunc_pow2(int32_t v, uint8_t shift)
{
if (v < 0)
return (int16_t)-((uint32_t)(-v) >> shift);
return (int16_t)((uint32_t)v >> shift);
}
static uint8_t cycleIndex(uint8_t value, uint8_t count, int8_t dir)
{
if (dir > 0)
return ++value < count ? value : 0u;
return value > 0u ? (uint8_t)(value - 1u) : (uint8_t)(count - 1u);
}
static void histSample(void){
histEma += div_trunc_pow2((int32_t)curDbm * 8 - histEma, 2u);
if(++histTick<HIST_DECIM) return;
histTick=0;
histBuf[histHead]=fillCount(div_trunc_pow2(histEma, 3u));
if(++histHead>=HIST_LEN) histHead=0;
}
static void rebase(void){
curDbm=A->rssi_dbm();
peakDbm=minDbm=trendRef=curDbm;
trendDelta=0; trendTick=0;
uint8_t lvl=fillCount(curDbm);
for(uint8_t i=0;i<HIST_LEN;i++) histBuf[i]=lvl;
histHead=0; histTick=0; histEma=(int16_t)(curDbm*8);
}
static void attCycle(int8_t dir){
attStep=cycleIndex(attStep,ATT_COUNT,dir);
applyAtt();
A->delay_ms(ATT_SETTLE);
rebase();
}
/* ---- drawing ---- */
static void fillRect(int16_t x0,int16_t y0,int16_t x1,int16_t y1){
for(int16_t x=x0;x<=x1;x++) A->draw_line(A->fb,x,y0,x,y1,true);
}
static void drawBar(void){
uint8_t n=fillCount(curDbm);
for(uint8_t i=0;i<n;i++){
int16_t sx=BAR_X0+i*SEG_PITCH;
fillRect(sx,SEG_BOTTOM-i,sx+SEG_W-1,SEG_BOTTOM);
}
A->draw_line(A->fb,BAR_X0,SEG_BOTTOM+2,BAR_X0+(SEG_COUNT-1)*SEG_PITCH+SEG_W-1,SEG_BOTTOM+2,true);
}
static void drawHist(void){
const uint8_t floorY=GRAPH_BOT-GRAPH_FLOOR;
const uint8_t span=floorY-GRAPH_TOP;
A->draw_line(A->fb,GRAPH_X0,GRAPH_BOT,GRAPH_X0+HIST_LEN-1,GRAPH_BOT,true);
uint8_t prevY=0, idx=histHead;
for(uint8_t c=0;c<HIST_LEN;c++){
uint8_t lvl=histBuf[idx]; if(++idx>=HIST_LEN)idx=0;
if(lvl>SEG_COUNT)lvl=SEG_COUNT;
uint8_t x=GRAPH_X0+c;
uint8_t y=(uint8_t)(floorY-((uint32_t)lvl*span)/(uint32_t)SEG_COUNT);
for(uint8_t yy=y+1;yy<=floorY;yy++) if(((x+yy)&1)==0) A->draw_line(A->fb,x,yy,x,yy,true);
if(c==0){ A->draw_line(A->fb,x,y,x,y,true); }
else { uint8_t lo=(y<prevY)?y:prevY, hi=(y<prevY)?prevY:y; A->draw_line(A->fb,x,lo,x,hi,true); }
prevY=y;
}
}
static void tag(const char *s,uint8_t x,uint8_t line){
A->print_inverse(s,x,line,false,true,(uint8_t)(x+slen(s)*4));
}
static void draw(void){
char big[8], sMeter[8];
A->display_clear();
A->status_clear();
A->print_inverse("FOX HUNT",2,0,true,true,34);
A->draw_battery();
/* status-bar icons: graph mode @38, audio mode @55, F/lock @69 */
if(foxGraphMode==GRAPH_HIST) cpy(A->status_line+38,BMP_GRAPH,15);
else cpy(A->status_line+38,BMP_BARS,11);
if(foxAudioMode==AUDIO_BEEP) cpy(A->status_line+55,BMP_SIGNAL,10);
else if(foxAudioMode==AUDIO_STATION) cpy(A->status_line+55,BMP_SPEAKER,10);
if(foxLocked) cpy(A->status_line+70,FONT_LOCK,9);
else if(fArm) cpy(A->status_line+70,FONT_F,sizeof(FONT_F));
i2str(big,curDbm);
A->display_freq(big,2,0,false);
A->print_normal("dBm",(uint8_t)(slen(big)*13+4),0,1);
/* trend arrow (line 0, right) + signed delta (line 1) */
cpy(A->fb[0]+115, trendDelta>0?BMP_UP:trendDelta<0?BMP_DOWN:BMP_FLAT, 11);
if(trendDelta!=0){
char *p=str; if(trendDelta>0)*p++='+'; else{*p++='-';}
int16_t a=iabs16(trendDelta); if(a<10){*p++='0';} i2str(p,a);
A->print_normal("dBm",127-3*7,0,1);
A->print_normal(str,(uint8_t)(127-3*7-2-slen(str)*7),1,1);
}
i2str(put(str,"PK "),peakDbm);
tag(str,4,2);
i2str(put(str,"MN "),minDbm);
tag(str,(uint8_t)(((uint32_t)LCD_WIDTH-(uint32_t)slen(str)*4u)/2u),2);
buildS(sMeter,curDbm);
A->print_inverse(sMeter,(uint8_t)(126-slen(sMeter)*4),2,false,true,125);
if(foxGraphMode==GRAPH_HIST) drawHist();
else drawBar();
if(attStep<ATT_BYP0){ char *o=put(str,"ATT "); o=puti(o,ATT_DB[attStep]); put(o,"dB")[0]='\0'; }
else put(str,(attStep==ATT_BYP0)?"BYP":"BYP+")[0]='\0';
tag(str,4,6);
{ uint32_t f=A->rx_freq(); char *o=puti(str,(int)(f/100000u)); *o++='.';
uint32_t fr=f%100000u; for(int8_t d=4;d>=0;d--){ uint32_t p=1; for(int8_t k=0;k<d;k++)p*=10; *o++=(char)('0'+(fr/p)%10);} *o='\0'; }
A->print_normal(str,(uint8_t)(126-slen(str)*7),0,6);
}
/* ---- config (deferred) ---- */
static void loadConfig(void){
uint8_t c[4];
A->cfg_load(c,4);
if(c[0]==CFG_MAGIC){
if(c[1]<ATT_COUNT) attStep=c[1];
if(c[2]<=GRAPH_HIST) foxGraphMode=c[2];
if(c[3]<=AUDIO_STATION) foxAudioMode=c[3];
}
}
static void saveConfig(void){
uint8_t c[4]={CFG_MAGIC,attStep,foxGraphMode,foxAudioMode};
A->cfg_save(c,4);
}
/* ---- input ---- */
static void handleKeys(void){
uint8_t key=A->get_key();
/* long-press F -> keypad lock */
if(key==APP_KEY_F){
if(!fLongDone){ fHoldMs+=TICK_MS; if(fHoldMs>=LOCK_HOLD_MS){ fLongDone=true; foxLocked=!foxLocked; fArm=false; A->backlight_on(); } }
} else { fHoldMs=0; fLongDone=false; }
if(key==APP_KEY_INVALID||key==prevKey){ prevKey=key; return; }
prevKey=key;
A->backlight_on();
if(foxLocked){
if(key==APP_KEY_UP||key==APP_KEY_DOWN) attCycle(A->nav_dir(key));
return;
}
if(key==APP_KEY_F){ fArm=!fArm; return; }
int8_t dir=fArm?-1:1;
switch(key){
case APP_KEY_EXIT: running=false; break;
case APP_KEY_1: foxGraphMode^=1; break;
case APP_KEY_2:
foxAudioMode=cycleIndex(foxAudioMode,3u,dir); setAudio();
if(foxAudioMode==AUDIO_BEEP){ audioTick=RATE_SLOW; }
break;
case APP_KEY_3: attCycle(dir); break;
case APP_KEY_UP:
case APP_KEY_DOWN: attCycle(A->nav_dir(key)); break;
case APP_KEY_MENU: peakDbm=minDbm=trendRef=curDbm; break;
default: break;
}
fArm=false;
}
static void tickDelay(void){
for(uint8_t i=0;i<TICK_MS/10;i++){ A->delay_ms(10); A->backlight_update(); }
}
__attribute__((section(".text.entry"),used))
void app_main(const app_api_t *api){
A=api;
foxLocked=fArm=fLongDone=false; fHoldMs=0;
attStep=0; foxGraphMode=GRAPH_BAR; foxAudioMode=AUDIO_OFF;
prevKey=APP_KEY_INVALID;
loadConfig();
A->backlight_on();
A->set_agc(false);
applyAtt();
setAudio();
A->delay_ms(ATT_SETTLE);
rebase();
running=true;
while(running){
handleKeys();
if(!running) break;
curDbm=A->rssi_dbm();
if(curDbm>peakDbm) peakDbm=curDbm;
if(curDbm<minDbm) minDbm=curDbm;
if(++trendTick>=TREND_TICKS){ trendDelta=curDbm-trendRef; trendRef=curDbm; trendTick=0; }
histSample();
draw();
A->blit_status();
A->blit_full();
if(foxAudioMode==AUDIO_BEEP && curDbm>=SILENCE_DBM){
if(++audioTick>=(uint8_t)lerp(curDbm,RATE_SLOW,RATE_FAST)){ audioTick=0; blip((uint16_t)lerp(curDbm,TONE_MIN,TONE_MAX)); }
} else if(foxAudioMode==AUDIO_STATION){
A->set_af(curDbm>=SILENCE_DBM?APP_AF_FM:APP_AF_MUTE);
}
A->battery_sample();
tickDelay();
}
saveConfig();
A->audio_path(false);
}
+109
View File
@@ -0,0 +1,109 @@
#!/usr/bin/env python3
# Pack an overlay-app raw binary into a .app blob: 64-byte header + code.
# Header layout mirrors app_overlay.h : app_header_t (little-endian, packed).
# The format/version constants (magic, header + ABI/API levels, overlay budget) are
# read straight from the C headers the firmware itself compiles, so there is a
# SINGLE source of truth - the packer can never silently drift from the loader.
# CRC-32 is zlib/PKZIP (init 0xFFFFFFFF, poly 0xEDB88320, final XOR) to match
# the firmware's mb_ext_image_crc32 / mb_crc32_bytes.
#
# ./pack_app.py breakout/breakout.bin breakout/breakout.app --name Breakout \
# --ver 1.0 --vma 0x20000280 --api-min 1
import argparse, os, re, struct, zlib, sys
HERE = os.path.dirname(os.path.abspath(__file__))
def cdefine(header: str, name: str) -> int:
"""Value of a `#define <name> <int-literal>` in a sibling C header.
Accepts a decimal or 0x-hex literal with optional u/U/l/L suffixes. Keeps the
blob format tied to the firmware's own headers (single source of truth)."""
pat = re.compile(r"^\s*#define\s+" + re.escape(name) +
r"\s+(0[xX][0-9a-fA-F]+|\d+)[uUlL]*\b")
with open(os.path.join(HERE, header)) as f:
for line in f:
m = pat.match(line)
if m:
return int(m.group(1), 0)
sys.exit(f"{header}: #define {name} not found")
# Single source of truth: the C headers the firmware also compiles.
MAGIC = cdefine("app_overlay.h", "APP_MAGIC").to_bytes(4, "little") # 0x31504146 -> b"FAP1"
HDR_VERSION = cdefine("app_overlay.h", "APP_HDR_VERSION")
ABI_MAJOR = cdefine("app_api.h", "APP_ABI_MAJOR")
API_LEVEL = cdefine("app_api.h", "APP_API_LEVEL")
OVERLAY_MAX = cdefine("app_overlay.h", "APP_OVERLAY_MAX") # 4 KiB overlay budget
FLAG_COMMITTED = cdefine("app_overlay.h", "APP_FLAG_COMMITTED")
FLAG_SCREEN_SAVER = cdefine("app_overlay.h", "APP_FLAG_SCREEN_SAVER")
FLAG_SHORTCUT_SHIFT = cdefine("app_overlay.h", "APP_FLAG_SHORTCUT_SHIFT")
SHORTCUTS = {
"none": 0,
"fm": cdefine("app_overlay.h", "APP_SHORTCUT_FM"),
"foxhunt": cdefine("app_overlay.h", "APP_SHORTCUT_FOXHUNT"),
"beacon": cdefine("app_overlay.h", "APP_SHORTCUT_BEACON"),
"beam": cdefine("app_overlay.h", "APP_SHORTCUT_BEAM"),
}
CAPABILITIES = {
"fm": cdefine("app_overlay.h", "APP_CAP_FM"),
"trivfo": cdefine("app_overlay.h", "APP_CAP_TRIVFO"),
"beam": cdefine("app_overlay.h", "APP_CAP_BEAM"),
}
def field(s: str, n: int) -> bytes:
b = s.encode("ascii", "strict")[: n - 1]
return b + b"\x00" * (n - len(b))
def main():
ap = argparse.ArgumentParser()
ap.add_argument("infile")
ap.add_argument("outfile")
ap.add_argument("--name", default="app")
ap.add_argument("--ver", default="1.0")
ap.add_argument("--entry", type=lambda x: int(x, 0), default=0)
ap.add_argument("--api-min", type=int, required=True,
help="minimum append-only firmware API level required by this app")
ap.add_argument("--vma", type=lambda x: int(x, 0), required=True,
help="RAM VMA the app was linked at (must match the firmware overlay)")
ap.add_argument("--screensaver", action="store_true",
help="allow the resident BLTime screen saver while this app is idle")
ap.add_argument("--shortcut", choices=SHORTCUTS, default="none",
help="resident quick action advertised by this app")
ap.add_argument("--require", action="append", choices=CAPABILITIES, default=[],
help="resident capability required by this app (repeatable)")
a = ap.parse_args()
if not 1 <= a.api_min <= API_LEVEL:
sys.exit(f"--api-min must be between 1 and current API level {API_LEVEL}")
code = open(a.infile, "rb").read()
if len(code) == 0:
sys.exit("empty input")
if len(code) > OVERLAY_MAX:
sys.exit(f"code {len(code)} B exceeds overlay budget {OVERLAY_MAX} B")
crc = zlib.crc32(code) & 0xFFFFFFFF
flags = (FLAG_COMMITTED |
(FLAG_SCREEN_SAVER if a.screensaver else 0) |
(SHORTCUTS[a.shortcut] << FLAG_SHORTCUT_SHIFT))
required_caps = 0
for capability in a.require:
required_caps |= CAPABILITIES[capability]
header = struct.pack(
"<4sHBBIIHH16s16sII4s",
MAGIC, HDR_VERSION, ABI_MAJOR, a.api_min,
len(code), crc, a.entry, flags,
field(a.name, 16), field(a.ver, 16), a.vma,
required_caps, b"\x00" * 4,
)
assert len(header) == 64, len(header)
with open(a.outfile, "wb") as f:
f.write(header)
f.write(code)
print(f"{a.outfile}: name={a.name!r} ver={a.ver!r} "
f"abi={ABI_MAJOR} api>={a.api_min} caps=0x{required_caps:08x} "
f"vma=0x{a.vma:08x} "
f"code={len(code)} B crc32=0x{crc:08x} -> blob {64 + len(code)} B")
if __name__ == "__main__":
main()
+43
View File
@@ -0,0 +1,43 @@
/* Overlay-app link script (POC).
*
* The blob is linked to run at the PY25Q16 sector-cache overlay VMA (shared with
* the multiboot RAM stub — never both active at once). Code + rodata + data +
* bss must all fit in the 4 KiB overlay. objcopy -O binary emits text+rodata+
* data only; the loader zeroes the overlay before copying, so bss starts clean.
*
* VMA must match ADDR(.mb_workspace) in Core/py32f071xb.ld. If the firmware RAM
* layout shifts, update APP_VMA and the loader's compile-time assert catches a
* mismatch. */
/* APP_VMA is the target firmware's __mb_workspace_start (read from its .map). It
* varies with the RAM layout, so pass it from the build: -Wl,--defsym,APP_VMA=0x...
* The default is only a fallback for a standalone size check. */
APP_VMA = DEFINED(APP_VMA) ? APP_VMA : 0x20000280;
APP_LENGTH = 0x1000; /* 4 KiB sector-cache overlay */
ENTRY(app_main)
MEMORY {
APP (rwx) : ORIGIN = APP_VMA, LENGTH = APP_LENGTH
}
SECTIONS {
.app APP_VMA : {
KEEP(*(.text.entry)) /* app_main pinned to offset 0 */
*(.text .text.*)
*(.rodata .rodata.*)
. = ALIGN(4);
*(.data .data.*)
. = ALIGN(4);
__app_bss_start = .;
*(.bss .bss.* COMMON)
. = ALIGN(4);
__app_bss_end = .;
} > APP
__app_end = .;
ASSERT(__app_end <= APP_VMA + APP_LENGTH,
"Plasma overlay app overflows the 4 KiB overlay")
/DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.eh_frame*) *(.comment) *(.note.*) }
}
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# Build one overlay-app blob (.app). Invoked by ../../../compile-app.sh inside
# the uvk1-uvk5v3 Docker image, run from this app's directory. Everything is
# derived from the directory name; only APP_NAME - the human label baked into
# the 64-byte blob header - is per-app. The .app file is named after it (spaces
# stripped), so "Broadcast FM" -> BroadcastFM.app.
set -euo pipefail
APP="$(basename "$PWD")" # breakout, foxhunt, beacon, fm, ...
APP_NAME="Plasma" # <-- the only per-app line
APP_VER="1.0"
APP_API_MIN=1
APP_VMA=${APP_VMA:-0x20000280} # pinned overlay VMA (Core/py32f071xb.ld)
OUT="${APP_NAME// /}" # blob basename ("Broadcast FM" -> BroadcastFM)
CC=/opt/toolchain/bin/arm-none-eabi-gcc
OBJCOPY=/opt/toolchain/bin/arm-none-eabi-objcopy
command -v arm-none-eabi-gcc >/dev/null 2>&1 && { CC=arm-none-eabi-gcc; OBJCOPY=arm-none-eabi-objcopy; }
CFLAGS="-mcpu=cortex-m0plus -mthumb -Os -std=gnu11 -ffreestanding -fno-builtin -fno-common \
-fomit-frame-pointer -ffunction-sections -fdata-sections -Wall -Wextra"
LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \
-Wl,--gc-sections -Wl,-Map=${APP}.map -Wl,--build-id=none -Wl,--no-warn-rwx-segments"
rm -f ./*.app ./*.elf ./*.bin # drop stale artifacts so discovery is unambiguous
step() { printf '\r 🔨 %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; }
trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
--name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" --screensaver >/dev/null
trap - ERR
BYTES=$(wc -c < "${APP}.bin")
if [ "$BYTES" -gt 4096 ]; then
printf '\r 🚨 %-13s OVERFLOWS 4 KiB (%d B) \n' "$APP_NAME" "$BYTES"; exit 1
fi
printf '\r ✅ %-13s %4d B (%d%% of 4 KiB) -> %s.app \n' \
"$APP_NAME" "$BYTES" "$(( BYTES * 100 / 4096 ))" "$OUT"
+180
View File
@@ -0,0 +1,180 @@
/* Copyright 2026 Armel F4HWN
* https://github.com/armel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Plasma — overlay app. A full-screen demoscene plasma on the 1-bit 128x64 LCD.
* The scalar field sums four scrolled sines: horizontal (x), vertical (y),
* diagonal (x+y) and a radial ripple around a slowly drifting centre. Because
* (x-cx)^2 + (y-cy)^2 is separable, the whole field is still built from per-column
* and per-row tables, so each of the 8192 pixels costs only a handful of adds.
* Rendered either with an ordered 4x4 Bayer dither (stipple) or as sweeping bands.
* Pure compute + framebuffer, no radio — it just works.
*
* Keys: UP/DOWN speed · 1-5 pattern · STAR stipple/bands · F auto-cycle ·
* MENU pause · EXIT quit.
*/
#include <stdint.h>
#include <stdbool.h>
#include "../app_api.h"
#define W 128
#define H 64
static const app_api_t *A;
/* 32 * sin(2*pi*i/256): four of these sum to -128..128. */
static const int8_t SIN[256] = {
0, 1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 9, 9, 10, 11, 12,
12, 13, 14, 14, 15, 16, 16, 17, 18, 18, 19, 20, 20, 21, 21, 22,
23, 23, 24, 24, 25, 25, 26, 26, 27, 27, 27, 28, 28, 29, 29, 29,
30, 30, 30, 30, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32,
32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 30, 30, 30,
30, 29, 29, 29, 28, 28, 27, 27, 27, 26, 26, 25, 25, 24, 24, 23,
23, 22, 21, 21, 20, 20, 19, 18, 18, 17, 16, 16, 15, 14, 14, 13,
12, 12, 11, 10, 9, 9, 8, 7, 6, 5, 5, 4, 3, 2, 2, 1,
0, -1, -2, -2, -3, -4, -5, -5, -6, -7, -8, -9, -9,-10,-11,-12,
-12,-13,-14,-14,-15,-16,-16,-17,-18,-18,-19,-20,-20,-21,-21,-22,
-23,-23,-24,-24,-25,-25,-26,-26,-27,-27,-27,-28,-28,-29,-29,-29,
-30,-30,-30,-30,-31,-31,-31,-31,-31,-32,-32,-32,-32,-32,-32,-32,
-32,-32,-32,-32,-32,-32,-32,-32,-31,-31,-31,-31,-31,-30,-30,-30,
-30,-29,-29,-29,-28,-28,-27,-27,-27,-26,-26,-25,-25,-24,-24,-23,
-23,-22,-21,-21,-20,-20,-19,-18,-18,-17,-16,-16,-15,-14,-14,-13,
-12,-12,-11,-10, -9, -9, -8, -7, -6, -5, -5, -4, -3, -2, -2, -1,
};
/* 4x4 ordered-dither matrix, flattened: idx = (y&3)*4 + (x&3), values 0..15. */
static const uint8_t BAYER[16] = { 0,8,2,10, 12,4,14,6, 3,11,1,9, 15,7,13,5 };
/* Pattern presets: {x scale, y scale, diagonal scale, radial ring shift}. */
#define NVAR 5
static const uint8_t VAR[NVAR][4] = {
{4,4,3,5}, {6,3,5,4}, {3,7,2,6}, {5,5,4,5}, {2,8,6,4},
};
/* Per-frame separable tables. */
static int8_t colA[W]; /* horizontal sine, by column x */
static int8_t rowA[H]; /* vertical sine, by row y */
static int8_t diagA[W + H]; /* diagonal sine, by (x + y) */
static uint16_t sqx[W]; /* (x - cx)^2 for the radial term */
static uint16_t sqy[H]; /* (y - cy)^2 */
static uint8_t prevKey;
__attribute__((section(".text.entry"), used))
void app_main(const app_api_t *api)
{
A = api;
A->backlight_on();
A->status_clear();
uint16_t t1 = 0, t2 = 0, t3 = 0, tr = 0, tc = 0, tc2 = 0;
uint16_t autoCtr = 0;
uint8_t speed = 3;
uint8_t var = 0;
bool bands = true;
bool autoc = false;
bool paused = false;
bool running = true;
prevKey = APP_KEY_INVALID;
while (running) {
uint8_t key = A->get_key();
if (key == APP_KEY_SAVER) {
prevKey = APP_KEY_INVALID;
A->delay_ms(10);
A->backlight_update();
continue;
}
if (key == APP_KEY_WAKE)
key = APP_KEY_INVALID;
if (key != prevKey && key != APP_KEY_INVALID) {
A->backlight_on();
switch (key) {
case APP_KEY_EXIT: running = false; break;
case APP_KEY_UP:
case APP_KEY_DOWN: {
const int8_t direction = A->nav_dir(key);
if (direction > 0 && speed < 8u) speed++;
if (direction < 0 && speed > 1u) speed--;
break;
}
case APP_KEY_MENU: paused = !paused; break;
case APP_KEY_STAR: bands = !bands; break;
case APP_KEY_F: autoc = !autoc; break;
case APP_KEY_1: case APP_KEY_2: case APP_KEY_3:
case APP_KEY_4: case APP_KEY_5:
var = (uint8_t)(key - APP_KEY_1); autoc = false; break;
default: break;
}
}
prevKey = key;
if (!running)
break;
const uint8_t sx = VAR[var][0], sy = VAR[var][1];
const uint8_t sd = VAR[var][2], rsh = VAR[var][3];
/* drifting radial centre (gentle Lissajous), then the separable squares */
const int cx = 64 + SIN[(uint8_t)tc]; /* 32..96 */
const int cy = 32 + (SIN[(uint8_t)tc2] >> 1);/* 16..48 */
for (uint8_t x = 0; x < W; x++) {
colA[x] = SIN[(uint8_t)(x * sx + t1)];
int dx = (int)x - cx; sqx[x] = (uint16_t)(dx * dx);
}
for (uint8_t y = 0; y < H; y++) {
rowA[y] = SIN[(uint8_t)(y * sy + t2)];
int dy = (int)y - cy; sqy[y] = (uint16_t)(dy * dy);
}
for (uint16_t s = 0; s < W + H; s++) diagA[s] = SIN[(uint8_t)(s * sd + t3)];
/* render all 8 pages: page 0 -> status line (top), pages 1..7 -> fb[0..6] */
for (uint8_t x = 0; x < W; x++) {
const int8_t cxv = colA[x];
const uint16_t sxv = sqx[x];
const uint8_t bx = x & 3u;
for (uint8_t p = 0; p < 8u; p++) {
uint8_t byte = 0;
for (uint8_t b = 0; b < 8u; b++) {
const uint8_t y = (uint8_t)(p * 8u + b);
int v = cxv + rowA[y] + diagA[x + y]
+ SIN[(uint8_t)(((sxv + sqy[y]) >> rsh) + tr)];
v += 128; /* 0..256 */
bool on;
if (bands) on = ((v >> 4) & 1u) != 0u; /* sweeping stripes */
else {
int lvl = v >> 4; if (lvl > 15) lvl = 15; /* 0..15 stipple */
on = lvl > (int)BAYER[((y & 3u) << 2) | bx];
}
if (on) byte = (uint8_t)(byte | (1u << b));
}
if (p == 0) A->status_line[x] = byte;
else A->fb[p - 1][x] = byte;
}
}
A->blit_status();
A->blit_full();
if (!paused) {
t1 += speed; t2 += (uint16_t)(speed + 1u); t3 += 1u;
tr += speed; tc += 1u; tc2 += 2u;
if (autoc && ++autoCtr >= 400u) { autoCtr = 0; var = (uint8_t)((var + 1u) % NVAR); }
}
A->backlight_update();
}
}
+30
View File
@@ -0,0 +1,30 @@
/* Tetris overlay app: code, constants, data and BSS share the 4 KiB workspace. */
APP_VMA = DEFINED(APP_VMA) ? APP_VMA : 0x20000280;
APP_LENGTH = 0x1000;
ENTRY(app_main)
MEMORY {
APP (rwx) : ORIGIN = APP_VMA, LENGTH = APP_LENGTH
}
SECTIONS {
.app APP_VMA : {
KEEP(*(.text.entry))
*(.text .text.*)
*(.rodata .rodata.*)
. = ALIGN(4);
*(.data .data.*)
. = ALIGN(4);
__app_bss_start = .;
*(.bss .bss.* COMMON)
. = ALIGN(4);
__app_bss_end = .;
} > APP
__app_end = .;
ASSERT(__app_end <= APP_VMA + APP_LENGTH,
"Tetris overlay app overflows the 4 KiB overlay")
/DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.eh_frame*) *(.comment) *(.note.*) }
}
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
set -euo pipefail
APP="$(basename "$PWD")"
APP_NAME="Tetris"
APP_VER="1.0"
APP_API_MIN=1
APP_VMA=${APP_VMA:-0x20000280}
OUT="${APP_NAME// /}"
CC=/opt/toolchain/bin/arm-none-eabi-gcc
OBJCOPY=/opt/toolchain/bin/arm-none-eabi-objcopy
command -v arm-none-eabi-gcc >/dev/null 2>&1 && { CC=arm-none-eabi-gcc; OBJCOPY=arm-none-eabi-objcopy; }
command -v "$CC" >/dev/null 2>&1 || { echo "❌ ARM compiler not found: $CC"; exit 1; }
command -v "$OBJCOPY" >/dev/null 2>&1 || { echo "❌ ARM objcopy not found: $OBJCOPY"; exit 1; }
CFLAGS="-mcpu=cortex-m0plus -mthumb -Os -std=gnu11 -ffreestanding -fno-builtin -fno-common \
-fomit-frame-pointer -ffunction-sections -fdata-sections -Wall -Wextra"
LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \
-Wl,--gc-sections -Wl,-Map=${APP}.map -Wl,--build-id=none -Wl,--no-warn-rwx-segments"
rm -f ./*.app ./*.elf ./*.bin
step() { printf '\r 🔨 %-13s [%d/3] %-8s' "$APP_NAME" "$1" "$2"; }
trap 'printf "\r ❌ %-13s build failed \n" "$APP_NAME"' ERR
step 1 compile ; "$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
step 2 objcopy ; "$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
step 3 pack ; python3 ../pack_app.py "${APP}.bin" "${OUT}.app" \
--name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" \
--vma "${APP_VMA}" --screensaver >/dev/null
trap - ERR
BYTES=$(wc -c < "${APP}.bin")
if [ "$BYTES" -gt 4096 ]; then
printf '\r 🚨 %-13s OVERFLOWS 4 KiB (%d B) \n' "$APP_NAME" "$BYTES"; exit 1
fi
printf '\r ✅ %-13s %4d B (%d%% of 4 KiB) -> %s.app \n' \
"$APP_NAME" "$BYTES" "$(( BYTES * 100 / 4096 ))" "$OUT"
+555
View File
@@ -0,0 +1,555 @@
/* Copyright 2026 Armel F4HWN
* https://github.com/armel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Tetris — compact overlay game for the 128x64 monochrome LCD.
*
* The 16x16 visible well fills the screen height down the left side; two hidden
* spawn rows sit just above it. Rather than solid walls, the well is framed by
* short corner brackets and a floor line. The narrow right-hand panel shows the
* next piece, score, cleared lines, level and persistent best score. Pieces
* come from a shuffled seven-piece bag; a dotted ghost previews the landing.
*
* Keys: LEFT/RIGHT or 4/6 move, MENU or 2 rotates, 8 soft-drops,
* STAR or 0 hard-drops, F pauses and EXIT quits.
*/
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include "../app_api.h"
#define W 128u
#define H 64u
#define COLS 16
#define VISIBLE_ROWS 16
#define HIDDEN_ROWS 2
#define ROWS (VISIBLE_ROWS + HIDDEN_ROWS)
#define CELL 4
#define WELL_X 3 /* physical x of board column 0 */
#define WELL_Y 0 /* physical y of visible row 0 (full height) */
#define WELL_L 1 /* container left rail (corner brackets) */
#define WELL_R 67 /* container right rail (corner brackets) */
#define WELL_B 63 /* solid floor line, physical y */
#define LABEL_X 72 /* left column shared by every panel label */
#define PREVIEW_CX 108 /* centre of the NEXT-piece preview area */
#define PREVIEW_CY 15
#define TICK_MS 20u
#define CFG_MAGIC 0x5445u
typedef struct {
uint16_t magic;
uint16_t version;
uint32_t best;
} config_t;
static const app_api_t *A;
/* GCC may lower aggregate clears to memset even for this freestanding blob. */
void *memset(void *dst, int value, size_t size)
{
uint8_t *p = dst;
while (size--)
*p++ = (uint8_t)value;
return dst;
}
/* Four 4x4 masks for I, O, T, S, Z, J and L. */
static const uint16_t MASK[7][4] = {
{0x00F0, 0x4444, 0x0F00, 0x2222},
{0x0066, 0x0066, 0x0066, 0x0066},
{0x0072, 0x0262, 0x0270, 0x0232},
{0x0036, 0x0462, 0x0360, 0x0231},
{0x0063, 0x0264, 0x0630, 0x0132},
{0x0071, 0x0226, 0x0470, 0x0322},
{0x0074, 0x0622, 0x0170, 0x0223},
};
static const uint16_t LINE_POINTS[5] = {0, 100, 300, 500, 800};
static const uint32_t DECIMAL_PLACE[6] = {100000u, 10000u, 1000u, 100u, 10u, 1u};
static uint16_t board[ROWS];
static uint8_t bag[7], bagPos;
static uint8_t piece, nextPiece, rotation;
static int8_t pieceX, pieceY;
static uint32_t randomState, score, best;
static uint16_t lines, gravityMs;
static uint8_t level;
static bool paused, gameOver, running, saverPaused, saverActive, bestDirty, newBest;
static uint8_t previousKey;
static uint16_t repeatMs;
static char text[9];
static void render(bool showPiece, uint32_t clearRows, uint8_t effect);
static void new_game(void);
/* -------------------------------------------------------------------------- */
/* RNG and the shuffled 7-piece bag */
/* -------------------------------------------------------------------------- */
static uint32_t random_next(void)
{
randomState = randomState * 1664525u + 1013904223u;
return randomState;
}
static void refill_bag(void)
{
for (uint8_t i = 0; i < 7u; i++)
bag[i] = i;
for (uint8_t i = 6u; i > 0u; i--) {
uint8_t j;
do {
j = (uint8_t)(random_next() >> 29);
} while (j > i);
const uint8_t t = bag[i]; bag[i] = bag[j]; bag[j] = t;
}
bagPos = 0;
}
static uint8_t take_piece(void)
{
if (bagPos >= 7u)
refill_bag();
return bag[bagPos++];
}
/* -------------------------------------------------------------------------- */
/* Framebuffer drawing: cells, pieces, side panel and dialogs */
/* -------------------------------------------------------------------------- */
static void set_pixel(int x, int y, bool on)
{
if ((unsigned)x >= W || (unsigned)y >= H)
return;
uint8_t *p;
if (y < 8)
p = &A->status_line[x];
else
p = &A->fb[(y >> 3) - 1][x];
const uint8_t bit = (uint8_t)(1u << (y & 7));
if (on) *p |= bit; else *p &= (uint8_t)~bit;
}
/* Short straight runs, in physical coordinates so they may cross the
* status-line / framebuffer seam that A->draw_line cannot. */
static void hline(int x0, int x1, int y)
{
for (int x = x0; x <= x1; x++) set_pixel(x, y, true);
}
static void vline(int x, int y0, int y1)
{
for (int y = y0; y <= y1; y++) set_pixel(x, y, true);
}
/* Corner brackets plus a solid floor — the well's frame, no vertical walls. */
static void container(void)
{
hline(WELL_L, WELL_R, WELL_B);
hline(WELL_L, WELL_L + 5, WELL_Y);
hline(WELL_R - 5, WELL_R, WELL_Y);
vline(WELL_L, WELL_Y, WELL_Y + 5);
vline(WELL_R, WELL_Y, WELL_Y + 5);
vline(WELL_L, WELL_B - 5, WELL_B);
vline(WELL_R, WELL_B - 5, WELL_B);
}
static void cell(int gx, int gy, uint8_t style)
{
const int visibleY = gy - HIDDEN_ROWS;
if ((unsigned)gx >= COLS || (unsigned)visibleY >= VISIBLE_ROWS)
return;
const int x = WELL_X + gx * CELL;
const int y = WELL_Y + visibleY * CELL;
if (style == 2u) {
set_pixel(x, y + 1, true);
set_pixel(x + 1, y, true);
set_pixel(x + 2, y + 1, true);
set_pixel(x + 1, y + 2, true);
} else {
const uint8_t size = style == 3u ? CELL : CELL - 1u;
for (uint8_t dy = 0; dy < size; dy++)
for (uint8_t dx = 0; dx < size; dx++)
set_pixel(x + dx, y + dy, true);
}
}
static bool collision(uint8_t type, uint8_t rot, int8_t px, int8_t py)
{
const uint16_t mask = MASK[type][rot & 3u];
for (uint8_t i = 0; i < 16u; i++) {
if (!(mask & (uint16_t)(1u << i)))
continue;
const int x = px + (i & 3u);
const int y = py + (i >> 2);
if (x < 0 || x >= COLS || y >= ROWS)
return true;
if (y >= 0 && (board[y] & (uint16_t)(1u << x)))
return true;
}
return false;
}
static void draw_piece(uint8_t type, uint8_t rot, int8_t px, int8_t py, uint8_t style)
{
const uint16_t mask = MASK[type][rot & 3u];
for (uint8_t i = 0; i < 16u; i++)
if (mask & (uint16_t)(1u << i))
cell(px + (i & 3u), py + (i >> 2), style);
}
static void number(uint32_t value, uint8_t width)
{
const uint8_t first = (uint8_t)(6u - width);
for (uint8_t i = 0; i < width; i++) {
const uint32_t place = DECIMAL_PLACE[first + i];
uint8_t digit = 0;
while (value >= place) {
value -= place;
digit++;
}
text[i] = (char)('0' + digit);
}
text[width] = 0;
}
static void preview(void)
{
const uint16_t mask = MASK[nextPiece][0];
const int ox = nextPiece < 2u ? PREVIEW_CX - 7 : PREVIEW_CX - 5;
const int oy = nextPiece == 0u ? PREVIEW_CY - 5 : PREVIEW_CY - 3;
for (uint8_t i = 0; i < 16u; i++) {
if (!(mask & (uint16_t)(1u << i)))
continue;
const int x = ox + (i & 3u) * CELL;
const int y = oy + (i >> 2) * CELL;
for (uint8_t dy = 0; dy < CELL - 1u; dy++)
for (uint8_t dx = 0; dx < CELL - 1u; dx++)
set_pixel(x + dx, y + dy, true);
}
}
/* One "LABEL value" row of the side panel; y is framebuffer-relative
* (physical y - 8), value right-aligned near the screen edge. */
static void stat(const char *label, uint8_t y, uint32_t value,
uint8_t valueX, uint8_t width)
{
A->print_tiny(label, LABEL_X, y, false, true);
number(value, width);
A->print_tiny(text, valueX, y, false, true);
}
static void panel(void)
{
A->print_tiny("NEXT", LABEL_X, 2, false, true);
preview();
if (gameOver)
A->print_inverse(newBest ? "NEW BEST!" : "GAME OVER", 82, 2, false, true, 118);
else if (paused)
A->print_inverse("PAUSE", 90, 2, false, true, 110);
stat("SCORE", 24, score > 999999u ? 999999u : score, 101, 6);
stat("LINES", 32, lines > 999u ? 999u : lines, 113, 3);
stat("LEVEL", 40, level, 117, 2);
stat("BEST", 48, best > 999999u ? 999999u : best, 101, 6);
}
/* effect 0 is normal, 1..8 is the line-clear sweep, 0xFF flashes a lock. */
static void render(bool showPiece, uint32_t clearRows, uint8_t effect)
{
A->display_clear();
A->status_clear();
A->print_inverse("TETRIS", 74, 0, true, true, 101);
container();
for (uint8_t y = 0; y < ROWS; y++)
for (uint8_t x = 0; x < COLS; x++)
if ((board[y] & (uint16_t)(1u << x)) && (!(clearRows & (1u << y)) ||
(x >= effect && x < COLS - effect)))
cell(x, y, 1u);
if (showPiece && !gameOver) {
int8_t ghostY = pieceY;
while (!collision(piece, rotation, pieceX, ghostY + 1))
ghostY++;
if (ghostY != pieceY)
draw_piece(piece, rotation, pieceX, ghostY, 2u);
draw_piece(piece, rotation, pieceX, pieceY, effect == 0xFFu ? 3u : 1u);
}
panel();
}
/* -------------------------------------------------------------------------- */
/* Board mechanics: scoring, line clears, spawn, lock and moves */
/* -------------------------------------------------------------------------- */
static void update_best(void)
{
if (score > best) {
best = score;
bestDirty = true;
newBest = true;
}
}
static uint8_t clear_lines(void)
{
uint32_t full = 0;
uint8_t count = 0;
for (uint8_t y = 0; y < ROWS; y++) {
if (board[y] == 0xFFFFu) { full |= 1u << y; count++; }
}
if (!count)
return 0;
A->led(true);
for (uint8_t step = 1u; step <= COLS / 2u; step++) {
render(false, full, step);
A->blit_status(); A->blit_full(); A->delay_ms(28);
}
A->led(false);
int8_t dst = ROWS - 1;
for (int8_t src = ROWS - 1; src >= 0; src--) {
if (full & (1u << src))
continue;
if (dst != src)
board[dst] = board[src];
dst--;
}
while (dst >= 0) {
board[dst] = 0;
dst--;
}
return count;
}
static void spawn_piece(void)
{
piece = nextPiece;
nextPiece = take_piece();
rotation = 0;
pieceX = COLS / 2 - 2;
pieceY = HIDDEN_ROWS - 1;
if (collision(piece, rotation, pieceX, pieceY)) {
gameOver = true;
update_best();
}
}
static void lock_piece(void)
{
render(true, 0, 0xFFu);
A->blit_status(); A->blit_full(); A->delay_ms(45);
const uint16_t mask = MASK[piece][rotation];
bool above = false;
for (uint8_t i = 0; i < 16u; i++) {
if (!(mask & (uint16_t)(1u << i)))
continue;
const int x = pieceX + (i & 3u);
const int y = pieceY + (i >> 2);
if (y < HIDDEN_ROWS) above = true;
else board[y] |= (uint16_t)(1u << x);
}
if (above) {
gameOver = true;
update_best();
return;
}
const uint8_t cleared = clear_lines();
lines = (uint16_t)(lines + cleared);
while (level < 15u && lines >= (uint16_t)level * 10u)
level++;
score += (uint32_t)LINE_POINTS[cleared] * level;
update_best();
spawn_piece();
}
static bool move_down(bool manual)
{
if (!collision(piece, rotation, pieceX, pieceY + 1)) {
pieceY++;
if (manual) { score++; update_best(); }
return true;
}
lock_piece();
return false;
}
static void rotate_piece(void)
{
const uint8_t next = (uint8_t)((rotation + 1u) & 3u);
static const int8_t kick[5] = {0, -1, 1, -2, 2};
for (uint8_t i = 0; i < 5u; i++)
if (!collision(piece, next, pieceX + kick[i], pieceY)) {
pieceX += kick[i]; rotation = next; return;
}
if (!collision(piece, next, pieceX, pieceY - 1)) {
pieceY--; rotation = next;
}
}
static void hard_drop(void)
{
uint8_t distance = 0;
while (!collision(piece, rotation, pieceX, pieceY + 1)) {
pieceY++; distance++;
}
score += (uint32_t)distance * 2u;
update_best();
lock_piece();
}
/* -------------------------------------------------------------------------- */
/* Input, screensaver handling and the main loop */
/* -------------------------------------------------------------------------- */
static void key_action(uint8_t key, bool repeat)
{
if (key == APP_KEY_EXIT) { running = false; return; }
if (gameOver) {
if (!repeat && (key == APP_KEY_MENU || key == APP_KEY_STAR || key == APP_KEY_0))
new_game();
return;
}
if (!repeat && key == APP_KEY_F) { paused = !paused; return; }
if (paused)
return;
int8_t direction = 0;
if (key == APP_KEY_4) direction = -1;
else if (key == APP_KEY_6) direction = 1;
else if (key == APP_KEY_UP || key == APP_KEY_DOWN) direction = A->nav_dir(key);
if (direction) {
if (!collision(piece, rotation, pieceX + direction, pieceY))
pieceX += direction;
return;
}
if (key == APP_KEY_8) { move_down(true); return; }
if (repeat)
return;
if (key == APP_KEY_MENU || key == APP_KEY_2) rotate_piece();
else if (key == APP_KEY_STAR || key == APP_KEY_0) hard_drop();
}
static void poll_key(void)
{
uint8_t key = A->get_key();
if (key == APP_KEY_SAVER) {
if (!paused && !gameOver) { paused = true; saverPaused = true; }
saverActive = true;
previousKey = APP_KEY_INVALID;
repeatMs = 0;
return;
}
if (key == APP_KEY_WAKE || key == APP_KEY_PTT) {
if (saverPaused) paused = false;
saverPaused = false;
saverActive = false;
key = APP_KEY_INVALID;
}
if (key == APP_KEY_INVALID) {
previousKey = key;
repeatMs = 0;
return;
}
if (key != previousKey) {
key_action(key, false);
repeatMs = 260u;
} else if (repeatMs > TICK_MS) {
repeatMs -= TICK_MS;
} else {
key_action(key, true);
repeatMs = 80u;
}
previousKey = key;
}
static void new_game(void)
{
for (uint8_t y = 0; y < ROWS; y++)
board[y] = 0;
score = 0;
lines = 0;
level = 1;
gravityMs = 0;
paused = false;
gameOver = false;
saverPaused = false;
saverActive = false;
newBest = false;
bagPos = 7;
nextPiece = take_piece();
spawn_piece();
}
__attribute__((section(".text.entry"), used))
void app_main(const app_api_t *api)
{
A = api;
config_t cfg;
A->cfg_load((uint8_t *)&cfg, sizeof(cfg));
best = cfg.magic == CFG_MAGIC && cfg.version == 1u ? cfg.best : 0u;
bestDirty = false;
randomState = ((uint32_t)A->bk_read(0x67u) << 16) ^ A->rx_freq() ^ best;
if (!randomState) randomState = 1;
previousKey = APP_KEY_INVALID;
repeatMs = 0;
running = true;
A->led(false);
A->backlight_on();
new_game();
while (running) {
poll_key();
if (!running)
break;
if (saverActive) {
A->backlight_update();
A->delay_ms(TICK_MS);
continue;
}
if (!paused && !gameOver) {
const uint16_t interval = level >= 15u ? 100u : (uint16_t)(900u - (level - 1u) * 55u);
gravityMs = (uint16_t)(gravityMs + TICK_MS);
if (gravityMs >= interval) {
gravityMs = 0;
move_down(false);
}
}
render(true, 0, 0);
A->blit_status();
A->blit_full();
A->backlight_update();
A->delay_ms(TICK_MS);
}
update_best();
if (bestDirty) {
cfg.magic = CFG_MAGIC;
cfg.version = 1u;
cfg.best = best;
A->cfg_save((const uint8_t *)&cfg, sizeof(cfg));
}
A->led(false);
}
+12
View File
@@ -0,0 +1,12 @@
APP_VMA = DEFINED(APP_VMA) ? APP_VMA : 0x20000280;
APP_LENGTH = 0x1000;
ENTRY(app_main)
MEMORY { APP (rwx) : ORIGIN = APP_VMA, LENGTH = APP_LENGTH }
SECTIONS {
.app APP_VMA : {
KEEP(*(.text.entry)) *(.text .text.*) *(.rodata .rodata.*)
. = ALIGN(4); *(.data .data.*) . = ALIGN(4); *(.bss .bss.* COMMON) . = ALIGN(4);
} > APP
ASSERT(. <= APP_VMA + APP_LENGTH, "Triple VFO overlay app overflows the 4 KiB overlay")
/DISCARD/ : { *(.ARM.exidx*) *(.ARM.extab*) *(.eh_frame*) *(.comment) *(.note.*) }
}
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail
APP="$(basename "$PWD")"
APP_NAME="Triple VFO"
APP_VER="1.0"
APP_API_MIN=1
APP_VMA=${APP_VMA:-0x20000280}
OUT="${APP_NAME// /}"
CC=/opt/toolchain/bin/arm-none-eabi-gcc
OBJCOPY=/opt/toolchain/bin/arm-none-eabi-objcopy
command -v arm-none-eabi-gcc >/dev/null 2>&1 && { CC=arm-none-eabi-gcc; OBJCOPY=arm-none-eabi-objcopy; }
CFLAGS="-mcpu=cortex-m0plus -mthumb -Os -std=gnu11 -ffreestanding -fno-builtin -fno-common -fomit-frame-pointer -ffunction-sections -fdata-sections -Wall -Wextra"
LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} -Wl,--gc-sections -Wl,-Map=${APP}.map -Wl,--build-id=none -Wl,--no-warn-rwx-segments"
rm -f ./*.app ./*.elf ./*.bin
"$CC" $CFLAGS $LDFLAGS "${APP}_app.c" -lgcc -o "${APP}.elf"
"$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
python3 ../pack_app.py "${APP}.bin" "${OUT}.app" --name "$APP_NAME" --ver "$APP_VER" --api-min "$APP_API_MIN" --vma "${APP_VMA}" --require trivfo --screensaver >/dev/null
BYTES=$(wc -c < "${APP}.bin")
test "$BYTES" -le 4096
printf ' ✅ %-13s %4d B (%d%% of 4 KiB) -> %s.app\n' \
"$APP_NAME" "$BYTES" "$(( BYTES * 100 / 4096 ))" "$OUT"
+266
View File
@@ -0,0 +1,266 @@
/* Copyright 2026 Armel F4HWN
* SPDX-License-Identifier: Apache-2.0
*
* Triple VFO — modal overlay receiver/transmitter. A and B are the resident
* Main Display VFOs; C is an app-persistent memory channel. The resident ABI
* performs all RF sequencing while this blob owns the seven-line Tiny UI.
*/
#include <stdint.h>
#include <stdbool.h>
#include "../app_api.h"
#define TICK_MS 20u
#define LONG_MS 400u
#define CFG_MAGIC 0xC3u
#define VFO_COUNT 3u
#define MR_MAX 1024u
#define STATUS_PTT_X 54u
static const app_api_t *A;
static app_trivfo_info_t vi[VFO_COUNT];
static uint8_t selected, state;
static uint16_t cChannel;
static bool running, fArm, txDenied, channelLabelOn, showFrequency;
static uint8_t batteryTicks;
static char text[16];
/* Resident status.c uses this exact 8-column inverted glyph at x=69. Keep
* the overlay copy byte-for-byte identical so F has the same position and
* polarity as every other firmware screen. */
static const uint8_t fontF[8] = {
0x7f, 0x00, 0x76, 0x76, 0x76, 0x76, 0x7e, 0x7f
};
/* Same two 2x6 glyphs and same x=54 slot as UI_DisplayStatus(). */
static const uint8_t fontPttOnePush[12] = {
0x00, 0x3e, 0x41, 0x41, 0x41, 0x3e,
0x00, 0x7f, 0x09, 0x09, 0x09, 0x06
};
static const uint8_t fontPttClassic[12] = {
0x00, 0x3e, 0x41, 0x41, 0x41, 0x22,
0x00, 0x7f, 0x40, 0x40, 0x40, 0x40
};
static uint8_t slen(const char *s){ uint8_t n=0; while(s[n])n++; return n; }
static bool copyName(char *d,const char *s){
uint8_t n=0;
while(n<10u){
uint8_t c=(uint8_t)s[n];
if(c==0u||c==0xffu) break;
d[n++]=(char)c;
}
while(n>0u&&d[n-1u]==' ') n--;
d[n]='\0';
return n>0u;
}
static char *put(char *o,const char *s){ while(*s)*o++=*s++; return o; }
static char *putu(char *o,uint32_t v){ char t[10]; int8_t n=0; do{t[n++]=(char)('0'+v%10u);v/=10u;}while(v); while(n--)*o++=t[n]; return o; }
static char *puti(char *o,int16_t v){ if(v<0){*o++='-';v=(int16_t)-v;} return putu(o,(uint16_t)v); }
static char *put3(char *o,uint16_t v){ *o++=(char)('0'+(v/100u)%10u); *o++=(char)('0'+(v/10u)%10u); *o++=(char)('0'+v%10u); return o; }
static char *put4(char *o,uint16_t v){ *o++=(char)('0'+(v/1000u)%10u); return put3(o,(uint16_t)(v%1000u)); }
static void formatFreq(char *s,uint32_t f){
char *o=putu(s,f/100000u); *o++='.'; f%=100000u;
uint32_t p=10000u; while(p){ *o++=(char)('0'+(f/p)%10u); p/=10u; } *o='\0';
}
static void formatChannel(char *s,const app_trivfo_info_t *v){
char *o=s;
if(v->channel<MR_MAX) o=put4(o,(uint16_t)(v->channel+1u));
else { *o++='F'; o=putu(o,(uint16_t)(v->channel-MR_MAX+1u)); }
*o='\0';
}
static void formatStep(char *s,uint16_t step){
char *o=putu(s,step/100u);
if(step%100u){ *o++='.'; *o++=(char)('0'+(step/10u)%10u); *o++=(char)('0'+step%10u); }
*o++='K'; *o='\0';
}
static void formatCode(char *s,const app_trivfo_info_t *v){
char *o=s;
if(v->code_type==1u){ o=putu(o,v->code_value/10u); *o++='.'; *o++=(char)('0'+v->code_value%10u); }
else if(v->code_type==2u||v->code_type==3u){
uint16_t n=v->code_value; *o++=(char)('0'+((n>>6)&7u)); *o++=(char)('0'+((n>>3)&7u)); *o++=(char)('0'+(n&7u)); *o++=(v->code_type==2u?'N':'I');
} else return formatStep(s,v->step);
*o='\0';
}
static void drawMeter(const app_trivfo_info_t *v){
int16_t dbm=v->rssi_dbm; if(dbm>-53)dbm=-53;
uint8_t s=0,over=0;
if(dbm>=-93){ s=9; over=(uint8_t)(dbm+93); if(over>40)over=40; }
/* dbm + 147 is non-negative in this branch. Keep the division unsigned:
* otherwise GCC pulls the ~460-byte signed division helper into the 4 KiB
* overlay even though no signed quotient is required. */
else if(dbm>=-141) s=(uint8_t)((uint16_t)(dbm+147)/6u);
char *o=text; if(dbm>-100)*o++=' '; o=puti(o,dbm); o=put(o," dBm"); *o='\0';
A->print_tiny(text,2,1,false,true);
if(over){ o=text; *o++='+'; if(over<10)*o++='0'; o=putu(o,over); }
else { o=text; *o++='S'; o=putu(o,s); }
*o='\0'; A->print_normal(text,38,0,0);
uint8_t level=(uint8_t)(s+over/10u); if(level>13u)level=13u;
for(uint8_t i=0;i<level;i++){
uint8_t *p=&A->fb[0][62u+i*5u];
p[0]=0x3e; p[1]=(i<9u)?0x3e:0x22; p[2]=(i<9u)?0x3e:0x22; p[3]=0x3e;
}
}
static void drawPttStatus(void){
const uint8_t *glyph=(vi[0].flags&APP_TRIVFO_PTT_ONEPUSH)?fontPttOnePush:fontPttClassic;
for(uint8_t i=0;i<12u;i++) A->status_line[STATUS_PTT_X+i]=glyph[i];
}
static void drawVfo(uint8_t n){
const app_trivfo_info_t *v=&vi[n];
uint8_t mainLine=(uint8_t)(n*2u+1u), techLine=(uint8_t)(mainLine+1u);
uint8_t techY=(uint8_t)(techLine*8u+1u);
formatChannel(text,v);
if(!(v->flags&APP_TRIVFO_RECEIVING)||channelLabelOn)
A->print_inverse(text,2,mainLine,false,true,(uint8_t)(slen(text)*4u+3u));
if(n==selected && txDenied)
A->print_tiny("TX DISABLE",22,(uint8_t)(mainLine*8u+1u),false,true);
else {
bool useName=false;
if(v->channel<MR_MAX&&!showFrequency) useName=copyName(text,v->name);
if(!useName) formatFreq(text,v->frequency);
if(n==selected)
A->print_bold(text,22,0,mainLine);
else
A->print_normal(text,22,0,mainLine);
}
const char *mod=v->modulation==0u?"FM":v->modulation==1u?"AM":v->modulation==2u?"USB":v->modulation==3u?"BYP":v->modulation==4u?"RAW":"?";
static const char *const power[7]={"LOW1","LOW2","LOW3","LOW4","LOW5","MID","HIGH"};
uint8_t p=(v->power>=1u&&v->power<=7u)?(uint8_t)(v->power-1u):0u;
if(v->flags&APP_TRIVFO_GUI_CLASSIC){
A->print_normal(mod,2,0,techLine);
if(v->modulation==0u&&v->code_type==1u) A->print_normal("CT",22,0,techLine);
else if(v->modulation==0u&&(v->code_type==2u||v->code_type==3u)) A->print_normal("DC",22,0,techLine);
static const char *const powerShort[7]={"L1","L2","L3","L4","L5","M","H"};
A->print_normal(powerShort[p],42,0,techLine);
if(v->flags&APP_TRIVFO_USER_POWER){
A->fb[techLine][38]=0x3e; A->fb[techLine][39]=0x1c; A->fb[techLine][40]=0x08;
}
if(v->offset_direction==1u) A->print_normal("+",60,0,techLine);
else if(v->offset_direction==2u) A->print_normal("-",60,0,techLine);
if(v->reverse) A->print_normal("R",68,0,techLine);
A->print_normal(v->bandwidth==0u?"W":v->bandwidth==1u?"N":"N+",80,0,techLine);
text[0]='S'; text[1]='Q'; text[2]='L'; text[3]=(char)('0'+(v->squelch%10u)); text[4]='\0';
A->print_normal(text,98,0,techLine);
} else {
A->print_tiny(mod,3,techY,false,true);
A->print_tiny(power[p],24,techY,false,true);
if(v->flags&APP_TRIVFO_USER_POWER){
A->fb[techLine][19]=0x3e; A->fb[techLine][20]=0x1c; A->fb[techLine][21]=0x08;
}
if(v->offset_direction==1u) A->print_normal("+",41,0,techLine);
else if(v->offset_direction==2u) A->print_normal("-",41,0,techLine);
if(v->reverse) A->print_tiny("R",51,techY,false,true);
if(v->code_type==1u){ A->print_tiny("CT",58,techY,false,true); formatCode(text,v); A->print_tiny(text,68,techY,false,true); }
else if(v->code_type==2u||v->code_type==3u){ A->print_tiny("DC",58,techY,false,true); formatCode(text,v); A->print_tiny(text,68,techY,false,true); }
else { formatStep(text,v->step); A->print_tiny(text,58,techY,false,true); }
A->print_tiny(v->bandwidth==0u?"WIDE":v->bandwidth==1u?"NAR":"NAR+",91,techY,false,true);
text[0]='S'; text[1]='Q'; text[2]='L'; text[3]=(char)('0'+(v->squelch%10u)); text[4]='\0';
A->print_tiny(text,110,techY,false,true);
}
}
static void draw(void){
for(uint8_t i=0;i<VFO_COUNT;i++) {
A->trivfo_get(i,&vi[i]);
/* The resident scanner can temporarily select the VFO carrying the
* reception. Mirror that selection locally so the bold main field
* follows it immediately. */
if(vi[i].flags&APP_TRIVFO_SELECTED) selected=i;
}
A->display_clear(); A->status_clear();
/* Keep the application title in the resident upper-left position. */
A->draw_battery();
A->print_inverse("TRIPLE VFO",2,0,true,true,42);
drawPttStatus();
if(fArm){ for(uint8_t i=0;i<8u;i++) A->status_line[69u+i]=fontF[i]; }
for(uint8_t i=0;i<VFO_COUNT;i++) drawVfo(i);
if(state==APP_TRIVFO_TX_STATE&&(vi[selected].flags&APP_TRIVFO_AUDIO_BAR))
A->audio_scope(0u,true);
else {
A->audio_scope(0u,false);
for(uint8_t i=0;i<VFO_COUNT;i++)
if(vi[i].flags&APP_TRIVFO_RECEIVING){ drawMeter(&vi[i]); break; }
}
A->blit_status(); A->blit_full();
}
static void selectNext(void){ selected=(uint8_t)((selected+1u)%VFO_COUNT); A->trivfo_select(selected); }
static void toggleNameFrequency(void){ showFrequency=!showFrequency; }
static void stepSelected(uint8_t key){ int8_t d=A->nav_dir(key); uint16_t ch=A->trivfo_step(selected,d); if(selected==2u&&ch!=0xFFFFu)cChannel=ch; }
static void loadCfg(void){ uint8_t c[4]; A->cfg_load(c,4); cChannel=(c[0]==CFG_MAGIC)?(uint16_t)(c[1]|((uint16_t)c[2]<<8)):0xFFFFu; }
static void saveCfg(void){ uint8_t c[3]={CFG_MAGIC,(uint8_t)cChannel,(uint8_t)(cChannel>>8)}; A->cfg_save(c,3); }
__attribute__((section(".text.entry"),used))
void app_main(const app_api_t *api){
A=api; running=true; fArm=false; txDenied=false; channelLabelOn=true; showFrequency=false; selected=batteryTicks=0; loadCfg();
cChannel=A->trivfo_enter(cChannel); A->backlight_on(); draw();
uint8_t held=APP_KEY_INVALID, blinkTicks=0; uint16_t heldMs=0; bool longDone=false, ptt=false;
while(running){
uint8_t key=A->get_key();
if(key==APP_KEY_SAVER){
state=A->trivfo_tick();
if(state==APP_TRIVFO_RX){
A->backlight_on();
draw();
}
for(uint8_t i=0;i<TICK_MS/10u;i++){
A->delay_ms(10); A->backlight_update();
}
continue;
}
if(key!=APP_KEY_INVALID&&key!=APP_KEY_WAKE) A->backlight_on();
if(key==APP_KEY_WAKE) key=APP_KEY_INVALID;
if(key==APP_KEY_PTT){
if(!ptt){ ptt=true; txDenied=A->trivfo_ptt(true)!=0; }
} else if(ptt){ ptt=false; A->trivfo_ptt(false); txDenied=false; }
if(key==APP_KEY_INVALID||key==APP_KEY_PTT){
if(held!=APP_KEY_INVALID&&!longDone){
if(held==APP_KEY_F) fArm=!fArm;
else if(held==APP_KEY_1&&fArm){ fArm=false; toggleNameFrequency(); }
else if(held==APP_KEY_2&&fArm){ fArm=false; selectNext(); }
else if(held==APP_KEY_EXIT) running=false;
}
held=APP_KEY_INVALID; heldMs=0; longDone=false;
} else if(key!=held){
held=key; heldMs=0; longDone=false;
if(key==APP_KEY_UP||key==APP_KEY_DOWN){ stepSelected(key); longDone=true; }
} else {
heldMs=(uint16_t)(heldMs+TICK_MS);
if((key==APP_KEY_UP||key==APP_KEY_DOWN)&&heldMs>=300u){ stepSelected(key); }
else if(!longDone&&heldMs>=LONG_MS){
longDone=true;
/* Long-1 toggles name/frequency and Long-2 selects the next
* VFO; both are deliberately independent of F. */
if(key==APP_KEY_1){ fArm=false; toggleNameFrequency(); }
else if(key==APP_KEY_2){ fArm=false; selectNext(); }
}
}
state=A->trivfo_tick();
if(state==APP_TRIVFO_RX||state==APP_TRIVFO_TX_STATE)
A->backlight_on();
if(state==APP_TRIVFO_RX){
if(++blinkTicks>=25u){ blinkTicks=0; channelLabelOn=!channelLabelOn; }
} else { blinkTicks=0; channelLabelOn=true; }
draw();
/* Match MAIN: no loaded-voltage samples during TX, then allow one
* second for the pack to recover before refreshing the four-sample
* battery average. */
if(state==APP_TRIVFO_TX_STATE) batteryTicks=0;
else if(++batteryTicks>=50u){ batteryTicks=0; A->battery_sample(); }
A->delay_ms(TICK_MS); A->backlight_update();
}
if(ptt)
A->trivfo_ptt(false);
saveCfg();
A->trivfo_leave();
}
+37 -105
View File
@@ -15,11 +15,11 @@
* limitations under the License.
*/
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
#include "app/fm.h"
#endif
#include "audio.h"
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
#include "driver/bk1080.h"
#endif
#include "driver/bk4819.h"
@@ -33,32 +33,32 @@
#include "settings.h"
#include "ui/ui.h"
static const uint16_t BEEP_Classic_array[][3] = { /* Tone Duration Repeats */
[BEEP_NONE] = {0, 0, 0 },
[BEEP_1KHZ_60MS_OPTIONAL] = {1000, 60, 1 },
[BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL] = {500, 60, 2 },
#ifdef ENABLE_DTMF_CALLING
[BEEP_880HZ_200MS] = {880, 200, 1 },
[BEEP_880HZ_500MS] = {880, 500, 1 },
#endif
#ifdef ENABLE_FEAT_F4HWN
[BEEP_400HZ_30MS] = {400, 30, 1 },
[BEEP_500HZ_30MS] = {500, 30, 1 },
[BEEP_600HZ_30MS] = {600, 30, 1 },
#endif
[BEEP_880HZ_60MS_TRIPLE_BEEP] = {880, 60, 3 }
};
BEEP_Type_t gBeepToPlay = BEEP_NONE;
static void AUDIO_PlayBeepPulse(void) {
BK4819_ExitTxMute();
SYSTEM_DelayMs(60);
BK4819_EnterTxMute();
SYSTEM_DelayMs(20);
}
void AUDIO_PlayBeep(BEEP_Type_t Beep)
{
if (Beep == BEEP_NONE)
return;
if (Beep != BEEP_880HZ_60MS_DOUBLE_BEEP &&
Beep != BEEP_500HZ_60MS_DOUBLE_BEEP &&
Beep != BEEP_440HZ_500MS &&
#ifdef ENABLE_DTMF_CALLING
Beep != BEEP_880HZ_200MS &&
Beep != BEEP_880HZ_500MS &&
#endif
#ifdef ENABLE_FEAT_F4HWN
Beep != BEEP_400HZ_30MS &&
Beep != BEEP_500HZ_30MS &&
Beep != BEEP_600HZ_30MS &&
#endif
!gEeprom.BEEP_CONTROL)
if ((Beep == BEEP_1KHZ_60MS_OPTIONAL ||
Beep == BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL) &&
!gEeprom.BEEP_CONTROL)
return;
if (gCurrentFunction == FUNCTION_RECEIVE)
@@ -67,7 +67,10 @@ void AUDIO_PlayBeep(BEEP_Type_t Beep)
if (gCurrentFunction == FUNCTION_MONITOR)
return;
#ifdef ENABLE_FMRADIO
if (Beep >= ARRAY_SIZE(BEEP_Classic_array))
return;
#ifdef ENABLE_FMRADIO_EMBEDDED
if (gFmRadioMode)
BK1080_Mute(true);
#endif
@@ -81,49 +84,14 @@ void AUDIO_PlayBeep(BEEP_Type_t Beep)
uint16_t ToneConfig = BK4819_ReadRegister(BK4819_REG_71);
uint16_t ToneFrequency;
switch (Beep)
{
default:
case BEEP_NONE:
ToneFrequency = 220;
break;
case BEEP_1KHZ_60MS_OPTIONAL:
ToneFrequency = 1000;
break;
case BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL:
case BEEP_500HZ_60MS_DOUBLE_BEEP:
ToneFrequency = 500;
break;
case BEEP_440HZ_500MS:
ToneFrequency = 440;
break;
case BEEP_880HZ_60MS_DOUBLE_BEEP:
#ifndef ENABLE_FEAT_F4HWN
case BEEP_880HZ_200MS:
case BEEP_880HZ_500MS:
#endif
ToneFrequency = 880;
break;
#ifdef ENABLE_FEAT_F4HWN
case BEEP_400HZ_30MS:
ToneFrequency = 400;
break;
case BEEP_500HZ_30MS:
ToneFrequency = 500;
break;
case BEEP_600HZ_30MS:
ToneFrequency = 600;
break;
#endif
}
if(Beep == BEEP_400HZ_30MS || Beep == BEEP_500HZ_30MS || Beep == BEEP_600HZ_30MS)
{
BK4819_WriteRegister(BK4819_REG_70, BK4819_REG_70_ENABLE_TONE1 | ((1 & 0x7f) << BK4819_REG_70_SHIFT_TONE1_TUNING_GAIN));
}
#endif
BK4819_PlayTone(ToneFrequency, true);
BK4819_PrepareToPlayTone(true);
SYSTEM_DelayMs(2);
@@ -131,46 +99,12 @@ void AUDIO_PlayBeep(BEEP_Type_t Beep)
SYSTEM_DelayMs(60);
uint16_t Duration;
switch (Beep)
{
case BEEP_880HZ_60MS_DOUBLE_BEEP:
AUDIO_PlayBeepPulse();
[[fallthrough]];
case BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL:
case BEEP_500HZ_60MS_DOUBLE_BEEP:
AUDIO_PlayBeepPulse();
[[fallthrough]];
case BEEP_1KHZ_60MS_OPTIONAL:
BK4819_ExitTxMute();
Duration = 60;
break;
#ifdef ENABLE_FEAT_F4HWN
case BEEP_400HZ_30MS:
case BEEP_500HZ_30MS:
case BEEP_600HZ_30MS:
BK4819_ExitTxMute();
Duration = 30;
break;
#endif
case BEEP_440HZ_500MS:
#ifndef ENABLE_FEAT_F4HWN
case BEEP_880HZ_200MS:
BK4819_ExitTxMute();
Duration = 200;
break;
case BEEP_880HZ_500MS:
#endif
default:
BK4819_ExitTxMute();
Duration = 500;
break;
for (uint8_t i = 0; i < BEEP_Classic_array[Beep][BEEP_REPEATS]; i++) {
BK4819_PlayToneRaw( BEEP_Classic_array[Beep][BEEP_TONE],
BEEP_Classic_array[Beep][BEEP_DURATION]);
SYSTEM_DelayMs(20);
}
SYSTEM_DelayMs(Duration);
BK4819_EnterTxMute();
SYSTEM_DelayMs(20);
AUDIO_AudioPathOff();
SYSTEM_DelayMs(5);
@@ -178,18 +112,17 @@ 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)
SYSTEM_DelayMs(10);
#endif
if (gEnableSpeaker)
AUDIO_AudioPathOn();
#ifdef ENABLE_FMRADIO
#ifdef ENABLE_FMRADIO_EMBEDDED
if (isFmRadio)
BK1080_Mute(false);
#endif
@@ -200,7 +133,6 @@ void AUDIO_PlayBeep(BEEP_Type_t Beep)
#ifdef ENABLE_VOX
gVoxResumeCountdown = 80;
#endif
}
#ifdef ENABLE_VOICE
@@ -343,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
@@ -367,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
@@ -508,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
+8 -3
View File
@@ -22,23 +22,28 @@
#include "driver/gpio.h"
enum {
BEEP_TONE = 0,
BEEP_DURATION,
BEEP_REPEATS
};
enum BEEP_Type_t
{
BEEP_NONE = 0,
BEEP_1KHZ_60MS_OPTIONAL,
BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL,
BEEP_440HZ_500MS,
#ifdef ENABLE_DTMF_CALLING
BEEP_880HZ_200MS,
BEEP_880HZ_500MS,
#endif
BEEP_500HZ_60MS_DOUBLE_BEEP,
#ifdef ENABLE_FEAT_F4HWN
BEEP_400HZ_30MS,
BEEP_500HZ_30MS,
BEEP_600HZ_30MS,
#endif
BEEP_880HZ_60MS_DOUBLE_BEEP
BEEP_880HZ_60MS_TRIPLE_BEEP
};
typedef enum BEEP_Type_t BEEP_Type_t;
+153 -8
View File
@@ -22,16 +22,17 @@ const uint8_t gFontPttClassic[2][6] =
{0x00, 0x7f, 0x40, 0x40, 0x40, 0x40},
};
const uint8_t gFontF[8] =
const uint8_t gFontF[9] =
{
0b00111110,
0b01111111,
0b00000000,
0b01110110,
0b01110110,
0b01110110,
0b01110110,
0b01111110,
0b01111111
0b01000001,
0b01110101,
0b01110101,
0b01110101,
0b01111101,
0b01111111,
0b00111110
};
const uint8_t gFontS[6] =
@@ -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
View File
@@ -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
View File
@@ -71,9 +71,18 @@ void BOARD_GPIO_Init(void)
LL_GPIO_InitTypeDef InitStruct;
LL_GPIO_StructInit(&InitStruct);
InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
InitStruct.Pull = LL_GPIO_PULL_UP;
InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
/* Keep the backlight dark from the first possible GPIO write. Its pin can
* otherwise float while the rest of the board GPIOs are being configured. */
LL_GPIO_ResetOutputPin(GPIOF, LL_GPIO_PIN_8);
InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
InitStruct.Pull = LL_GPIO_PULL_NO;
InitStruct.Pin = LL_GPIO_PIN_8;
LL_GPIO_Init(GPIOF, &InitStruct);
InitStruct.Pull = LL_GPIO_PULL_UP;
// ---------------------
// Input pins
@@ -123,9 +132,8 @@ void BOARD_GPIO_Init(void)
LL_GPIO_Init(GPIOF, &InitStruct);
#endif
// Backlight: PF8
// BK4819 CS: PF9
InitStruct.Pin = LL_GPIO_PIN_9 | LL_GPIO_PIN_8 ;
InitStruct.Pin = LL_GPIO_PIN_9;
LL_GPIO_Init(GPIOF, &InitStruct);
#ifndef ENABLE_SWD
@@ -177,13 +185,14 @@ void BOARD_ADC_GetBatteryInfo(uint16_t *pVoltage, uint16_t *pCurrent)
void BOARD_Init(void)
{
BOARD_GPIO_Init();
/* Blank and initialise the LCD as soon as its GPIOs are available. */
ST7565_Init();
BACKLIGHT_InitHardware();
BOARD_ADC_Init();
#ifdef ENABLE_VOICE
VOICE_Init();
#endif
PY25Q16_Init();
ST7565_Init();
#ifdef ENABLE_FMRADIO
BK1080_Init0();
#endif
+51
View File
@@ -14,6 +14,7 @@
* limitations under the License.
*/
#include <stddef.h>
#include "dcs.h"
#include "misc.h"
@@ -26,6 +27,12 @@ const uint16_t CTCSS_Options[50] = {
2035, 2065, 2107, 2181, 2257, 2291, 2336, 2418, 2503, 2541
};
// Indices in CTCSS_Options for the 12 non-homologated tones.
static const uint8_t CTCSS_ExtraIdx[12] = {
1, 26, 28, 30, 32, 34, 36, 38, 39, 41,
45, 49
};
const uint16_t DCS_Options[104] = {
0x0013, 0x0015, 0x0016, 0x0019, 0x001A, 0x001E, 0x0023, 0x0027,
0x0029, 0x002B, 0x002C, 0x0035, 0x0039, 0x003A, 0x003B, 0x003C,
@@ -42,6 +49,13 @@ const uint16_t DCS_Options[104] = {
0x01C3, 0x01CA, 0x01D3, 0x01D9, 0x01DA, 0x01DC, 0x01E3, 0x01EC,
};
// Indices in DCS_Options for the 21 non-PMR446 DCS codes.
static const uint8_t DCS_ExtraIdx[21] = {
5, 9, 19, 25, 34, 36, 41, 43, 44, 48,
50, 54, 56, 60, 71, 72, 73, 74, 75, 82,
83
};
static uint32_t DCS_CalculateGolay(uint32_t CodeWord)
{
unsigned int i;
@@ -108,3 +122,40 @@ uint8_t DCS_GetCtcssCode(int Code)
return Result;
}
static uint8_t DCS_GetApprovedIndex(uint8_t Option, size_t options_size, size_t extraidx_size, const uint8_t *extraidx)
{
unsigned int i;
unsigned int extra_pos = 0;
uint8_t approved_index = 0;
if (Option >= options_size)
return 0xFF;
for (i = 0; i < options_size; i++) {
const bool is_extra = extra_pos < extraidx_size &&
i == extraidx[extra_pos];
if (is_extra) {
extra_pos++;
continue;
}
if (i == Option)
return approved_index;
approved_index++;
}
return 0xFF;
}
uint8_t DCS_GetCtcssApprovedIndex(uint8_t Option)
{
return DCS_GetApprovedIndex(Option, ARRAY_SIZE(CTCSS_Options), ARRAY_SIZE(CTCSS_ExtraIdx), CTCSS_ExtraIdx);
}
uint8_t DCS_GetDcsApprovedIndex(uint8_t Option)
{
return DCS_GetApprovedIndex(Option, ARRAY_SIZE(DCS_Options), ARRAY_SIZE(DCS_ExtraIdx), DCS_ExtraIdx);
}
+2 -1
View File
@@ -40,6 +40,7 @@ extern const uint16_t DCS_Options[104];
uint32_t DCS_GetGolayCodeWord(DCS_CodeType_t CodeType, uint8_t Option);
uint8_t DCS_GetCdcssCode(uint32_t Code);
uint8_t DCS_GetCtcssCode(int Code);
uint8_t DCS_GetCtcssApprovedIndex(uint8_t Option);
uint8_t DCS_GetDcsApprovedIndex(uint8_t Option);
#endif
-1
View File
@@ -7,7 +7,6 @@
#include "driver/bk4819.h"
#include "string.h"
#include "external/printf/printf.h"
#include "am_fix.h"
static inline void LogUart(const char *const str)
{
+63 -18
View File
@@ -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
@@ -107,10 +122,15 @@ void BACKLIGHT_InitHardware()
static void BACKLIGHT_Sound(void)
{
if (gEeprom.POWER_ON_DISPLAY_MODE == POWER_ON_DISPLAY_MODE_SOUND || gEeprom.POWER_ON_DISPLAY_MODE == POWER_ON_DISPLAY_MODE_ALL)
if (gEeprom.POWER_ON_DISPLAY_MODE == POWER_ON_DISPLAY_MODE_SOUND ||
gEeprom.POWER_ON_DISPLAY_MODE == POWER_ON_DISPLAY_MODE_ALL
#ifdef ENABLE_FEAT_F4HWN_LOGO
|| gEeprom.POWER_ON_DISPLAY_MODE == POWER_ON_DISPLAY_MODE_LOGO
#endif
)
{
AUDIO_PlayBeep(BEEP_880HZ_60MS_DOUBLE_BEEP);
AUDIO_PlayBeep(BEEP_880HZ_60MS_DOUBLE_BEEP);
AUDIO_PlayBeep(BEEP_880HZ_60MS_TRIPLE_BEEP);
AUDIO_PlayBeep(BEEP_880HZ_60MS_TRIPLE_BEEP);
}
gK5startup = false;
@@ -123,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
@@ -156,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
}
@@ -177,18 +233,7 @@ void BACKLIGHT_TurnOn(void)
void BACKLIGHT_TurnOff()
{
#ifdef ENABLE_BLMIN_TMP_OFF
register uint8_t tmp;
if (gEeprom.BACKLIGHT_MIN_STAT == BLMIN_STAT_ON)
tmp = gEeprom.BACKLIGHT_MIN;
else
tmp = 0;
BACKLIGHT_SetBrightness(tmp);
#else
BACKLIGHT_SetBrightness(gEeprom.BACKLIGHT_MIN);
#endif
gBacklightCountdown_500ms = 0;
backlightOn = false;
}
-8
View File
@@ -31,14 +31,6 @@ extern uint8_t gBacklightBrightness;
extern uint16_t gSleepModeCountdown_500ms;
#endif
#ifdef ENABLE_BLMIN_TMP_OFF
typedef enum {
BLMIN_STAT_ON,
BLMIN_STAT_OFF,
BLMIN_STAT_UNKNOWN
} BLMIN_STAT_t;
#endif
void BACKLIGHT_InitHardware();
void BACKLIGHT_UpdateTickless(void);
void BACKLIGHT_TurnOn();
+22 -6
View File
@@ -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();
}
+7 -4
View File
@@ -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);
@@ -105,13 +106,15 @@ void BK4819_SetCompander(const unsigned int mode);
void BK4819_DisableVox(void);
void BK4819_DisableDTMF(void);
void BK4819_EnableDTMF(void);
void BK4819_PrepareToPlayTone(bool bTuningGainSwitch);
void BK4819_PlayTone(uint16_t Frequency, bool bTuningGainSwitch);
void BK4819_PlayToneRaw(const unsigned int tone_Hz, const unsigned int delay);
void BK4819_PlaySingleTone(const unsigned int tone_Hz, const unsigned int delay, const unsigned int level, const bool play_speaker);
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);
@@ -147,11 +150,11 @@ uint8_t BK4819_GetGlitchIndicator(void);
uint8_t BK4819_GetExNoiceIndicator(void);
uint16_t BK4819_GetVoiceAmplitudeOut(void);
uint8_t BK4819_GetAfTxRx(void);
void BK4819_SetRxAudioGain(void);
bool BK4819_GetFrequencyScanResult(uint32_t *pFrequency);
BK4819_CssScanResult_t BK4819_GetCxCSSScanResult(uint32_t *pCdcssFreq, uint16_t *pCtcssFreq);
void BK4819_DisableFrequencyScan(void);
void BK4819_EnableFrequencyScan(void);
void BK4819_SetFrequencyScan(bool enable);
void BK4819_SetScanFrequency(uint32_t Frequency);
void BK4819_Disable(void);
@@ -167,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);
+109 -88
View File
@@ -39,6 +39,16 @@ static const uint16_t FSK_RogerTable[7] = {0xF1A2, 0x7446, 0x61A4, 0x6544, 0x4E8
static uint16_t gBK4819_GpioOutState;
#define SHORT_DELAY() \
__asm volatile("nop\n nop\n nop\n nop\n nop\n" \
"nop\n nop\n nop\n nop\n nop\n" \
"nop\n nop\n nop\n nop\n nop\n" \
"nop\n nop\n nop\n nop\n nop\n" \
"nop\n nop\n nop\n nop\n nop\n" \
"nop\n nop\n nop\n nop\n nop\n" \
"nop\n nop\n nop\n nop\n nop\n" \
"nop\n nop\n nop\n nop\n nop\n")
bool gRxIdleMode;
static inline void CS_Assert()
@@ -197,63 +207,78 @@ static uint16_t BK4819_ReadU16(void)
uint16_t Value;
SDA_SetDir(false);
SYSTICK_DelayUs(1);
SHORT_DELAY();
Value = 0;
for (i = 0; i < 16; i++)
{
Value <<= 1;
Value |= SDA_ReadInput();
SCL_Set();
SYSTICK_DelayUs(1);
SHORT_DELAY();
SCL_Reset();
SYSTICK_DelayUs(1);
SHORT_DELAY();
}
SDA_SetDir(true);
return Value;
}
static uint16_t reg_30_cache = 0xFFFF;
static uint16_t reg_47_cache = 0xFFFF;
uint16_t BK4819_ReadRegister(BK4819_REGISTER_t Register)
{
uint16_t Value;
CS_Release();
SCL_Reset();
SYSTICK_DelayUs(1);
SHORT_DELAY();
CS_Assert();
BK4819_WriteU8(Register | 0x80);
Value = BK4819_ReadU16();
CS_Release();
SYSTICK_DelayUs(1);
SHORT_DELAY();
SCL_Set();
SDA_Set();
if (Register == BK4819_REG_30)
reg_30_cache = Value;
else if (Register == BK4819_REG_47)
reg_47_cache = Value;
return Value;
}
void BK4819_WriteRegister(BK4819_REGISTER_t Register, uint16_t Data)
{
if (Register == BK4819_REG_30)
{
if (Data == reg_30_cache)
return;
reg_30_cache = Data;
}
else if (Register == BK4819_REG_47)
{
if (Data == reg_47_cache)
return;
reg_47_cache = Data;
}
CS_Release();
SCL_Reset();
SYSTICK_DelayUs(1);
SHORT_DELAY();
CS_Assert();
BK4819_WriteU8(Register);
SYSTICK_DelayUs(1);
SHORT_DELAY();
BK4819_WriteU16(Data);
SYSTICK_DelayUs(1);
SHORT_DELAY();
CS_Release();
SYSTICK_DelayUs(1);
SHORT_DELAY();
SCL_Set();
SDA_Set();
@@ -271,14 +296,14 @@ void BK4819_WriteU8(uint8_t Data)
else
SDA_Set();
SYSTICK_DelayUs(1);
SHORT_DELAY();
SCL_Set();
SYSTICK_DelayUs(1);
SHORT_DELAY();
Data <<= 1;
SCL_Reset();
SYSTICK_DelayUs(1);
SHORT_DELAY();
}
}
@@ -294,14 +319,14 @@ void BK4819_WriteU16(uint16_t Data)
else
SDA_Set();
SYSTICK_DelayUs(1);
SHORT_DELAY();
SCL_Set();
Data <<= 1;
SYSTICK_DelayUs(1);
SHORT_DELAY();
SCL_Reset();
SYSTICK_DelayUs(1);
SHORT_DELAY();
}
}
@@ -416,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
@@ -671,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
@@ -1006,7 +1036,7 @@ void BK4819_EnableDTMF(void)
(15u << BK4819_REG_24_SHIFT_MAX_SYMBOLS)); // 0 ~ 15
}
void BK4819_PlayTone(uint16_t Frequency, bool bTuningGainSwitch)
void BK4819_PrepareToPlayTone(bool bTuningGainSwitch)
{
uint16_t ToneConfig = BK4819_REG_70_ENABLE_TONE1;
@@ -1021,10 +1051,23 @@ void BK4819_PlayTone(uint16_t Frequency, bool bTuningGainSwitch)
BK4819_WriteRegister(BK4819_REG_30, 0);
BK4819_WriteRegister(BK4819_REG_30, BK4819_REG_30_ENABLE_AF_DAC | BK4819_REG_30_ENABLE_DISC_MODE | BK4819_REG_30_ENABLE_TX_DSP);
}
void BK4819_PlayTone(uint16_t Frequency, bool bTuningGainSwitch)
{
BK4819_PrepareToPlayTone(bTuningGainSwitch);
BK4819_WriteRegister(BK4819_REG_71, scale_freq(Frequency));
}
void BK4819_PlayToneRaw(const unsigned int tone_Hz, const unsigned int delay) {
BK4819_WriteRegister(BK4819_REG_71, scale_freq(tone_Hz));
BK4819_ExitTxMute();
SYSTEM_DelayMs(delay);
BK4819_EnterTxMute();
}
// level 0 ~ 127
void BK4819_PlaySingleTone(const unsigned int tone_Hz, const unsigned int delay, const unsigned int level, const bool play_speaker)
{
@@ -1044,11 +1087,7 @@ void BK4819_PlaySingleTone(const unsigned int tone_Hz, const unsigned int delay,
BK4819_EnableTXLink();
SYSTEM_DelayMs(50);
BK4819_WriteRegister(BK4819_REG_71, scale_freq(tone_Hz));
BK4819_ExitTxMute();
SYSTEM_DelayMs(delay);
BK4819_EnterTxMute();
BK4819_PlayToneRaw(tone_Hz, delay);
if (play_speaker)
{
@@ -1094,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
@@ -1432,24 +1471,15 @@ void BK4819_GenTail(uint8_t Tail)
// freq(Hz) * 20.64888 for XTAL 13M/26M or
// freq(Hz)*20.97152 for XTAL 12.8M/19.2M/25.6M/38.4M
switch (Tail)
{
case 0: // 134.4Hz CTCSS Tail
BK4819_WriteRegister(BK4819_REG_52, 0x828F); // 1 00 0 001010 001111
break;
case 1: // 120° phase shift
BK4819_WriteRegister(BK4819_REG_52, 0xA28F); // 1 01 0 001010 001111
break;
case 2: // 180° phase shift
BK4819_WriteRegister(BK4819_REG_52, 0xC28F); // 1 10 0 001010 001111
break;
case 3: // 240° phase shift
BK4819_WriteRegister(BK4819_REG_52, 0xE28F); // 1 11 0 001010 001111
break;
case 4: // 55Hz tone freq
BK4819_WriteRegister(BK4819_REG_07, 0x046f); // 0 00 0 010001 101111
break;
}
if (Tail <= 3)
// 0: 134.4Hz CTCSS Tail
// 1: 120° phase shift
// 2: 180° phase shift
// 3: 240° phase shift
BK4819_WriteRegister(BK4819_REG_52, 0x028F | (Tail << 13));
else if (Tail == 4)
// 4: 55Hz tone freq
BK4819_WriteRegister(BK4819_REG_07, 0x046F);
}
void BK4819_PlayCDCSSTail(void)
@@ -1531,6 +1561,14 @@ uint8_t BK4819_GetAfTxRx(void)
return BK4819_ReadRegister(BK4819_REG_6F) & 0x003F;
}
void BK4819_SetRxAudioGain(void) {
BK4819_WriteRegister(BK4819_REG_48,
(11u << 12) | // ??? .. 0 ~ 15, doesn't seem to make any difference
( 0u << 10) | // AF Rx Gain-1
(gEeprom.VOLUME_GAIN << 4) | // AF Rx Gain-2
(gEeprom.DAC_GAIN << 0)); // AF DAC Gain (after Gain-1 and Gain-2)
}
bool BK4819_GetFrequencyScanResult(uint32_t *pFrequency)
{
const uint16_t High = BK4819_ReadRegister(BK4819_REG_0D);
@@ -1566,7 +1604,7 @@ BK4819_CssScanResult_t BK4819_GetCxCSSScanResult(uint32_t *pCdcssFreq, uint16_t
return BK4819_CSS_RESULT_NOT_FOUND;
}
void BK4819_DisableFrequencyScan(void)
void BK4819_SetFrequencyScan(bool enable)
{
// REG_32
//
@@ -1582,32 +1620,10 @@ void BK4819_DisableFrequencyScan(void)
// 1 = enable
// 0 = disable
//
BK4819_WriteRegister(BK4819_REG_32, // 0x0244); // 00 0000100100010 0
BK4819_WriteRegister(BK4819_REG_32,
( 0u << 14) | // 0 frequency scan Time
(290u << 1) | // ???
( 0u << 0)); // 0 frequency scan enable
}
void BK4819_EnableFrequencyScan(void)
{
// REG_32
//
// <15:14> 0 frequency scan time
// 0 = 0.2 sec
// 1 = 0.4 sec
// 2 = 0.8 sec
// 3 = 1.6 sec
//
// <13:1> ???
//
// <0> 0 frequency scan enable
// 1 = enable
// 0 = disable
//
BK4819_WriteRegister(BK4819_REG_32, // 0x0245); // 00 0000100100010 1
( 0u << 14) | // 0 frequency scan time
(290u << 1) | // ???
( 1u << 0)); // 1 frequency scan enable
(enable ? 1u : 0u)); // frequency scan (1: enable | 0: disable)
}
void BK4819_SetScanFrequency(uint32_t Frequency)
@@ -1671,7 +1687,7 @@ void BK4819_Disable(void)
void BK4819_StopScan(void)
{
BK4819_DisableFrequencyScan();
BK4819_SetFrequencyScan(false);
BK4819_Disable();
}
@@ -1743,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;
@@ -1758,26 +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_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);
BK4819_EnterTxMute();
BK4819_WriteRegister(BK4819_REG_71, scale_freq(tone2_Hz));
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
}
@@ -1830,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
View File
@@ -39,25 +39,30 @@ void EEPROM_ReadBuffer(uint16_t Address, void *pBuffer, uint8_t Size)
I2C_Stop();
}
void EEPROM_WriteBuffer(uint16_t Address, const void *pBuffer)
void EEPROM_WriteBuffer(uint16_t Address, const void *pBuffer, uint8_t Size)
{
if (pBuffer == NULL || Address >= 0x2000)
if (pBuffer == NULL)
return;
uint8_t buffer[8];
EEPROM_ReadBuffer(Address, buffer, 8);
if (memcmp(pBuffer, buffer, 8) == 0) {
return;
while (Size >= sizeof(buffer) && Address < 0x2000)
{
EEPROM_ReadBuffer(Address, buffer, sizeof(buffer));
if (memcmp(pBuffer, buffer, sizeof(buffer)) != 0)
{
I2C_Start();
I2C_Write(0xA0);
I2C_Write((Address >> 8) & 0xFF);
I2C_Write((Address >> 0) & 0xFF);
I2C_WriteBuffer(pBuffer, sizeof(buffer));
I2C_Stop();
// give the EEPROM time to burn the data in (apparently takes 5ms)
SYSTEM_DelayMs(8);
}
Address += sizeof(buffer);
pBuffer += sizeof(buffer);
Size -= sizeof(buffer);
}
I2C_Start();
I2C_Write(0xA0);
I2C_Write((Address >> 8) & 0xFF);
I2C_Write((Address >> 0) & 0xFF);
I2C_WriteBuffer(pBuffer, 8);
I2C_Stop();
// give the EEPROM time to burn the data in (apparently takes 5ms)
SYSTEM_DelayMs(8);
}
+1 -2
View File
@@ -20,7 +20,6 @@
#include <stdint.h>
void EEPROM_ReadBuffer(uint16_t Address, void *pBuffer, uint8_t Size);
void EEPROM_WriteBuffer(uint16_t Address, const void *pBuffer);
void EEPROM_WriteBuffer(uint16_t Address, const void *pBuffer, uint8_t Size);
#endif
+22 -7
View File
@@ -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
@@ -59,13 +64,26 @@ static const AddrMapping_t ADDR_MAPPINGS[] = {
// MR FM * 128 Bytes (0x003000) 0x00A028 -> 0x00A0A8
// Settings * 80 Bytes (0x007000) 0x00A0A8 -> 0x00A0F8
// Settings * 56 Bytes (0x008000) 0x00A0F8 -> 0x00A130
// Settings Scanlist * 8 Bytes (0x009000) 0x00A130 -> 0x00A140
// Settings AES * 16 Bytes (0x00A000) 0x00A140 -> 0x00A150
// Settings Scanlist * 8 Bytes (0x009000) 0x00A130 -> 0x00A138
// Settings AES * 16 Bytes (0x00A000) 0x00A138 -> 0x00A148
// Settings Spectrum * 8 Bytes 0x00A148 -> 0x00A150
// Settings * 8 Bytes (0x00B000) 0x00A150 -> 0x00A158
// Settings F4HWN * 8 Bytes (0x00C000) 0x00A158 -> 0x00A160
// Settings Version * 16 Bytes 0x00A160 -> 0x00A170
_MK_MAPPING(0x010000, 0x00B000, 0x00B200), // Calibration 512 Bytes!!!
_MK_MAPPING(0x011000, 0x00C000, 0x00D000), // Boot Logo sector (4 KB):
// [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);
@@ -91,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
View File
@@ -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
View File
@@ -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
+252
View File
@@ -0,0 +1,252 @@
/* Copyright 2026 Armel F4HWN
* https://github.com/armel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Multiboot flash programmer.
*
* M1 (validated): brick-critical core - reprogram the internal application flash
* from an image held in the external SPI flash, running from RAM.
*
* M2 (this file): slot format + integrity validation. Each slot starts with a
* header (magic, size, CRC32, name, version); a restore validates the header and
* the image CRC32 *before* erasing anything, so an incompatible or corrupt image
* can never brick the radio. There is a single firmware for both the K1 and the
* K5v3 (the keypad difference is handled at runtime by the hidden SetNav menu),
* so no per-model guard is needed.
*/
#ifndef DRIVER_MB_FLASH_H
#define DRIVER_MB_FLASH_H
#include <stdint.h>
#include <stdbool.h>
/* Internal flash application region (see Core/py32f071xb.ld:
* FLASH origin 0x08002800, length 118 KiB). 0x08002800 is 256-byte aligned, so
* the whole region can be page-erased (256 B granularity) without touching the
* factory bootloader that lives just below it. */
#define MB_INT_APP_BASE 0x08002800u
#define MB_INT_APP_SIZE 0x0001D800u /* 118 KiB */
/* External SPI flash slot layout.
* Each 128 KiB slot = one header sector (4 KiB) followed by the image.
* Slot 0 is the firmware-managed BACKUP of the normally-flashed firmware
* (written by MB_BackupInternalToSlot0, protected from host writes); slots
* 1..4 are the user slots managed from UV Studio. */
#define MB_SLOT_STRIDE 0x00020000u /* 128 KiB per slot */
#define MB_SLOT_IMG_OFFSET 0x00001000u /* image starts after header sector */
#define MB_SLOT0_EXT_BASE 0x00020000u /* slot 0 (backup) header base */
#define MB_SLOT_COUNT 5u /* slot 0 backup + slots 1..4 */
#define MB_SLOT_BACKUP 0u /* slot 0 = firmware base backup */
/* Slot header (stored at the slot base, first 4 KiB sector). 64 bytes. */
#define MB_SLOT_MAGIC 0x31424D46u /* "FMB1" */
#define MB_HDR_VERSION 1u
#define MB_FLAG_COMMITTED (1u << 0) /* image written and verified */
#define MB_NAME_LEN 16
#define MB_VERSION_LEN 16
typedef struct __attribute__((packed)) {
uint32_t magic; /* MB_SLOT_MAGIC */
uint16_t hdr_version; /* MB_HDR_VERSION */
uint16_t flags; /* MB_FLAG_COMMITTED, ... */
uint32_t image_size; /* bytes, <= MB_INT_APP_SIZE */
uint32_t image_crc32; /* CRC-32 (zlib) over image_size B */
char name[MB_NAME_LEN]; /* human-readable, NUL-terminated */
char fw_version[MB_VERSION_LEN]; /* firmware version string */
uint8_t reserved[16]; /* pad to 64 bytes, future use */
} mb_slot_header_t;
/* Multiboot operation result (a successful restore resets before returning). */
enum {
MB_OK = 0,
MB_ERR_MAGIC, /* no/invalid slot header */
MB_ERR_VERSION, /* header format too new */
MB_ERR_NOT_COMMITTED,/* image not marked complete */
MB_ERR_SIZE, /* image_size out of range */
MB_ERR_CRC, /* image CRC32 mismatch */
MB_ERR_SPI, /* external flash read/write timed out*/
MB_ERR_SLOT, /* slot or bank index out of range */
MB_ERR_AUTH, /* write refused: timestamp mismatch */
MB_ERR_RAM_LOAD /* restore stub RAM copy mismatch */
};
/* CRC-32 (zlib) over a resident memory buffer. Multiboot and overlay apps
* share this implementation; keeping it here avoids carrying two identical
* bitwise CRC loops in the MCU flash. */
uint32_t MB_Crc32Bytes(const uint8_t *data, uint32_t len);
/* Multi-slot API used by the boot selector. Validation always covers the full
* image CRC before restore. progress_line may point to a 128-byte LCD page; the
* RAM copier then fills it while reflashing. Pass NULL to disable LCD updates.
* With ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY, the copier is loaded over the
* PY25Q16 sector cache only after validation and immediately before this call. */
uint8_t MB_ValidateSlot(uint8_t slot, mb_slot_header_t *out_header, uint32_t *out_crc);
uint8_t MB_RestoreSlot(uint8_t slot, uint8_t *progress_line);
/*
* Host-tool slot management (M4, "Firmware Slots" in UV Studio). Everything is
* bounds-checked (slot < MB_SLOT_COUNT, offset+len <= MB_SLOT_STRIDE) and writes
* touch the EXTERNAL flash only, so none of this is brick-critical: a bad slot is
* simply refused at restore by the CRC validation above.
*
* - MB_SlotInfo reads the 64-byte header only (fast, no CRC recompute) and
* returns MB_OK / MB_ERR_* describing the header state.
* - MB_SlotErase erases the whole 128 KiB slot region (header + image).
* - MB_SlotWrite programs `len` bytes at slot_base+offset. The slot MUST have
* been erased first (NOR flash only clears 1->0 bits); the host
* writes the image, then the COMMITTED header last.
* Use MB_ValidateSlot afterwards to confirm the full image CRC.
*/
uint8_t MB_SlotInfo(uint8_t slot, mb_slot_header_t *out_header);
uint8_t MB_SlotErase(uint8_t slot);
uint8_t MB_SlotWrite(uint8_t slot, uint32_t offset, const uint8_t *data, uint32_t len);
/* -------------------------------------------------------------------------- */
/* Config banks (one per slot by default, switchable via SetCfg). */
/* */
/* Each firmware slot gets its own config bank by default (memory channels, */
/* names, VFOs, settings) so switching firmware does not implicitly share or */
/* clobber settings between editions. SetCfg deliberately lets the user pick */
/* another bank after confirmation; compatibility is then the user's concern. */
/* The banking itself is a single address offset applied in the flash driver */
/* (PY25Q16_SetBankBase): everything below PY25Q16_BANK_SHARED_FROM */
/* (0x010000, the calibration boundary) is per-bank, at/above stays shared. */
/* Restoring slot N selects bank N initially, but slot and bank are tracked */
/* independently afterwards. Bank 0 reuses the historical config region at */
/* 0x000000 - no migration. */
/* */
/* External SPI flash (PY25Q16, 2 MiB) map: */
/* 0x000000 bank 0 config (channels/settings) ] per-bank */
/* 0x010000 calibration (512 B) ] shared */
/* 0x011000 boot logo (4 KiB) ] shared */
/* 0x020000 slot 0 firmware (BACKUP, 128 KiB) ] firmware-managed */
/* 0x040000 slot 1 firmware (128 KiB) ] */
/* 0x060000 slot 2 firmware ] user (UV Studio) */
/* 0x080000 slot 3 firmware ] */
/* 0x0A0000 slot 4 firmware ] */
/* 0x0C0000 bank 1 config (64 KiB) ] */
/* 0x0D0000 bank 2 config (64 KiB) ] per-bank */
/* 0x0E0000 bank 3 config (64 KiB) ] */
/* 0x0F0000 bank 4 config (64 KiB) ] */
/* 0x100000 multiboot state A (4 KiB marker) ] shared */
/* 0x101000 multiboot state B (4 KiB marker) ] redundant */
/* 0x102000 -- free ~888 KiB -- */
/* 0x1E0000 RX/TX log (32 KiB) ] shared */
/* */
/* Banks are 64 KiB for headroom; the live config footprint is ~44 KiB (max */
/* physical config address 0x00A170). Keep the config banks past the last */
/* slot if MB_SLOT_COUNT ever grows. */
#define MB_BANK_COUNT MB_SLOT_COUNT /* selectable config banks (0..4) */
#define MB_BANK_SIZE 0x00010000u /* 64 KiB per config bank */
#define MB_BANK1_EXT_BASE 0x000C0000u /* banks 1..4, right after slot 4 */
#define MB_STATE_A_BASE 0x00100000u /* redundant marker sector A */
#define MB_STATE_B_BASE 0x00101000u /* redundant marker sector B */
/* Active-state marker: two alternating external-flash sectors, never banked,
* outside the EEPROM logical map. A new record is verified in the inactive
* sector before it supersedes the previous one, so a power loss always leaves
* at least one usable state. The expected firmware identity is stored here as
* well: boot resolution never depends on the slot header remaining readable. */
#define MB_STATE_LEGACY_MAGIC 0x31504D46u /* "FMP1" (single 8-byte record) */
#define MB_STATE_V2_MAGIC 0x32504D46u /* "FMP2" (slot == config bank) */
#define MB_STATE_MAGIC 0x33504D46u /* "FMP3" (slot + bank separated) */
typedef struct __attribute__((packed)) {
uint32_t magic; /* MB_STATE_MAGIC */
uint32_t generation; /* monotonically increasing record version */
uint32_t image_size; /* expected internal image size */
uint32_t image_crc32; /* expected internal image CRC-32 */
uint8_t firmware_slot; /* exact source slot of the running image */
uint8_t slot_inv; /* ~firmware_slot, quick integrity check */
uint8_t config_bank; /* active config bank 0..MB_BANK_COUNT-1 */
uint8_t bank_inv; /* ~config_bank, quick integrity check */
uint32_t state_crc32; /* CRC-32 over all preceding fields */
} mb_state_t;
/* External-flash base of a config bank. Bank 0 -> 0 (historical
* region, identity map); banks 1..N -> past the slots. Out-of-range -> 0. */
uint32_t MB_BankBase(uint8_t bank);
/* Result of reading the active-state marker. A boot must treat these very
* differently: MISSING = fresh radio (adopting the running firmware as Main is
* fine); IO / CORRUPT = uncertain (must NOT overwrite Main). */
typedef enum {
MB_MARK_VALID = 0, /* valid FMP2/FMP3; *state normalized to FMP3 */
MB_MARK_LEGACY, /* valid FMP1 index; identity not stored */
MB_MARK_MISSING, /* read OK but both sectors erased */
MB_MARK_CORRUPT, /* read OK but magic/integrity bad */
MB_MARK_IO, /* could not be read (SPI error) */
} mb_mark_status_t;
/* Read the newest valid active-state marker, distinguishing the states above. */
mb_mark_status_t MB_ReadActiveState(mb_state_t *state);
/* Write the active-state marker into the inactive redundant sector and read it
* back to confirm it landed. The previous valid record is kept intact. The slot
* header supplies the expected internal firmware identity. Use this right before
* reflashing to `slot`: both the exact firmware slot and the initial config
* bank become `slot`. */
uint8_t MB_SetActiveSlot(uint8_t slot);
/* Switch ONLY the active settings bank, keeping the firmware that is running.
* Unlike MB_SetActiveSlot (which records that slot's image as the
* expected identity, for the imminent reflash to that slot), this preserves the
* running firmware's identity taken from the current marker and changes only the
* config bank. The next boot therefore maps a different bank with no
* reflash and is never mistaken for an out-of-multiboot firmware change (which
* would self-backup + reset to bank 0). Requires a currently valid marker -
* what every normal boot leaves behind - else MB_ERR_SPI / MB_ERR_MAGIC. The
* caller resets the MCU afterwards; the new bank takes effect at the next boot. */
uint8_t MB_SetActiveBank(uint8_t bank);
/* Erase a whole config bank (host "Reset config" for a user slot):
* the next boot using that bank reads 0xFF and re-seeds factory defaults. Bank 0
* (the base) is refused - reset it by factory-resetting the base firmware.
* External flash only, never brick-critical. */
uint8_t MB_BankErase(uint8_t bank);
/* -------------------------------------------------------------------------- */
/* Slot 0 self-backup (base firmware). */
/* -------------------------------------------------------------------------- */
/* Progress callback for the (slow) slot-0 self-backup; may be NULL. */
typedef void (*mb_progress_fn)(uint32_t done, uint32_t total);
/* Does the running internal firmware carry the DECLARED identity of `slot`
* (CRC32 over image_size == the slot header's image_crc32)? This checks the
* header's claim, not the stored image itself (that is validated once, right
* after a backup). IO is reported separately so a transient SPI error is never
* mistaken for a mismatch - which would wrongly trigger a self-backup over Main. */
typedef enum {
MB_FW_MATCH = 0, /* internal carries this slot's declared identity */
MB_FW_MISMATCH, /* reliably not this slot (or slot has no header) */
MB_FW_IO, /* slot header unreadable: uncertain */
} mb_fw_match_t;
mb_fw_match_t MB_InternalMatchesSlot(uint8_t slot);
/* Compare internal flash with the identity stored in a validated marker. */
bool MB_InternalMatchesState(const mb_state_t *state);
/* Back up the running internal firmware into slot 0 (erase + full image +
* COMMITTED header, name=edition, version) and validate the stored image by a
* full external CRC read-back. External flash only, never brick-critical; a
* failure (SPI or CRC) leaves the slot uncommitted and does not advance the
* marker, so boot resolution retries it. progress may be NULL. */
uint8_t MB_BackupInternalToSlot0(mb_progress_fn progress);
#endif /* DRIVER_MB_FLASH_H */
+89 -4
View File
@@ -27,6 +27,10 @@
#include "external/printf/printf.h"
#include "misc.h"
/* MBMARK was an on-screen SPI trace used while bringing up multiboot (M1/M2).
* The tracer is gone; keep the call sites as no-ops. */
#define MBMARK(s)
// #define DEBUG
#define SPIx SPI2
@@ -39,10 +43,42 @@
#define PAGE_SIZE 0x100
static uint32_t SectorCacheAddr = 0x1000000;
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT_OVERLAY
/* The restore-only RAM stub is copied over this cache immediately before it
* erases internal flash. A reset always follows, so the cache is never needed
* again after the overlay becomes active. */
static uint8_t SectorCache[SECTOR_SIZE]
__attribute__((section(".bss.mb_workspace"), aligned(4), used));
#else
static uint8_t SectorCache[SECTOR_SIZE];
#endif
static uint8_t BlackHole[4] __attribute__((aligned(4)));
static volatile bool TC_Flag;
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
/* Active settings-bank base (see py25q16.h). 0 = bank 0 / historical
* config region, i.e. an identity mapping. */
static uint32_t BankBase = 0;
void PY25Q16_SetBankBase(uint32_t Base)
{
BankBase = Base;
}
/* Redirect config-region accesses (addr < boundary) into the active bank.
* Calibration/logo/slots/marker (addr >= boundary) are returned unchanged.
* BankBase is sector-aligned, so alignment done by callers is preserved. */
static inline uint32_t BankMap(uint32_t Address)
{
return (Address < PY25Q16_BANK_SHARED_FROM) ? (Address + BankBase) : Address;
}
#else
static inline uint32_t BankMap(uint32_t Address)
{
return Address;
}
#endif
static inline void CS_Assert()
{
GPIO_ResetOutputPin(CS_PIN);
@@ -218,6 +254,7 @@ static void WriteEnable();
static void SectorErase(uint32_t Addr);
static void SectorProgram(uint32_t Addr, const uint8_t *Buf, uint32_t Size);
static void PageProgram(uint32_t Addr, const uint8_t *Buf, uint32_t Size);
static void ReadBufferRaw(uint32_t Address, void *pBuffer, uint32_t Size);
void PY25Q16_Init()
{
@@ -225,19 +262,23 @@ void PY25Q16_Init()
SPI_Init();
}
void PY25Q16_ReadBuffer(uint32_t Address, void *pBuffer, uint32_t Size)
static void ReadBufferRaw(uint32_t Address, void *pBuffer, uint32_t Size)
{
MBMARK("RD cmd"); // about to assert CS + send read command
CS_Assert();
SPI_WriteByte(0x03); // Send read command
MBMARK("RD addr"); // command sent, about to send address
WriteAddr(Address); // Send address (3 bytes)
MBMARK("RD flush"); // address sent, about to flush RX FIFO
// CRITICAL: Flush RX FIFO before DMA to remove residual data
while (LL_SPI_RX_FIFO_EMPTY != LL_SPI_GetRxFIFOLevel(SPIx))
{
LL_SPI_ReceiveData8(SPIx); // Read and discard
}
MBMARK("RD data"); // FIFO flushed, about to read the data
if (Size >= 16) {
SPI_ReadBuf((uint8_t *)pBuffer, Size);
} else {
@@ -247,11 +288,31 @@ void PY25Q16_ReadBuffer(uint32_t Address, void *pBuffer, uint32_t Size)
}
}
MBMARK("RD end"); // data read, about to release CS
CS_Release();
}
void PY25Q16_ReadBuffer(uint32_t Address, void *pBuffer, uint32_t Size)
{
ReadBufferRaw(BankMap(Address), pBuffer, Size);
}
// Like PY25Q16_ReadBuffer, but waits for the flash to be idle first (WIP=0),
// exactly as PY25Q16_WriteBuffer does before its internal reads. A standalone
// read issued while the chip is still busy from a prior program/erase never
// returns the expected data.
void PY25Q16_ReadBufferSafe(uint32_t Address, void *pBuffer, uint32_t Size)
{
MBMARK("SAFE wip"); // about to WaitWIP()
WaitWIP();
MBMARK("SAFE rb"); // WaitWIP done, about to ReadBuffer
PY25Q16_ReadBuffer(Address, pBuffer, Size);
}
void PY25Q16_WriteBuffer(uint32_t Address, const void *pBuffer, uint32_t Size, bool Append)
{
Address = BankMap(Address); /* map once; internal reads use *Raw below */
#ifdef DEBUG
printf("spi flash write: %06x %ld %d\n", Address, Size, Append);
#endif
@@ -277,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;
}
}
}
+38
View File
@@ -22,7 +22,45 @@
void PY25Q16_Init();
void PY25Q16_ReadBuffer(uint32_t Address, void *pBuffer, uint32_t Size);
void PY25Q16_ReadBufferSafe(uint32_t Address, void *pBuffer, uint32_t Size);
void PY25Q16_WriteBuffer(uint32_t Address, const void *pBuffer, uint32_t Size, bool Append);
void PY25Q16_SectorErase(uint32_t Address);
/* Drop the internal single-sector write cache. Call after erasing/programming
* flash behind the driver's back (e.g. the raw multiboot slot/bank ops) so a
* later write cannot skip or resurrect data based on a stale cached sector. It
* is also called before multiboot reuses the cache storage as a RAM overlay. */
void PY25Q16_InvalidateCache(void);
#ifdef ENABLE_FEAT_F4HWN_OVERLAY_APPS
/* The 4 KiB sector cache, reused as the overlay-app execution workspace. */
uint8_t *PY25Q16_OverlayBuffer(void);
#endif
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
/*
* Multiboot per-bank config banking.
*
* Each firmware slot gets its own config bank by default (memory channels,
* names, VFOs, settings), though SetCfg can point the running firmware at a
* different bank. A non-zero bank base transparently shifts every flash access
* BELOW PY25Q16_BANK_SHARED_FROM into the active bank; calibration, boot logo,
* firmware slots and the multiboot marker all live at/above that boundary and
* stay shared across every bank.
*
* This is the single choke point: both the EEPROM emulation (eeprom_compat.c)
* and the firmware's direct config reads/writes (settings.c) end up here, so
* one offset covers them all - no per-call-site patching.
*
* Set once at boot, before any settings read, from
* PY25Q16_SetBankBase(MB_BankBase(MB_BootResolveState()));
* and never changed again during a session (a slot restore or SetCfg selection
* records the next bank and resets first), so the banking itself never needs a
* cache flush. Raw bank erases behind the driver explicitly call
* PY25Q16_InvalidateCache().
*/
#define PY25Q16_BANK_SHARED_FROM 0x00010000u /* calibration boundary (see flash map) */
void PY25Q16_SetBankBase(uint32_t Base);
#endif
#endif
+19 -10
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+1 -1
View File
@@ -362,7 +362,7 @@ const uint8_t gFontSmall[95-1][6] =
{0x20, 0x40, 0x40, 0x3D, 0x00, 0x00}, // 'j'
{0x00, 0x7F, 0x10, 0x28, 0x44, 0x00}, // 'k'
{0x00, 0x00, 0x3F, 0x40, 0x00, 0x00}, // 'l'
{0x7C, 0x08, 0x10, 0x10, 0x08, 0x7C}, // 'm'
{0x7C, 0x04, 0x7C, 0x04, 0x78, 0x00}, // 'm'
{0x7C, 0x04, 0x04, 0x04, 0x78, 0x00}, // 'n'
{0x38, 0x44, 0x44, 0x44, 0x38, 0x00}, // 'o'
{0x7C, 0x14, 0x14, 0x14, 0x08, 0x00}, // 'p'
+35 -43
View File
@@ -17,13 +17,13 @@
#include <string.h>
#include "app/dtmf.h"
#if defined(ENABLE_FMRADIO)
#if defined(ENABLE_FMRADIO_EMBEDDED)
#include "app/fm.h"
#endif
#include "audio.h"
#include "dcs.h"
#include "driver/backlight.h"
#if defined(ENABLE_FMRADIO)
#if defined(ENABLE_FMRADIO_EMBEDDED)
#include "driver/bk1080.h"
#endif
#include "driver/bk4819.h"
@@ -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
@@ -151,36 +154,13 @@ void FUNCTION_Transmit()
// clear the DTMF RX live decoder buffer
gDTMF_RX_live_timeout = 0;
memset(gDTMF_RX_live, 0, sizeof(gDTMF_RX_live));
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
+1
View File
@@ -17,6 +17,7 @@
#ifndef FUNCTIONS_H
#define FUNCTIONS_H
#include <stdbool.h>
#include <stdint.h>
enum FUNCTION_Type_t
+13 -2
View File
@@ -17,6 +17,7 @@
#include <assert.h>
#include "battery.h"
#include "board.h"
#include "driver/backlight.h"
#include "driver/st7565.h"
#include "functions.h"
@@ -129,6 +130,16 @@ unsigned int BATTERY_VoltsToPercent(const unsigned int voltage_10mV)
return 0;
}
#if defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) || defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON)
void BATTERY_Sample(const bool bDisplayBatteryLevel)
{
BOARD_ADC_GetBatteryInfo(&gBatteryVoltages[gBatteryVoltageIndex++], &gBatteryCurrent);
if (gBatteryVoltageIndex > 3u)
gBatteryVoltageIndex = 0u;
BATTERY_GetReadings(bDisplayBatteryLevel);
}
#endif
void BATTERY_GetReadings(const bool bDisplayBatteryLevel)
{
const uint8_t PreviousBatteryLevel = gBatteryDisplayLevel;
@@ -228,7 +239,7 @@ void BATTERY_TimeSlice500ms(void)
if (lowBatteryCountdown < lowBatteryPeriod) {
if (lowBatteryCountdown == lowBatteryPeriod-1 && !gChargingWithTypeC && !gLowBatteryConfirmed) {
AUDIO_PlayBeep(BEEP_500HZ_60MS_DOUBLE_BEEP);
AUDIO_PlayBeep(BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL);
}
return;
}
@@ -241,7 +252,7 @@ void BATTERY_TimeSlice500ms(void)
// not on charge
if (!gLowBatteryConfirmed) {
AUDIO_PlayBeep(BEEP_500HZ_60MS_DOUBLE_BEEP);
AUDIO_PlayBeep(BEEP_500HZ_60MS_DOUBLE_BEEP_OPTIONAL);
#ifdef ENABLE_VOICE
AUDIO_SetVoiceID(0, VOICE_ID_LOW_VOLTAGE);
#endif
+3
View File
@@ -45,6 +45,9 @@ typedef enum {
unsigned int BATTERY_VoltsToPercent(unsigned int voltage_10mV);
#if defined(ENABLE_FEAT_F4HWN_OVERLAY_APPS) || defined(ENABLE_FEAT_F4HWN_FOXHUNT) || defined(ENABLE_FEAT_F4HWN_BEACON)
void BATTERY_Sample(bool bDisplayBatteryLevel);
#endif
void BATTERY_GetReadings(bool bDisplayBatteryLevel);
void BATTERY_TimeSlice500ms(void);
+33 -8
View File
@@ -29,20 +29,40 @@
#include "settings.h"
#include "ui/menu.h"
#include "ui/ui.h"
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
#include "ui/multiboot.h"
#endif
BOOT_Mode_t BOOT_GetMode(void)
{
unsigned int i;
KEY_Code_t Keys[2];
bool PttPressed[2];
/* Poll the keypad even without PTT: holding MENU alone enters multiboot.
* The two samples keep the same debounce rule as the legacy boot modes. */
for (i = 0; i < 2; i++)
{
if (!GPIO_IsPttPressed())
return BOOT_MODE_NORMAL; // PTT not pressed
PttPressed[i] = GPIO_IsPttPressed();
Keys[i] = KEYBOARD_Poll();
SYSTEM_DelayMs(20);
}
#ifdef ENABLE_FEAT_F4HWN_MULTIBOOT
if (!PttPressed[0] && !PttPressed[1] &&
Keys[0] == KEY_MENU && Keys[1] == KEY_MENU)
{
gKeyReading0 = Keys[0];
gKeyReading1 = Keys[0];
gDebounceCounter = 2;
return BOOT_MODE_MULTIBOOT;
}
#endif
/* All historical special modes still require PTT for both samples. */
if (!PttPressed[0] || !PttPressed[1])
return BOOT_MODE_NORMAL;
#ifdef ENABLE_FEAT_F4HWN_RESCUE_OPS
if (Keys[0] == (10 + gEeprom.SET_KEY))
{
@@ -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);
}
Loaded 100 of 180 files, more files were not shown because too many files have changed in this diff. Show more